diff --git a/.ci.yaml b/.ci.yaml new file mode 100644 index 000000000000..2f947ff38da1 --- /dev/null +++ b/.ci.yaml @@ -0,0 +1,16 @@ +# Describes the targets run in continuous integration environment. +# +# Flutter infra uses this file to generate a checklist of tasks to be performed +# for every commit. +# +# More information at: +# * https://github.com/flutter/cocoon/blob/master/scheduler/README.md +enabled_branches: + - master + +targets: + - name: Windows Plugins + builder: Windows Plugins + postsubmit: false + scheduler: luci + diff --git a/.ci/Dockerfile b/.ci/Dockerfile index 13ac087498d1..13fd48be9c14 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -6,7 +6,7 @@ RUN sudo apt-get install -y --no-install-recommends gnupg # Add repo for gcloud sdk and install it RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | \ - sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list + sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | \ sudo apt-key --keyring /usr/share/keyrings/cloud.google.gpg add - @@ -23,7 +23,21 @@ RUN yes | sdkmanager \ RUN yes | sdkmanager --licenses +# Install formatter. +RUN sudo apt-get install -y clang-format + +# Install xvfb to allow running headless +RUN sudo apt-get install -y xvfb libegl1-mesa +# Install Linux desktop build tool requirements. +RUN sudo apt-get install -y clang cmake ninja-build file pkg-config +# Install necessary libraries. +RUN sudo apt-get install -y libgtk-3-dev libblkid-dev liblzma-dev libgcrypt20-dev + # Add repo for Google Chrome and install it RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - RUN echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' | sudo tee /etc/apt/sources.list.d/google-chrome.list RUN sudo apt-get update && sudo apt-get install -y --no-install-recommends google-chrome-stable + +# Make Chrome the default so http: has a handler for url_launcher tests. +RUN sudo apt-get install -y xdg-utils +RUN xdg-settings set default-web-browser google-chrome.desktop diff --git a/.ci/Dockerfile-LinuxDesktop b/.ci/Dockerfile-LinuxDesktop deleted file mode 100644 index 63e4516e26fc..000000000000 --- a/.ci/Dockerfile-LinuxDesktop +++ /dev/null @@ -1,31 +0,0 @@ -FROM cirrusci/flutter:stable - -RUN sudo apt-get update -y - -RUN sudo apt-get install -y --no-install-recommends gnupg - -# Add repo for gcloud sdk and install it -RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | \ - sudo tee -a /etc/apt/sources.list.d/google-cloud-sdk.list - -RUN curl https://packages.cloud.google.com/apt/doc/apt-key.gpg | \ - sudo apt-key --keyring /usr/share/keyrings/cloud.google.gpg add - - -RUN sudo apt-get update && sudo apt-get install -y google-cloud-sdk && \ - gcloud config set core/disable_usage_reporting true && \ - gcloud config set component_manager/disable_update_check true - -# Install xvfb to allow running headless -RUN sudo apt-get install -y xvfb libegl1-mesa -# Install Linux desktop build tool requirements. -RUN sudo apt-get install -y clang cmake ninja-build file pkg-config -# Install necessary libraries. -RUN sudo apt-get install -y libgtk-3-dev - -# Add repo for Google Chrome and install it, for url_launcher tests. -RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add - -RUN echo 'deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main' | sudo tee /etc/apt/sources.list.d/google-chrome.list -RUN sudo apt-get update && sudo apt-get install -y --no-install-recommends google-chrome-stable -# Make it the default so http: has a handler. -RUN sudo apt-get install -y xdg-utils -RUN xdg-settings set default-web-browser google-chrome.desktop diff --git a/.ci/dev/README.md b/.ci/dev/README.md deleted file mode 100644 index d266afbf2687..000000000000 --- a/.ci/dev/README.md +++ /dev/null @@ -1,19 +0,0 @@ -This directory contains resources that the Flutter team uses during -the development of plugins. - -## Luci builder file -`try_builders.json` contains the supported luci try builders -for plugins. It follows format: -```json -{ - "builders":[ - { - "name":"yyy", - "repo":"plugins", - "enabled":true - } - ] -} -``` -This file will be mainly used in [`flutter/cocoon`](https://github.com/flutter/cocoon) -to trigger/update pre-submit luci tasks. diff --git a/.ci/dev/try_builders.json b/.ci/dev/try_builders.json deleted file mode 100644 index 6334a3d64fa4..000000000000 --- a/.ci/dev/try_builders.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "builders":[ - { - "name":"Windows Plugins", - "repo":"plugins", - "enabled":true - } - ] -} diff --git a/.cirrus.yml b/.cirrus.yml index 872cfc2316fa..91a656226f38 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,65 +1,128 @@ +gcp_credentials: ENCRYPTED[ec898795b6f1b54f9cc2ab4104909f1053651f65fcab96397cfdc33dae6df5fd0fa972e29ba19f4f95125de844ab1641] + +# Don't run on release tags since it creates O(n^2) tasks where n is the +# number of plugins +only_if: $CIRRUS_TAG == '' +env: + CHANNEL: "master" # Default to master when not explicitly set by a task. + PLUGIN_TOOLS: "dart run ./script/tool/lib/src/main.dart" + +tool_setup_template: &TOOL_SETUP_TEMPLATE + tool_setup_script: + - git fetch origin master # 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: + - flutter channel $CHANNEL + - flutter upgrade + << : *TOOL_SETUP_TEMPLATE + +macos_template: &MACOS_TEMPLATE + # Only one macOS task can run in parallel without credits, so use them for + # PRs on macOS. + use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' + osx_instance: + image: big-sur-xcode-12.5 + cocoapod_install_script: sudo gem install cocoapods + +# Light-workload Linux tasks. +# These use default machines, with fewer CPUs, to reduce pressure on the +# concurrency limits. task: - # don't run on release tags since it creates O(n^2) tasks where n is the number of plugins - only_if: $CIRRUS_TAG == '' - use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' && $CIRRUS_PR == '' - container: + << : *FLUTTER_UPGRADE_TEMPLATE + gke_container: dockerfile: .ci/Dockerfile - cpu: 8 - memory: 16G - env: - INTEGRATION_TEST_PATH: "./packages/integration_test" - upgrade_script: - - flutter channel stable - - flutter upgrade - - flutter channel master - - flutter upgrade - - git fetch origin master - activate_script: pub global activate flutter_plugin_tools + builder_image_name: docker-builder # gce vm image + builder_image_project: flutter-cirrus + cluster_name: test-cluster + zone: us-central1-a + namespace: default matrix: + ### Platform-agnostic tasks ### + - name: plugin_tools_tests + script: + - cd script/tool + - CIRRUS_BUILD_ID=null pub run test - name: publishable script: - - flutter channel master - - ./script/check_publish.sh + - ./script/tool_runner.sh version-check + - ./script/tool_runner.sh publish-check - name: format - install_script: - - wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - - - sudo apt-add-repository "deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial-7 main" - - sudo apt-get update - - sudo apt-get install -y --allow-unauthenticated clang-format-7 - format_script: ./script/incremental_build.sh format --travis --clang-format=clang-format-7 + format_script: ./script/tool_runner.sh format --fail-on-change + pubspec_script: ./script/tool_runner.sh pubspec-check + license_script: + - dart script/tool/lib/src/main.dart license-check - name: test env: matrix: CHANNEL: "master" CHANNEL: "stable" test_script: - # TODO(jackson): Allow web plugins once supported on stable - # https://github.com/flutter/flutter/issues/42864 - - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi - - flutter channel $CHANNEL - - ./script/incremental_build.sh test + - ./script/tool_runner.sh test - name: analyze - script: ./script/incremental_build.sh analyze + env: + matrix: + CHANNEL: "master" + CHANNEL: "stable" + tool_script: + - cd script/tool + - dart analyze --fatal-infos + script: + - ./script/tool_runner.sh analyze + ### Android tasks ### - name: build_all_plugins_apk + env: + matrix: + CHANNEL: "master" + CHANNEL: "stable" script: - # TODO(jackson): Allow web plugins once supported on stable - # https://github.com/flutter/flutter/issues/42864 - - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi - - flutter channel $CHANNEL - ./script/build_all_plugins_app.sh apk - - name: integration_web_smoke_test - # Tests integration example test in web. - only_if: "changesInclude('.cirrus.yml', 'packages/integration_test/**') || $CIRRUS_PR == ''" - install_script: - - flutter config --enable-web - - git clone https://github.com/flutter/web_installers.git - - cd web_installers/packages/web_drivers/ - - pub get - - dart lib/web_driver_installer.dart chromedriver --install-only - - ./chromedriver/chromedriver --port=4444 & - test_script: - - cd $INTEGRATION_TEST_PATH/example/ - - flutter drive -v --driver=test_driver/integration_test.dart --target=integration_test/example_test.dart -d web-server --release --browser-name=chrome + ### Web tasks ### + - name: build_all_plugins_web + env: + matrix: + CHANNEL: "master" + CHANNEL: "stable" + script: + - ./script/build_all_plugins_app.sh web + ### Linux desktop tasks ### + - name: build_all_plugins_linux + env: + matrix: + CHANNEL: "master" + CHANNEL: "stable" + script: + - flutter config --enable-linux-desktop + - ./script/build_all_plugins_app.sh linux + - name: build-linux+drive-examples + env: + matrix: + CHANNEL: "master" + CHANNEL: "stable" + build_script: + - flutter config --enable-linux-desktop + - ./script/tool_runner.sh build-examples --linux + drive_script: + - xvfb-run ./script/tool_runner.sh drive-examples --linux + +# Heavy-workload Linux tasks. +# These use machines with more CPUs and memory, so will reduce parallelization +# for non-credit runs. +task: + << : *FLUTTER_UPGRADE_TEMPLATE + gke_container: + dockerfile: .ci/Dockerfile + builder_image_name: docker-builder # gce vm image + builder_image_project: flutter-cirrus + cluster_name: test-cluster + zone: us-central1-a + namespace: default + cpu: 4 + memory: 12G + matrix: + ### Android tasks ### - name: build-apks+java-test+firebase-test-lab env: matrix: @@ -73,10 +136,6 @@ task: MAPS_API_KEY: ENCRYPTED[596a9f6bca436694625ac50851dc5da6b4d34cba8025f7db5bc9465142e8cd44e15f69e3507787753accebfc4910d550] GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[07586610af1fdfc894e5969f70ef2458341b9b7e9c3b7c4225a663b4a48732b7208a4d91c3b7d45305a6b55fa2a37fc4] script: - # TODO(jackson): Allow web plugins once supported on stable - # https://github.com/flutter/flutter/issues/42864 - - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi - - flutter channel $CHANNEL # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they # might include non-ASCII characters which makes Gradle crash. # See: https://github.com/flutter/flutter/issues/24935 @@ -87,82 +146,60 @@ task: - echo "$CIRRUS_COMMIT_MESSAGE" > /tmp/cirrus_commit_message.txt - export CIRRUS_CHANGE_MESSAGE="" - export CIRRUS_COMMIT_MESSAGE="" - - ./script/incremental_build.sh build-examples --apk - - ./script/incremental_build.sh java-test # must come after apk build - - if [[ $GCLOUD_FIREBASE_TESTLAB_KEY == ENCRYPTED* ]]; then - - echo "This user does not have permission to run Firebase Test Lab tests." - - else + - ./script/tool_runner.sh build-examples --apk + - ./script/tool_runner.sh java-test # must come after apk build + - if [[ -n "$GCLOUD_FIREBASE_TESTLAB_KEY" ]]; then - echo $GCLOUD_FIREBASE_TESTLAB_KEY > ${HOME}/gcloud-service-key.json - - ./script/incremental_build.sh firebase-test-lab --device model=flame,version=29 --device model=starqlteue,version=26 + - ./script/tool_runner.sh firebase-test-lab --device model=flame,version=29 --device model=starqlteue,version=26 + - else + - echo "This user does not have permission to run Firebase Test Lab tests." - fi - export CIRRUS_CHANGE_MESSAGE=`cat /tmp/cirrus_change_message.txt` - export CIRRUS_COMMIT_MESSAGE=`cat /tmp/cirrus_commit_message.txt` - -task: - # don't run on release tags since it creates O(n^2) tasks where n is the number of plugins - only_if: $CIRRUS_TAG == '' - use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' && $CIRRUS_PR == '' - container: - dockerfile: .ci/Dockerfile-LinuxDesktop - cpu: 8 - memory: 16G - env: - INTEGRATION_TEST_PATH: "./packages/integration_test" - upgrade_script: - - flutter channel stable - - flutter upgrade - - flutter channel master - - flutter upgrade - - git fetch origin master - activate_script: pub global activate flutter_plugin_tools - matrix: - - name: build-linux+drive-examples + ### Web tasks ### + - name: build-web+drive-examples + env: + # Currently missing; see https://github.com/flutter/flutter/issues/81982 + # and https://github.com/flutter/flutter/issues/82211 + PLUGINS_TO_EXCLUDE_INTEGRATION_TESTS: "file_selector,image_picker_for_web,shared_preferences_web" + matrix: + CHANNEL: "master" + CHANNEL: "stable" install_script: - - flutter config --enable-linux-desktop + - git clone https://github.com/flutter/web_installers.git + - cd web_installers/packages/web_drivers/ + - dart pub get + - dart lib/web_driver_installer.dart chromedriver --install-only + - ./chromedriver/chromedriver --port=4444 & build_script: - # TODO(stuartmorgan): Include stable once Linux is supported on stable. - - flutter channel master - - ./script/incremental_build.sh build-examples --linux - - xvfb-run ./script/incremental_build.sh drive-examples --linux + - ./script/tool_runner.sh build-examples --web + drive_script: + - ./script/tool_runner.sh drive-examples --web --exclude $PLUGINS_TO_EXCLUDE_INTEGRATION_TESTS +# macOS tasks. task: - # don't run on release tags since it creates O(n^2) tasks where n is the number of plugins - only_if: $CIRRUS_TAG == '' - use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' - osx_instance: - image: catalina-xcode-11.3.1-flutter - upgrade_script: - - flutter channel stable - - flutter upgrade - - flutter channel master - - flutter upgrade - - git fetch origin master - activate_script: pub global activate flutter_plugin_tools - create_simulator_script: - - xcrun simctl list - - xcrun simctl create Flutter-iPhone com.apple.CoreSimulator.SimDeviceType.iPhone-X com.apple.CoreSimulator.SimRuntime.iOS-13-3 | xargs xcrun simctl boot + << : *MACOS_TEMPLATE + << : *FLUTTER_UPGRADE_TEMPLATE matrix: - - name: build_all_plugins_ipa - script: - # TODO(jackson): Allow web plugins once supported on stable - # https://github.com/flutter/flutter/issues/42864 - - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi - - flutter channel $CHANNEL - - ./script/build_all_plugins_app.sh ios --no-codesign + ### iOS+macOS tasks *** - name: lint_darwin_plugins + script: + - ./script/tool_runner.sh podspecs + ### iOS tasks ### + - name: build_all_plugins_ipa env: matrix: - PLUGIN_SHARDING: "--shardIndex 0 --shardCount 2" - PLUGIN_SHARDING: "--shardIndex 1 --shardCount 2" + CHANNEL: "master" + CHANNEL: "stable" script: - # TODO(jmagman): Lint macOS podspecs but skip any that fail library validation. - - find . -name "*.podspec" | xargs grep -l "osx" | xargs rm - # Skip the dummy podspecs used to placate the tool. - - find . -name "*_web*.podspec" -o -name "*_mac*.podspec" | xargs rm - - ./script/incremental_build.sh podspecs + - ./script/build_all_plugins_app.sh ios --no-codesign - name: build-ipas+drive-examples env: PATH: $PATH:/usr/local/bin + # in_app_purchase_ios is currently missing tests; see https://github.com/flutter/flutter/issues/81695 + # ios_platform_images is currently missing tests; see https://github.com/flutter/flutter/issues/82208 + # sensor hangs on CI. + PLUGINS_TO_EXCLUDE_INTEGRATION_TESTS: "in_app_purchase_ios,ios_platform_images,sensors" matrix: PLUGIN_SHARDING: "--shardIndex 0 --shardCount 4" PLUGIN_SHARDING: "--shardIndex 1 --shardCount 4" @@ -172,35 +209,40 @@ task: CHANNEL: "master" CHANNEL: "stable" 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 build_script: - # TODO(jackson): Allow web plugins once supported on stable - # https://github.com/flutter/flutter/issues/42864 - - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi - - flutter channel $CHANNEL - - ./script/incremental_build.sh build-examples --ipa - - ./script/incremental_build.sh drive-examples -task: - # don't run on release tags since it creates O(n^2) tasks where n is the number of plugins - only_if: $CIRRUS_TAG == '' - use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' - osx_instance: - image: catalina-xcode-11.3.1-flutter - setup_script: - - flutter config --enable-macos-desktop - upgrade_script: - - flutter channel master - - flutter upgrade - - git fetch origin master - activate_script: pub global activate flutter_plugin_tools - matrix: - - name: build_all_plugins_app + - ./script/tool_runner.sh build-examples --ipa + xctest_script: + - ./script/tool_runner.sh xctest --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 `xctest`, changing the order will result ci failure. + - ./script/tool_runner.sh drive-examples --ios --exclude $PLUGINS_TO_EXCLUDE_INTEGRATION_TESTS + ### macOS desktop tasks ### + - name: build_all_plugins_macos + env: + matrix: + CHANNEL: "master" + CHANNEL: "stable" script: - - flutter channel master + - flutter config --enable-macos-desktop - ./script/build_all_plugins_app.sh macos - - name: build-apps+drive-examples + - name: build-macos+drive-examples env: + # conncectivity_macos is deprecated, so is not getting unit test backfill. + # package_info is deprecated, so is not getting unit test backfill. + PLUGINS_TO_EXCLUDE_MACOS_XCTESTS: "connectivity_macos,package_info" + matrix: + CHANNEL: "master" + CHANNEL: "stable" PATH: $PATH:/usr/local/bin build_script: - - flutter channel master - - ./script/incremental_build.sh build-examples --macos --no-ipa - - ./script/incremental_build.sh drive-examples --macos + - flutter config --enable-macos-desktop + - ./script/tool_runner.sh build-examples --macos --no-ipa + xctest_script: + - ./script/tool_runner.sh xctest --macos --exclude $PLUGINS_TO_EXCLUDE_MACOS_XCTESTS + drive_script: + - ./script/tool_runner.sh drive-examples --macos diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 083e125b32d2..3972cd29b8c7 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,37 +1,32 @@ -## Description +*Replace this paragraph with a description of what this PR is changing or adding, and why. Consider including before/after screenshots.* -*Replace this paragraph with a description of what this PR is doing. If you're modifying existing behavior, describe the existing behavior, how this PR is changing it, and what motivated the change.* +*List which issues are fixed by this PR. You must list at least one issue.* -## Related Issues +*If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* -*Replace this paragraph with a list of issues related to this PR from the [issue database](https://github.com/flutter/flutter/issues). Indicate, which of these issues are resolved or fixed by this PR. Note that you'll have to prefix the issue numbers with flutter/flutter#.* - -## Checklist - -Before you create this PR confirm that it meets all requirements listed below by checking the relevant checkboxes (`[x]`). This will ensure a smooth and quick review process. +## Pre-launch Checklist - [ ] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. -- [ ] My PR includes unit or integration tests for *all* changed/updated/fixed behaviors (See [Contributor Guide]). -- [ ] All existing and new tests are passing. -- [ ] I updated/added relevant documentation (doc comments with `///`). -- [ ] The analyzer (`flutter analyze`) does not report any problems on my PR. -- [ ] I read and followed the [Flutter Style Guide]. -- [ ] The title of the PR starts with the name of the plugin surrounded by square brackets, e.g. [shared_preferences] +- [ ] I read the [Tree Hygiene] wiki page, which explains my responsibilities. +- [ ] I read and followed the [relevant style guides] and ran [the auto-formatter]. (Note that 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 with an appropriate new version according to the [pub versioning philosophy]. - [ ] I updated CHANGELOG.md to add a description of the change. -- [ ] I signed the [CLA]. -- [ ] I am willing to follow-up on review comments in a timely manner. - -## Breaking Change - -Does your PR require plugin users to manually update their apps to accommodate your change? +- [ ] 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. +- [ ] All existing and new tests are passing. -- [ ] Yes, this is a breaking change (please indicate a breaking change in CHANGELOG.md and increment major revision). -- [ ] No, this is *not* a breaking change. +If you need help, consider asking for advice on the #hackers-new channel on [Discord]. -[issue database]: https://github.com/flutter/flutter/issues [Contributor Guide]: https://github.com/flutter/plugins/blob/master/CONTRIBUTING.md -[Flutter Style Guide]: https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo -[pub versioning philosophy]: https://www.dartlang.org/tools/pub/versioning +[Tree Hygiene]: https://github.com/flutter/flutter/wiki/Tree-hygiene +[relevant style guides]: https://github.com/flutter/plugins/blob/master/CONTRIBUTING.md#style [CLA]: https://cla.developers.google.com/ +[flutter/tests]: https://github.com/flutter/tests +[breaking change policy]: https://github.com/flutter/flutter/wiki/Tree-hygiene#handling-breaking-changes +[Discord]: https://github.com/flutter/flutter/wiki/Chat +[pub versioning philosophy]: https://dart.dev/tools/pub/versioning +[the auto-formatter]: https://github.com/flutter/plugins/blob/master/script/tool/README.md#format-code diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 000000000000..38ee94c004f9 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,104 @@ +'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/**/* + +'p: file_selector': + - packages/file_selector/**/* + +'p: flutter_plugin_android_lifecycle': + - packages/flutter_plugin_android_lifecycle/**/* + +'p: google_maps_flutter': + - packages/google_maps_flutter/**/* + +'p: google_sign_in': + - packages/google_sign_in/**/* + +'p: image_picker': + - packages/image_picker/**/* + +'p: in_app_purchase': + - packages/in_app_purchase/**/* + +'p: ios_platform_images': + - packages/ios_platform_images/**/* + +'p: local_auth': + - packages/local_auth/**/* + +'p: package_info': + - packages/package_info/**/* + +'p: path_provider': + - packages/path_provider/**/* + +'p: plugin_platform_interface': + - packages/plugin_platform_interface/**/* + +'p: quick_actions': + - packages/quick_actions/**/* + +'p: sensors': + - packages/sensors/**/* + +'p: share': + - packages/share/**/* + +'p: shared_preferences': + - packages/shared_preferences/**/* + +'p: url_launcher': + - packages/url_launcher/**/* + +'p: video_player': + - packages/video_player/**/* + +'p: webview_flutter': + - packages/webview_flutter/**/* + +'p: wifi_info_flutter': + - packages/wifi_info_flutter/**/* + +'platform-android': + - packages/*/*_android/**/* + - packages/**/android/**/* + +'platform-ios': + - packages/*/*_ios/**/* + - packages/**/ios/**/* + +'platform-linux': + - packages/*/*_linux/**/* + - packages/**/linux/**/* + +'platform-macos': + - packages/*/*_macos/**/* + - packages/**/macos/**/* + +'platform-web': + - packages/*/*_web/**/* + - packages/**/web/**/* + +'platform-windows': + - packages/*/*_windows/**/* + - packages/**/windows/**/* diff --git a/.github/post_merge_labeler.yml b/.github/post_merge_labeler.yml new file mode 100644 index 000000000000..bb14486c8749 --- /dev/null +++ b/.github/post_merge_labeler.yml @@ -0,0 +1,2 @@ +'needs-publishing': + - packages/**/pubspec.yaml diff --git a/.github/workflows/pull_request_label.yml b/.github/workflows/pull_request_label.yml new file mode 100644 index 000000000000..6b93864d3f3a --- /dev/null +++ b/.github/workflows/pull_request_label.yml @@ -0,0 +1,31 @@ +# This workflow applies labels to pull requests based on the +# paths that are modified in the pull request. +# +# Edit `.github/labeler.yml` and `.github/post_merge_labeler.yml` +# to configure labels. +# +# For more information, see: https://github.com/actions/labeler + +name: Pull Request Labeler + +on: + pull_request_target: + types: [opened, synchronize, reopened, closed] + +jobs: + label: + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@9794b1493b6f1fa7b006c5f8635a19c76c98be95 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + sync-labels: true + + post_merge_label: + if: github.event.action == 'closed' && github.event.pull_request.merged == true + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@9794b1493b6f1fa7b006c5f8635a19c76c98be95 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + configuration-path: .github/post_merge_labeler.yml diff --git a/.gitignore b/.gitignore index 88ce490e4701..f4fa0b9b7795 100644 --- a/.gitignore +++ b/.gitignore @@ -11,12 +11,12 @@ flutter_export_environment.sh examples/all_plugins/pubspec.yaml -Podfile Podfile.lock Pods/ .symlinks/ **/Flutter/App.framework/ **/Flutter/ephemeral/ +**/Flutter/Flutter.podspec **/Flutter/Flutter.framework/ **/Flutter/Generated.xcconfig **/Flutter/flutter_assets/ @@ -37,6 +37,7 @@ gradle-wrapper.jar generated_plugin_registrant.dart GeneratedPluginRegistrant.h GeneratedPluginRegistrant.m +generated_plugin_registrant.cc GeneratedPluginRegistrant.java GeneratedPluginRegistrant.swift build/ @@ -45,3 +46,6 @@ build/ .project .classpath .settings + +# Downloaded by the plugin tools. +google-java-format-1.3-all-deps.jar diff --git a/AUTHORS b/AUTHORS index 51345c9a3481..0ca697b6a756 100644 --- a/AUTHORS +++ b/AUTHORS @@ -4,6 +4,7 @@ # Name/Organization Google Inc. +The Chromium Authors German Saprykin Benjamin Sauer larsenthomasj@gmail.com @@ -59,3 +60,8 @@ Kazuki Yamaguchi Eitan Schwartz Chris Rutkowski Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Daniel Roek diff --git a/CODEOWNERS b/CODEOWNERS index 160c0d5a3d10..1d52dcefcbef 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -4,23 +4,10 @@ # These names are just suggestions. It is fine to have your changes # reviewed by someone else. -packages/android_alarm_manager/** @bkonyi -packages/android_intent/** @mklim @matthew-carroll -packages/battery/** @amirh @matthew-carroll + packages/camera/** @bparrishMines -packages/connectivity/** @cyanglaz @matthew-carroll -packages/device_info/** @matthew-carroll -packages/espresso/** @collinjackson @adazh packages/file_selector/** @ditman packages/google_maps_flutter/** @cyanglaz -packages/google_sign_in/** @cyanglaz @mehmetf packages/image_picker/** @cyanglaz -packages/integration_test/** @dnfield -packages/in_app_purchase/** @mklim @cyanglaz @LHLL +packages/in_app_purchase/** @cyanglaz @LHLL packages/ios_platform_images/** @gaaclarke -packages/package_info/** @cyanglaz @matthew-carroll -packages/path_provider/** @matthew-carroll -packages/shared_preferences/** @matthew-carroll -packages/url_launcher/** @mklim -packages/video_player/** @iskakaushik @cyanglaz -packages/webview_flutter/** @amirh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f58dfea6a065..a5fc990033ba 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,220 +1,77 @@ # Contributing to Flutter Plugins - [![Build Status](https://api.cirrus-ci.com/github/flutter/plugins.svg)](https://cirrus-ci.com/github/flutter/plugins/master) -_See also: [Flutter's code of conduct](https://flutter.io/design-principles/#code-of-conduct)_ - -## Things you will need - - - * Linux, Mac OS X, or Windows. - * git (used for source version control). - * An ssh client (used to authenticate with GitHub). - -## Getting the code and configuring your environment - - - * Ensure all the dependencies described in the previous section are installed. - * Fork `https://github.com/flutter/plugins` into your own GitHub account. If - you already have a fork, and are now installing a development environment on - a new machine, make sure you've updated your fork so that you don't use stale - configuration options from long ago. - * If you haven't configured your machine with an SSH key that's known to github, then - follow [GitHub's directions](https://help.github.com/articles/generating-ssh-keys/) - to generate an SSH key. - * `git clone git@github.com:/plugins.git` - * `cd plugins` - * `git remote add upstream git@github.com:flutter/plugins.git` (So that you - fetch from the master repository, not your clone, when running `git fetch` - et al.) - -## Running the examples - - -To run an example with a prebuilt binary from the cloud, switch to that -example's directory, run `pub get` to make sure its dependencies have been -downloaded, and use `flutter run`. Make sure you have a device connected over -USB and debugging enabled on that device. - - * `cd packages/battery/example` - * `flutter run` - -## Running the tests - -### Integration tests - -To run the integration tests using Flutter driver: - -```console -cd example -flutter drive test_driver/.dart -``` - -To run integration tests as instrumentation tests on a local Android device: - -```console -cd example -flutter build apk -cd android && ./gradlew -Ptarget=$(pwd)/../test_driver/_test.dart app:connectedAndroidTest -``` - -These tests may also be in folders just named "test," or have filenames ending -with "e2e". - -### Dart unit tests - -To run the unit tests: - -```console -flutter test test/_test.dart -``` - -### Java unit tests - -These can be ran through Android Studio once the example app is opened as an -Android project. - -Without Android Studio, they can be ran through the terminal. - -```console -cd example -flutter build apk -cd android -./gradlew test -``` - -## Contributing code - -We gladly accept contributions via GitHub pull requests. - -Please peruse our -[style guide](https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo) and -[design principles](https://flutter.io/design-principles/) before -working on anything non-trivial. These guidelines are intended to -keep the code consistent and avoid common pitfalls. - -To start working on a patch: - - * `git fetch upstream` - * `git checkout upstream/master -b ` - * Hack away. - * Verify changes with [flutter_plugin_tools](https://pub.dartlang.org/packages/flutter_plugin_tools) -``` -pub global activate flutter_plugin_tools -pub global run flutter_plugin_tools format --plugins plugin_name -pub global run flutter_plugin_tools analyze --plugins plugin_name -pub global run flutter_plugin_tools test --plugins plugin_name -``` - * `git commit -a -m ""` - * `git push origin ` - -To send us a pull request: - -* `git pull-request` (if you are using [Hub](http://github.com/github/hub/)) or - go to `https://github.com/flutter/plugins` and click the - "Compare & pull request" button - -Please make sure all your checkins have detailed commit messages explaining the patch. - -Plugins tests are run automatically on contributions using Cirrus CI. However, due to -cost constraints, pull requests from non-committers may not run all the tests -automatically. - -Once you've gotten an LGTM from a project maintainer and once your PR has received -the green light from all our automated testing, wait for one the package maintainers -to merge the pull request and `pub submit` any affected packages. - -You must complete the -[Contributor License Agreement](https://cla.developers.google.com/clas). -You can do this online, and it only takes a minute. -If you've never submitted code before, you must add your (or your -organization's) name and contact info to the [AUTHORS](AUTHORS) file. +_See also: [Flutter's code of conduct](https://github.com/flutter/flutter/blob/master/CODE_OF_CONDUCT.md)_ + +## Welcome + +For an introduction to contributing to Flutter, see [our contributor +guide](https://github.com/flutter/flutter/blob/master/CONTRIBUTING.md). + +Additional resources specific to the plugins repository: +- [Setting up the Plugins development + environment](https://github.com/flutter/flutter/wiki/Setting-up-the-Plugins-development-environment), + which covers the setup process for this repository. +- [Plugins repository structure](https://github.com/flutter/flutter/wiki/Plugins-and-Packages-repository-structure), + to get an overview of how this repository is laid out. +- [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 + this is critical to read before submitting a PR. + +## 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 + +Flutter plugins follow Google style—or Flutter style for Dart—for the languages they +use, and use auto-formatters: +- [Dart](https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo) formatted + with `dart format` +- [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) + 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 -* This is a new process we are currently experimenting with, feedback on the process is welcomed at the Gitter contributors channel. * - -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. +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. - * **Closing the PR** - if the PR maintainer decides that the PR should not be merged. - * **Moving the PR to the backlog** - if the review requires non trivial effort and the issue isn't a priority; in this case the maintainer will: - * Make sure that the PR has an associated issue labeled with "plugin". + * **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: + * **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 -### The release process - -We push releases manually. Generally every merged PR upgrades at least one -plugin's `pubspec.yaml`, so also needs to be published as a package release. The -Flutter team member most involved with the PR should be the person responsible -for publishing the package release. In cases where the PR is authored by a -Flutter maintainer, the publisher should probably be the author. In other cases -where the PR is from a contributor, it's up to the reviewing Flutter team member -to publish the release instead. - -Some things to keep in mind before publishing the release: - -- Has CI ran on the master commit and gone green? Even if CI shows as green on - the PR it's still possible for it to fail on merge, for multiple reasons. - There may have been some bug in the merge that introduced new failures. CI - runs on PRs as it's configured on their branch state, and not on tip of tree. - CI on PRs also only runs tests for packages that it detects have been directly - changed, vs running on every single package on master. -- [Publishing is - forever.](https://dart.dev/tools/pub/publishing#publishing-is-forever) - Hopefully any bugs or breaking in changes in this PR have already been caught - in PR review, but now's a second chance to revert before anything goes live. -- "Don't deploy on a Friday." Consider carefully whether or not it's worth - immediately publishing an update before a stretch of time where you're going - to be unavailable. There may be bugs with the release or questions about it - from people that immediately adopt it, and uncovering and resolving those - support issues will take more time if you're unavailable. - -Releasing a package is a two-step process. - -1. Push the package update to [pub.dev](https://pub.dev) using `pub publish`. -2. Tag the commit with git in the format of `-v`, - and then push the tag to the `flutter/plugins` master branch. This can be - done manually with `git tag $tagname && git push upstream $tagname` while - checked out on the commit that updated `version` in `pubspec.yaml`. - -We've recently updated -[flutter_plugin_tools](https://github.com/flutter/plugin_tools) to wrap both of -those steps into one command to make it a little easier. This new tool is -experimental. Feel free to fall back on manually running `pub publish` and -creating and pushing the tag in git if there are issues with it. - -Install the tool by running: - -```terminal -$ pub global activate flutter_plugin_tools -``` - -Then, from the root of your local `flutter/plugins` repo, use the tool to -publish a release. - -```terminal -$ pub global run flutter_plugin_tools publish-plugin --package $package -``` - -By default the tool tries to push tags to the `upstream` remote, but that and -some additional settings can be configured. Run `pub global activate -flutter_plugin_tools --help` for more usage information. - -The tool wraps `pub publish` for pushing the package to pub, and then will -automatically use git to try and create and push tags. It has some additional -safety checking around `pub publish` too. By default `pub publish` publishes -_everything_, including untracked or uncommitted files in version control. -`flutter_plugin_tools publish-plugin` will first check the status of the local -directory and refuse to publish if there are any mismatched files with version -control present. - -There is a lot about this process that is still to be desired. Some top level -items are being tracked in -[flutter/flutter#27258](https://github.com/flutter/flutter/issues/27258). +If you are a team member landing a PR, or just want to know what the release +process is for plugin changes, see [the release +documentation](https://github.com/flutter/flutter/wiki/Releasing-a-Plugin-or-Package). diff --git a/LICENSE b/LICENSE index a6d6c0749818..c6823b81eb84 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017 The Chromium Authors. All rights reserved. +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: diff --git a/README.md b/README.md index 42c64e1c6a50..1dd6d80f2a11 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,9 @@ These plugins are also available on Please file any issues, bugs, or feature requests in the [main flutter repo](https://github.com/flutter/flutter/issues/new). +Issues pertaining to this repository are [labeled +"plugin"](https://github.com/flutter/flutter/issues?q=is%3Aopen+is%3Aissue+label%3Aplugin). + ## Contributing If you wish to contribute a new plugin to the Flutter ecosystem, please @@ -38,29 +41,36 @@ These are the available plugins in this repository. | Plugin | Pub | Points | Popularity | Likes | |--------|-----|--------|------------|-------| -| [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) | [![pub points](https://badges.bar/android_alarm_manager/pub%20points)](https://pub.dev/packages/android_alarm_manager/score) | [![popularity](https://badges.bar/android_alarm_manager/popularity)](https://pub.dev/packages/android_alarm_manager/score) | [![likes](https://badges.bar/android_alarm_manager/likes)](https://pub.dev/packages/android_alarm_manager/score) | -| [android_intent](./packages/android_intent/) | [![pub package](https://img.shields.io/pub/v/android_intent.svg)](https://pub.dev/packages/android_intent) | [![pub points](https://badges.bar/android_intent/pub%20points)](https://pub.dev/packages/android_intent/score) | [![popularity](https://badges.bar/android_intent/popularity)](https://pub.dev/packages/android_intent/score) | [![likes](https://badges.bar/android_intent/likes)](https://pub.dev/packages/android_intent/score) | -| [battery](./packages/battery/) | [![pub package](https://img.shields.io/pub/v/battery.svg)](https://pub.dev/packages/battery) | [![pub points](https://badges.bar/battery/pub%20points)](https://pub.dev/packages/battery/score) | [![popularity](https://badges.bar/battery/popularity)](https://pub.dev/packages/battery/score) | [![likes](https://badges.bar/battery/likes)](https://pub.dev/packages/battery/score) | | [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) | -| [connectivity](./packages/connectivity/) | [![pub package](https://img.shields.io/pub/v/connectivity.svg)](https://pub.dev/packages/connectivity) | [![pub points](https://badges.bar/connectivity/pub%20points)](https://pub.dev/packages/connectivity/score) | [![popularity](https://badges.bar/connectivity/popularity)](https://pub.dev/packages/connectivity/score) | [![likes](https://badges.bar/connectivity/likes)](https://pub.dev/packages/connectivity/score) | -| [device_info](./packages/device_info/) | [![pub package](https://img.shields.io/pub/v/device_info.svg)](https://pub.dev/packages/device_info) | [![pub points](https://badges.bar/device_info/pub%20points)](https://pub.dev/packages/device_info/score) | [![popularity](https://badges.bar/device_info/popularity)](https://pub.dev/packages/device_info/score) | [![likes](https://badges.bar/device_info/likes)](https://pub.dev/packages/device_info/score) | -| [e2e (Discontinued, use integration_test)](./packages/e2e/) | [![pub package](https://img.shields.io/pub/v/e2e.svg)](https://pub.dev/packages/e2e) | [![pub points](https://badges.bar/e2e/pub%20points)](https://pub.dev/packages/e2e/score) | [![popularity](https://badges.bar/e2e/popularity)](https://pub.dev/packages/e2e/score) | [![likes](https://badges.bar/e2e/likes)](https://pub.dev/packages/e2e/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) | -| [integration_test](./packages/integration_test/) | [![pub package](https://img.shields.io/pub/v/integration_test.svg)](https://pub.dev/packages/integration_test) | [![pub points](https://badges.bar/integration_test/pub%20points)](https://pub.dev/packages/integration_test/score) | [![popularity](https://badges.bar/integration_test/popularity)](https://pub.dev/packages/integration_test/score) | [![likes](https://badges.bar/integration_test/likes)](https://pub.dev/packages/integration_test/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) | -| [package_info](./packages/package_info/) | [![pub package](https://img.shields.io/pub/v/package_info.svg)](https://pub.dev/packages/package_info) | [![pub points](https://badges.bar/package_info/pub%20points)](https://pub.dev/packages/package_info/score) | [![popularity](https://badges.bar/package_info/popularity)](https://pub.dev/packages/package_info/score) | [![likes](https://badges.bar/package_info/likes)](https://pub.dev/packages/package_info/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) | -| [sensors](./packages/sensors/) | [![pub package](https://img.shields.io/pub/v/sensors.svg)](https://pub.dev/packages/sensors) | [![pub points](https://badges.bar/sensors/pub%20points)](https://pub.dev/packages/sensors/score) | [![popularity](https://badges.bar/sensors/popularity)](https://pub.dev/packages/sensors/score) | [![likes](https://badges.bar/sensors/likes)](https://pub.dev/packages/sensors/score) | -| [share](./packages/share/) | [![pub package](https://img.shields.io/pub/v/share.svg)](https://pub.dev/packages/share) | [![pub points](https://badges.bar/share/pub%20points)](https://pub.dev/packages/share/score) | [![popularity](https://badges.bar/share/popularity)](https://pub.dev/packages/share/score) | [![likes](https://badges.bar/share/likes)](https://pub.dev/packages/share/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) | diff --git a/analysis_options.yaml b/analysis_options.yaml index b1261f36fac9..901067736edc 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,9 +1,250 @@ -include: package:pedantic/analysis_options.1.8.0.yaml +# This is a copy (as of March 2021) of flutter/flutter's analysis_options file, +# with minimal changes for this repository. The goal is to move toward using a +# shared set of analysis options as much as possible, and eventually a shared +# file. +# +# 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 +# +# 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 +# +# This file contains the analysis options used by Flutter tools, such as IntelliJ, +# Android Studio, and the `flutter analyze` command. + analyzer: + strong-mode: + implicit-casts: false + implicit-dynamic: false + errors: + # treat missing required parameters as a warning (not a hint) + missing_required_param: warning + # treat missing returns as a warning (not a hint) + missing_return: warning + # allow having TODOs in the code + todo: ignore + # allow self-reference to deprecated members (we do this because otherwise we have + # to annotate every member in every test, assert, etc, when we deprecate something) + deprecated_member_use_from_same_package: ignore + # Ignore analyzer hints for updating pubspecs when using Future or + # Stream and not importing dart:async + # Please see https://github.com/flutter/flutter/pull/24528 for details. + sdk_version_async_exported_from_core: ignore + ### 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 + linter: rules: - - public_member_api_docs + # these rules are documented on and in the same order as + # the Dart Lint rules page to make maintenance easier + # https://github.com/dart-lang/linter/blob/master/example/all.yaml + - always_declare_return_types + - always_put_control_body_on_new_line + # - always_put_required_named_parameters_first # we prefer having parameters in the same order as fields https://github.com/flutter/flutter/issues/10219 + - always_require_non_null_named_parameters + - always_specify_types + # - 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_classes_with_only_static_members + # - avoid_double_and_int_checks # only useful when targeting JS runtime + - avoid_empty_else + - avoid_equals_and_hash_code_on_mutable_classes + # - avoid_escaping_inner_quotes # not yet tested + - avoid_field_initializers_in_const_classes + - avoid_function_literals_in_foreach_calls + # - avoid_implementing_value_types # not yet tested + - avoid_init_to_null + # - avoid_js_rounded_ints # only useful when targeting JS runtime + - avoid_null_checks_in_equality_operators + # - avoid_positional_boolean_parameters # not yet tested + # - avoid_print # not yet tested + # - 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_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_for_void + # - avoid_returning_this # there are plenty of valid reasons to return this + # - avoid_setters_without_getters # not yet tested + - avoid_shadowing_type_parameters + - avoid_single_cascade_in_expression_statements + - avoid_slow_async_io + # - avoid_type_to_string # we do this commonly + - avoid_types_as_parameter_names + # - avoid_types_on_closure_parameters # conflicts with always_specify_types + # - avoid_unnecessary_containers # not yet tested + - avoid_unused_constructor_parameters + - avoid_void_async + # - avoid_web_libraries_in_flutter # not yet tested + - await_only_futures + - camel_case_extensions + - camel_case_types + - cancel_subscriptions + # - cascade_invocations # not yet tested + - cast_nullable_to_non_nullable + # - close_sinks # not reliable enough + # - comment_references # blocked on https://github.com/flutter/flutter/issues/20765 + # - 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 + - directives_ordering + # - do_not_use_environment # we do this commonly + - empty_catches + - empty_constructor_bodies + - empty_statements + - exhaustive_cases + # - file_names # not yet tested + - flutter_style_todos + - hash_and_equals + - implementation_imports + # - invariant_booleans # too many false positives: https://github.com/dart-lang/linter/issues/811 + - iterable_contains_unrelated_type + # - join_return_with_assignment # not required by flutter style + - leading_newlines_in_multiline_strings + - library_names + - library_prefixes + # - 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 + - no_adjacent_strings_in_list + # - no_default_cases # too many false positives + - no_duplicate_case_values + - no_logic_in_create_state + # - no_runtimeType_toString # ok in tests; we enable this only in packages/ + - non_constant_identifier_names + - null_check_on_nullable_type_parameter + # - null_closures # not required by flutter style + # - 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 + - overridden_fields + - package_api_docs + # - package_names # non conforming packages in sdk + - package_prefixed_library_names + # - parameter_assignments # we do this commonly + - prefer_adjacent_string_concatenation + - prefer_asserts_in_initializer_lists + # - prefer_asserts_with_message # not required by flutter style + - prefer_collection_literals + - prefer_conditional_assignment + - prefer_const_constructors + - prefer_const_constructors_in_immutables + - prefer_const_declarations + - prefer_const_literals_to_create_immutables + # - prefer_constructors_over_static_methods # far too many false positives + - prefer_contains + # - prefer_double_quotes # opposite of prefer_single_quotes + - prefer_equal_for_default_values + # - prefer_expression_function_bodies # conflicts with https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#consider-using--for-short-functions-and-methods + - prefer_final_fields + - prefer_final_in_for_each + - prefer_final_locals + - prefer_for_elements_to_map_fromIterable + - prefer_foreach + # - prefer_function_declarations_over_variables # not yet tested + - 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_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_single_quotes + - prefer_spread_collections + - prefer_typing_uninitialized_variables + - prefer_void_to_null + # - provide_deprecation_message # not yet tested + # - 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 + - slash_for_doc_comments + # - sort_child_properties_last # not yet tested + - sort_constructors_first + - 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 + - unnecessary_brace_in_string_interps + - unnecessary_const + # - unnecessary_final # conflicts with prefer_final_locals + - unnecessary_getters_setters + # - unnecessary_lambdas # has false positives: https://github.com/dart-lang/linter/issues/498 + - unnecessary_new + - unnecessary_null_aware_assignments + # - unnecessary_null_checks # not yet tested + - unnecessary_null_in_if_null_operators + - unnecessary_nullable_for_final_variable_declarations + - unnecessary_overrides + - unnecessary_parenthesis + # - unnecessary_raw_strings # not yet tested + - unnecessary_statements + - unnecessary_string_escapes + - unnecessary_string_interpolations + - unnecessary_this + - unrelated_type_equality_checks + # - unsafe_html # not yet tested + - use_full_hex_values_for_flutter_colors + # - use_function_type_syntax_for_parameters # not yet tested + - use_is_even_rather_than_modulo + # - use_key_in_widget_constructors # not yet tested + - use_late_for_private_fields_and_variables + - use_raw_strings + - use_rethrow_when_possible + # - use_setters_to_change_properties # not yet tested + # - use_string_buffers # has false positives: https://github.com/dart-lang/sdk/issues/34182 + # - 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 ### + # These are from flutter/flutter/packages, so will need to be preserved + # separately when moving to a shared file. + - no_runtimeType_toString # use objectRuntimeType from package:foundation + - public_member_api_docs # see https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#documentation-dartdocs-javadocs-etc + # Flutter has a specific use case for dependencies that are intentionally + # not sorted, which doesn't apply to this repo. + - sort_pub_dependencies diff --git a/analysis_options_legacy.yaml b/analysis_options_legacy.yaml new file mode 100644 index 000000000000..2b62a6a9e2b9 --- /dev/null +++ b/analysis_options_legacy.yaml @@ -0,0 +1,13 @@ +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 + 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/AUTHORS b/packages/android_alarm_manager/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/android_alarm_manager/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/android_alarm_manager/CHANGELOG.md b/packages/android_alarm_manager/CHANGELOG.md index aff0e4a30982..7c40428c22ba 100644 --- a/packages/android_alarm_manager/CHANGELOG.md +++ b/packages/android_alarm_manager/CHANGELOG.md @@ -1,3 +1,39 @@ +## 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. diff --git a/packages/android_alarm_manager/LICENSE b/packages/android_alarm_manager/LICENSE index a6d6c0749818..c6823b81eb84 100644 --- a/packages/android_alarm_manager/LICENSE +++ b/packages/android_alarm_manager/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017 The Chromium Authors. All rights reserved. +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: diff --git a/packages/android_alarm_manager/README.md b/packages/android_alarm_manager/README.md index e7c7f6ee2713..500c5d5232f9 100644 --- a/packages/android_alarm_manager/README.md +++ b/packages/android_alarm_manager/README.md @@ -1,16 +1,24 @@ # android_alarm_manager -[![pub package](https://img.shields.io/pub/v/android_alarm_manager.svg)](https://pub.dartlang.org/packages/android_alarm_manager) +--- -A Flutter plugin for accessing the Android AlarmManager service, and running -Dart code in the background when alarms fire. +## 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. -**Please set your constraint to `android_alarm_manager: '>=0.4.y+x <2.0.0'`** +--- -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.4.y+z`. -Please use `android_alarm_manager: '>=0.4.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 +[![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 @@ -36,7 +44,7 @@ Next, within the `` tags, add: android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver" android:enabled="false"> - + @@ -109,18 +117,13 @@ Which must be reflected in the application's `AndroidManifest.xml`. E.g.: **Note:** Not calling `AlarmService.setPluginRegistrant` will result in an exception being thrown when an alarm eventually fires. -### Flutter Android Embedding V2 (Flutter Version >= 1.12) +### Flutter Android Embedding V2 For the Flutter Android Embedding V2, plugins are registered with the background isolate via reflection so `AlarmService.setPluginRegistrant` does not need to be called. -**NOTE: this plugin is not completely compatible with the V2 embedding on -Flutter versions < 1.12 as the background isolate will not automatically -register plugins. This can be resolved by running `flutter upgrade` to upgrade -to the latest Flutter version.** - For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). -For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code). +For help on editing plugin code, view the [documentation](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin). diff --git a/packages/android_alarm_manager/analysis_options.yaml b/packages/android_alarm_manager/analysis_options.yaml new file mode 100644 index 000000000000..cda4f6e153e6 --- /dev/null +++ b/packages/android_alarm_manager/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options_legacy.yaml diff --git a/packages/android_alarm_manager/android/build.gradle b/packages/android_alarm_manager/android/build.gradle index 8a59fed74438..52a07082dded 100644 --- a/packages/android_alarm_manager/android/build.gradle +++ b/packages/android_alarm_manager/android/build.gradle @@ -5,7 +5,7 @@ def args = ["-Xlint:deprecation","-Xlint:unchecked","-Werror"] buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -16,7 +16,7 @@ buildscript { rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } @@ -27,7 +27,7 @@ project.getTasks().withType(JavaCompile){ apply plugin: 'com.android.library' android { - compileSdkVersion 28 + compileSdkVersion 29 compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 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 index a8968a2095d9..c471643628bc 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/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 index 3287789f6c3d..d333a4950026 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/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 index 557913a626d5..fd3a9c5e87dd 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/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 index 86010ff3f089..d9c40bfe7181 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/PluginRegistrantException.java b/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/PluginRegistrantException.java index debcd7ee7529..afbc1c71bd3f 100644 --- a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/PluginRegistrantException.java +++ b/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/PluginRegistrantException.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/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 index b920afa1c1b7..9135755863a1 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/android_alarm_manager/example/README.md b/packages/android_alarm_manager/example/README.md index 476cf1359345..0df1ed9fb4ec 100644 --- a/packages/android_alarm_manager/example/README.md +++ b/packages/android_alarm_manager/example/README.md @@ -5,4 +5,4 @@ Demonstrates how to use the android_alarm_manager plugin. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). diff --git a/packages/android_alarm_manager/example/android/app/build.gradle b/packages/android_alarm_manager/example/android/app/build.gradle index f066040810c2..9722ec280205 100644 --- a/packages/android_alarm_manager/example/android/app/build.gradle +++ b/packages/android_alarm_manager/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 28 + compileSdkVersion 29 lintOptions { disable 'InvalidPackage' 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 index ce34b25ec505..d6927232fb80 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/DriverExtensionActivity.java b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/DriverExtensionActivity.java index 4f521a387bac..9a57f2c1d914 100644 --- a/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/DriverExtensionActivity.java +++ b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/androidalarmmanager/DriverExtensionActivity.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/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 index 373e770697d5..0272c14a8328 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/android_alarm_manager/example/android/app/src/main/AndroidManifest.xml b/packages/android_alarm_manager/example/android/app/src/main/AndroidManifest.xml index 356c10f45651..2a9dc331ebf1 100644 --- a/packages/android_alarm_manager/example/android/app/src/main/AndroidManifest.xml +++ b/packages/android_alarm_manager/example/android/app/src/main/AndroidManifest.xml @@ -41,7 +41,7 @@ android:name="io.flutter.plugins.androidalarmmanager.RebootBroadcastReceiver" android:enabled="false"> - + /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - E95CF7E4BD7CAFC3E0F4E1E2 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.androidAlarmManagerExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.androidAlarmManagerExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/android_alarm_manager/example/ios/Runner/AppDelegate.h b/packages/android_alarm_manager/example/ios/Runner/AppDelegate.h deleted file mode 100644 index 36e21bbf9cf4..000000000000 --- a/packages/android_alarm_manager/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,6 +0,0 @@ -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/android_alarm_manager/example/ios/Runner/AppDelegate.m b/packages/android_alarm_manager/example/ios/Runner/AppDelegate.m deleted file mode 100644 index 59a72e90be12..000000000000 --- a/packages/android_alarm_manager/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,13 +0,0 @@ -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/packages/android_alarm_manager/example/ios/Runner/Info.plist b/packages/android_alarm_manager/example/ios/Runner/Info.plist deleted file mode 100644 index 1d076337d6f4..000000000000 --- a/packages/android_alarm_manager/example/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - android_alarm_manager_example - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/android_alarm_manager/example/ios/Runner/main.m b/packages/android_alarm_manager/example/ios/Runner/main.m deleted file mode 100644 index dff6597e4513..000000000000 --- a/packages/android_alarm_manager/example/ios/Runner/main.m +++ /dev/null @@ -1,9 +0,0 @@ -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/android_alarm_manager/example/lib/main.dart b/packages/android_alarm_manager/example/lib/main.dart index 4ba697744dbf..75648b8ded5f 100644 --- a/packages/android_alarm_manager/example/lib/main.dart +++ b/packages/android_alarm_manager/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -22,7 +22,7 @@ const String isolateName = 'isolate'; final ReceivePort port = ReceivePort(); /// Global [SharedPreferences] object. -SharedPreferences prefs; +late SharedPreferences prefs; Future main() async { // TODO(bkonyi): uncomment @@ -54,7 +54,7 @@ class AlarmManagerExampleApp extends StatelessWidget { } class _AlarmHomePage extends StatefulWidget { - _AlarmHomePage({Key key, this.title}) : super(key: key); + _AlarmHomePage({Key? key, required this.title}) : super(key: key); final String title; @override @@ -86,7 +86,7 @@ class _AlarmHomePageState extends State<_AlarmHomePage> { } // The background - static SendPort uiSendPort; + static SendPort? uiSendPort; // The callback for our alarm static Future callback() async { @@ -94,7 +94,7 @@ class _AlarmHomePageState extends State<_AlarmHomePage> { // Get the previous cached count and increment it. final prefs = await SharedPreferences.getInstance(); - int currentCount = prefs.getInt(countKey); + int currentCount = prefs.getInt(countKey) ?? 0; await prefs.setInt(countKey, currentCount + 1); // This will be null if we're running in the background. @@ -104,11 +104,7 @@ class _AlarmHomePageState extends State<_AlarmHomePage> { @override Widget build(BuildContext context) { - // TODO(jackson): This has been deprecated and should be replaced - // with `headline4` when it's available on all the versions of - // Flutter that we test. - // ignore: deprecated_member_use - final textStyle = Theme.of(context).textTheme.display1; + final textStyle = Theme.of(context).textTheme.headline4; return Scaffold( appBar: AppBar( title: Text(widget.title), @@ -135,7 +131,7 @@ class _AlarmHomePageState extends State<_AlarmHomePage> { ), ], ), - RaisedButton( + ElevatedButton( child: Text( 'Schedule OneShot Alarm', ), @@ -144,7 +140,7 @@ class _AlarmHomePageState extends State<_AlarmHomePage> { await AndroidAlarmManager.oneShot( const Duration(seconds: 5), // Ensure we have a unique alarm ID. - Random().nextInt(pow(2, 31)), + Random().nextInt(pow(2, 31).toInt()), callback, exact: true, wakeup: true, diff --git a/packages/android_alarm_manager/example/pubspec.yaml b/packages/android_alarm_manager/example/pubspec.yaml index 93cbc57d8de5..821440c49659 100644 --- a/packages/android_alarm_manager/example/pubspec.yaml +++ b/packages/android_alarm_manager/example/pubspec.yaml @@ -1,15 +1,25 @@ 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: ^0.5.6 + shared_preferences: ^2.0.0 integration_test: - path: ../../integration_test - path_provider: ^1.3.1 + sdk: flutter + path_provider: ^2.0.0 dev_dependencies: espresso: ^0.0.1+3 @@ -17,7 +27,7 @@ dev_dependencies: sdk: flutter flutter_test: sdk: flutter - pedantic: ^1.8.0 + 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 index 4e32b483f8d7..6a0e6fa82dbe 100644 --- a/packages/android_alarm_manager/example/test_driver/integration_test.dart +++ b/packages/android_alarm_manager/example/test_driver/integration_test.dart @@ -1,45 +1,9 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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'; +// @dart=2.9 -import 'package:flutter_driver/flutter_driver.dart'; -import 'package:vm_service_client/vm_service_client.dart'; +import 'package:integration_test/integration_test_driver.dart'; -Future> resumeIsolatesOnPause( - FlutterDriver driver) async { - final VM vm = await driver.serviceClient.getVM(); - for (VMIsolateRef isolateRef in vm.isolates) { - final VMIsolate isolate = await isolateRef.load(); - if (isolate.isPaused) { - await isolate.resume(); - } - } - return driver.serviceClient.onIsolateRunnable - .asBroadcastStream() - .listen((VMIsolateRef isolateRef) async { - final VMIsolate isolate = await isolateRef.load(); - if (isolate.isPaused) { - await isolate.resume(); - } - }); -} - -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - // flutter drive causes isolates to be paused on spawn. The background isolate - // for this plugin will need to be resumed for the test to pass. - final StreamSubscription subscription = - await resumeIsolatesOnPause(driver); - final String data = await driver.requestData( - null, - timeout: const Duration(minutes: 1), - ); - await driver.close(); - await subscription.cancel(); - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} +Future main() => integrationDriver(); diff --git a/packages/android_alarm_manager/ios/Classes/AndroidAlarmManagerPlugin.h b/packages/android_alarm_manager/ios/Classes/AndroidAlarmManagerPlugin.h deleted file mode 100644 index 595fcf60fee1..000000000000 --- a/packages/android_alarm_manager/ios/Classes/AndroidAlarmManagerPlugin.h +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import - -@interface FLTAndroidAlarmManagerPlugin : NSObject -@end diff --git a/packages/android_alarm_manager/ios/Classes/AndroidAlarmManagerPlugin.m b/packages/android_alarm_manager/ios/Classes/AndroidAlarmManagerPlugin.m deleted file mode 100644 index 0aa4f2b2122d..000000000000 --- a/packages/android_alarm_manager/ios/Classes/AndroidAlarmManagerPlugin.m +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "AndroidAlarmManagerPlugin.h" - -@implementation FLTAndroidAlarmManagerPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - FlutterMethodChannel* channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/android_alarm_manager" - binaryMessenger:[registrar messenger] - codec:[FlutterJSONMethodCodec sharedInstance]]; - FLTAndroidAlarmManagerPlugin* instance = [[FLTAndroidAlarmManagerPlugin alloc] init]; - [registrar addMethodCallDelegate:instance channel:channel]; -} - -- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - result(FlutterMethodNotImplemented); -} - -@end diff --git a/packages/android_alarm_manager/ios/android_alarm_manager.podspec b/packages/android_alarm_manager/ios/android_alarm_manager.podspec deleted file mode 100644 index 2b253878c1ea..000000000000 --- a/packages/android_alarm_manager/ios/android_alarm_manager.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'android_alarm_manager' - s.version = '0.0.1' - s.summary = 'Flutter Android Alarm Manager' - s.description = <<-DESC -A Flutter plugin for accessing the Android AlarmManager service, and running Dart code in the background when alarms fire. -This plugin a no-op on iOS. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/android_alarm_manager' } - s.documentation_url = 'https://pub.dev/packages/android_alarm_manager' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } -end diff --git a/packages/android_alarm_manager/lib/android_alarm_manager.dart b/packages/android_alarm_manager/lib/android_alarm_manager.dart index b8afa134472c..e4e3855933ca 100644 --- a/packages/android_alarm_manager/lib/android_alarm_manager.dart +++ b/packages/android_alarm_manager/lib/android_alarm_manager.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -31,7 +31,7 @@ void _alarmManagerCallbackDispatcher() { // PluginUtilities.getCallbackFromHandle performs a lookup based on the // callback handle and returns a tear-off of the original callback. - final Function closure = PluginUtilities.getCallbackFromHandle(handle); + final Function? closure = PluginUtilities.getCallbackFromHandle(handle); if (closure == null) { print('Fatal: could not find callback'); @@ -56,7 +56,7 @@ void _alarmManagerCallbackDispatcher() { // A lambda that returns the current instant in the form of a [DateTime]. typedef DateTime _Now(); // A lambda that gets the handle for the given [callback]. -typedef CallbackHandle _GetCallbackHandle(Function callback); +typedef CallbackHandle? _GetCallbackHandle(Function callback); /// A Flutter plugin for registering Dart callbacks with the Android /// AlarmManager service. @@ -77,7 +77,7 @@ class AndroidAlarmManager { /// the plugin. @visibleForTesting static void setTestOverides( - {_Now now, _GetCallbackHandle getCallbackHandle}) { + {_Now? now, _GetCallbackHandle? getCallbackHandle}) { _now = (now ?? _now); _getCallbackHandle = (getCallbackHandle ?? _getCallbackHandle); } @@ -88,12 +88,12 @@ class AndroidAlarmManager { /// Returns a [Future] that resolves to `true` on success and `false` on /// failure. static Future initialize() async { - final CallbackHandle handle = + final CallbackHandle? handle = _getCallbackHandle(_alarmManagerCallbackDispatcher); if (handle == null) { return false; } - final bool r = await _channel.invokeMethod( + final bool? r = await _channel.invokeMethod( 'AlarmService.start', [handle.toRawHandle()]); return r ?? false; } @@ -207,11 +207,11 @@ class AndroidAlarmManager { assert(callback is Function() || callback is Function(int)); assert(id.bitLength < 32); final int startMillis = time.millisecondsSinceEpoch; - final CallbackHandle handle = _getCallbackHandle(callback); + final CallbackHandle? handle = _getCallbackHandle(callback); if (handle == null) { return false; } - final bool r = + final bool? r = await _channel.invokeMethod('Alarm.oneShotAt', [ id, alarmClock, @@ -222,7 +222,7 @@ class AndroidAlarmManager { rescheduleOnReboot, handle.toRawHandle(), ]); - return (r == null) ? false : r; + return r ?? false; } /// Schedules a repeating timer to run `callback` with period `duration`. @@ -262,7 +262,7 @@ class AndroidAlarmManager { Duration duration, int id, Function callback, { - DateTime startAt, + DateTime? startAt, bool exact = false, bool wakeup = false, bool rescheduleOnReboot = false, @@ -274,11 +274,11 @@ class AndroidAlarmManager { final int period = duration.inMilliseconds; final int first = startAt != null ? startAt.millisecondsSinceEpoch : now + period; - final CallbackHandle handle = _getCallbackHandle(callback); + final CallbackHandle? handle = _getCallbackHandle(callback); if (handle == null) { return false; } - final bool r = await _channel.invokeMethod( + final bool? r = await _channel.invokeMethod( 'Alarm.periodic', [ id, exact, @@ -288,7 +288,7 @@ class AndroidAlarmManager { rescheduleOnReboot, handle.toRawHandle() ]); - return (r == null) ? false : r; + return r ?? false; } /// Cancels a timer. @@ -299,8 +299,8 @@ class AndroidAlarmManager { /// Returns a [Future] that resolves to `true` on success and `false` on /// failure. static Future cancel(int id) async { - final bool r = + final bool? r = await _channel.invokeMethod('Alarm.cancel', [id]); - return (r == null) ? false : r; + return r ?? false; } } diff --git a/packages/android_alarm_manager/pubspec.yaml b/packages/android_alarm_manager/pubspec.yaml index d4292a0dc940..450fb914b739 100644 --- a/packages/android_alarm_manager/pubspec.yaml +++ b/packages/android_alarm_manager/pubspec.yaml @@ -1,20 +1,13 @@ name: android_alarm_manager description: Flutter plugin for accessing the Android AlarmManager service, and running Dart code in the background when alarms fire. -# 0.4.y+z is compatible with 1.0.0, if you land a breaking change bump -# the version to 2.0.0. -# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.4.5+14 -homepage: https://github.com/flutter/plugins/tree/master/packages/android_alarm_manager +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 -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - pedantic: ^1.8.0 +environment: + sdk: '>=2.12.0 <3.0.0' + flutter: ">=1.20.0" flutter: plugin: @@ -23,6 +16,11 @@ flutter: package: io.flutter.plugins.androidalarmmanager pluginClass: AndroidAlarmManagerPlugin -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" +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 index 1f9d2856838e..908bb957c0f2 100644 --- a/packages/android_alarm_manager/test/android_alarm_manager_test.dart +++ b/packages/android_alarm_manager/test/android_alarm_manager_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/android_intent/AUTHORS b/packages/android_intent/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/android_intent/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/android_intent/CHANGELOG.md b/packages/android_intent/CHANGELOG.md index 711057ce3ec6..2934d72012f8 100644 --- a/packages/android_intent/CHANGELOG.md +++ b/packages/android_intent/CHANGELOG.md @@ -1,3 +1,29 @@ +## 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. diff --git a/packages/android_intent/LICENSE b/packages/android_intent/LICENSE index a6d6c0749818..c6823b81eb84 100644 --- a/packages/android_intent/LICENSE +++ b/packages/android_intent/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017 The Chromium Authors. All rights reserved. +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: diff --git a/packages/android_intent/README.md b/packages/android_intent/README.md index f7dfdfed9860..0ad1117daa0c 100644 --- a/packages/android_intent/README.md +++ b/packages/android_intent/README.md @@ -1,22 +1,30 @@ # 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. -**Please set your constraint to `android_intent: '>=0.3.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.3.y+z`. -Please use `android_intent: '>=0.3.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - Use it by specifying action, category, data and extra arguments for the intent. It does not support returning the result of the launched activity. Sample usage: ```dart -if (platform.isAndroid) { +if (Platform.isAndroid) { AndroidIntent intent = AndroidIntent( action: 'action_view', data: 'https://play.google.com/store/apps/details?' @@ -40,7 +48,7 @@ for it in the plugin and use an action constant to refer to it. For instance: `'action_application_details_settings'` translates to `android.settings.ACTION_APPLICATION_DETAILS_SETTINGS` ```dart -if (platform.isAndroid) { +if (Platform.isAndroid) { final AndroidIntent intent = AndroidIntent( action: 'action_application_details_settings', data: 'package:com.example.app', // replace com.example.app with your applicationId @@ -53,13 +61,13 @@ if (platform.isAndroid) { Feel free to add support for additional Android intents. The Dart values supported for the arguments parameter, and their corresponding -Android values, are listed [here](https://flutter.io/platform-channels/#codec). +Android values, are listed [here](https://flutter.dev/docs/development/platform-integration/platform-channels#codec). On the Android side, the arguments are used to populate an Android `Bundle` instance. This process currently restricts the use of lists to homogeneous lists of integers or strings. > Note that a similar method does not currently exist for iOS. Instead, the -[url_launcher](https://pub.dartlang.org/packages/url_launcher) plugin +[url_launcher](https://pub.dev/packages/url_launcher) plugin can be used for deep linking. Url launcher can also be used for creating ACTION_VIEW intents for Android, however this intent plugin also allows clients to set extra parameters for the intent. @@ -67,6 +75,6 @@ clients to set extra parameters for the intent. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). -For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code). +For help on editing plugin code, view the [documentation](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin). diff --git a/packages/android_intent/analysis_options.yaml b/packages/android_intent/analysis_options.yaml new file mode 100644 index 000000000000..cda4f6e153e6 --- /dev/null +++ b/packages/android_intent/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options_legacy.yaml diff --git a/packages/android_intent/android/build.gradle b/packages/android_intent/android/build.gradle index 5d0f1cae7159..adf53f94393c 100644 --- a/packages/android_intent/android/build.gradle +++ b/packages/android_intent/android/build.gradle @@ -5,7 +5,7 @@ def args = ["-Xlint:deprecation","-Xlint:unchecked","-Werror"] buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -16,7 +16,7 @@ buildscript { rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } @@ -27,7 +27,7 @@ project.getTasks().withType(JavaCompile){ apply plugin: 'com.android.library' android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { minSdkVersion 16 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 index 30e0915aed1f..883d05922874 100644 --- 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 @@ -1,3 +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. + package io.flutter.plugins.androidintent; import androidx.annotation.NonNull; 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 index b1a590d79c84..2c05a914c888 100644 --- 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 @@ -1,3 +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. + package io.flutter.plugins.androidintent; import android.app.Activity; 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 index 753541bf9338..bcd843b64228 100644 --- 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 @@ -1,3 +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. + package io.flutter.plugins.androidintent; import android.content.ComponentName; 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 index cf0a28e822d4..0ea03a0690f1 100644 --- 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 @@ -1,3 +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. + package io.flutter.plugins.androidintent; import static org.junit.Assert.assertEquals; diff --git a/packages/android_intent/example/README.md b/packages/android_intent/example/README.md index a2bc5241adbb..460d46efe631 100644 --- a/packages/android_intent/example/README.md +++ b/packages/android_intent/example/README.md @@ -5,4 +5,4 @@ Demonstrates how to use the android_intent plugin. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). diff --git a/packages/android_intent/example/android/app/build.gradle b/packages/android_intent/example/android/app/build.gradle index 48178f2be030..a309dc2e2d5c 100644 --- a/packages/android_intent/example/android/app/build.gradle +++ b/packages/android_intent/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 28 + compileSdkVersion 29 lintOptions { disable 'InvalidPackage' diff --git a/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/androidintentexample/EmbeddingV1ActivityTest.java b/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/androidintentexample/EmbeddingV1ActivityTest.java index 7ab0e87e7c5a..36f8444dea62 100644 --- a/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/androidintentexample/EmbeddingV1ActivityTest.java +++ b/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/androidintentexample/EmbeddingV1ActivityTest.java @@ -1,3 +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. + package io.flutter.plugins.androidintentexample; import androidx.test.rule.ActivityTestRule; 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 index 619dbcd853e7..d9ba10729001 100644 --- 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 @@ -1,3 +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. + package io.flutter.plugins.androidintentexample; import androidx.test.rule.ActivityTestRule; diff --git a/packages/android_intent/example/android/app/src/main/java/io/flutter/plugins/androidintentexample/EmbeddingV1Activity.java b/packages/android_intent/example/android/app/src/main/java/io/flutter/plugins/androidintentexample/EmbeddingV1Activity.java index f64ae47a7656..5906a8940236 100644 --- a/packages/android_intent/example/android/app/src/main/java/io/flutter/plugins/androidintentexample/EmbeddingV1Activity.java +++ b/packages/android_intent/example/android/app/src/main/java/io/flutter/plugins/androidintentexample/EmbeddingV1Activity.java @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/android_intent/example/android/build.gradle b/packages/android_intent/example/android/build.gradle index 541636cc492a..e101ac08df55 100644 --- a/packages/android_intent/example/android/build.gradle +++ b/packages/android_intent/example/android/build.gradle @@ -1,7 +1,7 @@ buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -12,7 +12,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/packages/android_intent/example/integration_test/android_intent_test.dart b/packages/android_intent/example/integration_test/android_intent_test.dart index 78a667b27a09..5ae86cba6a03 100644 --- a/packages/android_intent/example/integration_test/android_intent_test.dart +++ b/packages/android_intent/example/integration_test/android_intent_test.dart @@ -1,3 +1,9 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.9 + import 'dart:io'; import 'package:android_intent/android_intent.dart'; diff --git a/packages/android_intent/example/ios/Runner.xcodeproj/project.pbxproj b/packages/android_intent/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 430cec7ef2b5..000000000000 --- a/packages/android_intent/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,490 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 3FC5CBD67A867C34C8CFD7E1 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7ABB9ACA70E30025F77BB759 /* libPods-Runner.a */; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; - 7ABB9ACA70E30025F77BB759 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 9B21C620C27B8C2AF08BFA21 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - EFC3461395B2546568135556 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - 3FC5CBD67A867C34C8CFD7E1 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 2C36A917BF8B34817D5A406D /* Pods */ = { - isa = PBXGroup; - children = ( - EFC3461395B2546568135556 /* Pods-Runner.debug.xcconfig */, - 9B21C620C27B8C2AF08BFA21 /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - 7423FCEB8AD9C632FAF625A3 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 7ABB9ACA70E30025F77BB759 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B80C3931E831B6300D905FE /* App.framework */, - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 2C36A917BF8B34817D5A406D /* Pods */, - 7423FCEB8AD9C632FAF625A3 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - ECD6A6833016AB689F7B8471 /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 4B2738B48C3E53795176CD79 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; - }; - 4B2738B48C3E53795176CD79 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; - ECD6A6833016AB689F7B8471 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.androidIntentExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.androidIntentExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/android_intent/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/android_intent/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 3bb3697ef41c..000000000000 --- a/packages/android_intent/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/android_intent/example/ios/Runner/AppDelegate.h b/packages/android_intent/example/ios/Runner/AppDelegate.h deleted file mode 100644 index d9e18e990f2e..000000000000 --- a/packages/android_intent/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/android_intent/example/ios/Runner/AppDelegate.m b/packages/android_intent/example/ios/Runner/AppDelegate.m deleted file mode 100644 index f08675707182..000000000000 --- a/packages/android_intent/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d22f10b2ab63..000000000000 --- a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/android_intent/example/ios/Runner/Info.plist b/packages/android_intent/example/ios/Runner/Info.plist deleted file mode 100644 index 61ad692e0180..000000000000 --- a/packages/android_intent/example/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - android_intent_example - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/android_intent/example/ios/Runner/main.m b/packages/android_intent/example/ios/Runner/main.m deleted file mode 100644 index bec320c0bee0..000000000000 --- a/packages/android_intent/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/android_intent/example/lib/main.dart b/packages/android_intent/example/lib/main.dart index 45de9632e975..c2276d080aa4 100644 --- a/packages/android_intent/example/lib/main.dart +++ b/packages/android_intent/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -59,12 +59,12 @@ class MyHomePage extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - RaisedButton( + ElevatedButton( child: const Text( 'Tap here to set an alarm\non weekdays at 9:30pm.'), onPressed: _createAlarm, ), - RaisedButton( + ElevatedButton( child: const Text('Tap here to test explicit intents.'), onPressed: () => _openExplicitIntentsView(context)), ], @@ -166,40 +166,40 @@ class ExplicitIntentsWidget extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - RaisedButton( + ElevatedButton( child: const Text( 'Tap here to display panorama\nimagery in Google Street View.'), onPressed: _openGoogleMapsStreetView, ), - RaisedButton( + ElevatedButton( child: const Text('Tap here to display\na map in Google Maps.'), onPressed: _displayMapInGoogleMaps, ), - RaisedButton( + ElevatedButton( child: const Text( 'Tap here to launch turn-by-turn\nnavigation in Google Maps.'), onPressed: _launchTurnByTurnNavigationInGoogleMaps, ), - RaisedButton( + ElevatedButton( child: const Text('Tap here to open link in Google Chrome.'), onPressed: _openLinkInGoogleChrome, ), - RaisedButton( + ElevatedButton( child: const Text('Tap here to start activity in new task.'), onPressed: _startActivityInNewTask, ), - RaisedButton( + ElevatedButton( child: const Text( 'Tap here to test explicit intent fallback to implicit.'), onPressed: _testExplicitIntentFallback, ), - RaisedButton( + ElevatedButton( child: const Text( 'Tap here to open Location Settings Configuration', ), onPressed: _openLocationSettingsConfiguration, ), - RaisedButton( + ElevatedButton( child: const Text( 'Tap here to open Application Details', ), diff --git a/packages/android_intent/example/pubspec.yaml b/packages/android_intent/example/pubspec.yaml index 31d434e8c038..42e59930ce64 100644 --- a/packages/android_intent/example/pubspec.yaml +++ b/packages/android_intent/example/pubspec.yaml @@ -1,18 +1,28 @@ 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: - path: ../../integration_test + sdk: flutter flutter_driver: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 # The following section is specific to Flutter. flutter: diff --git a/packages/android_intent/example/test_driver/integration_test.dart b/packages/android_intent/example/test_driver/integration_test.dart index 34483b996049..6a0e6fa82dbe 100644 --- a/packages/android_intent/example/test_driver/integration_test.dart +++ b/packages/android_intent/example/test_driver/integration_test.dart @@ -1,16 +1,9 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; +// Copyright 2013 The Flutter Authors. All 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/flutter_driver.dart'; +// @dart=2.9 -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String data = await driver.requestData( - null, - timeout: const Duration(minutes: 1), - ); - await driver.close(); - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/android_intent/ios/Classes/AndroidIntentPlugin.h b/packages/android_intent/ios/Classes/AndroidIntentPlugin.h deleted file mode 100644 index 8810c13f61cf..000000000000 --- a/packages/android_intent/ios/Classes/AndroidIntentPlugin.h +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import - -@interface FLTAndroidIntentPlugin : NSObject -@end diff --git a/packages/android_intent/ios/Classes/AndroidIntentPlugin.m b/packages/android_intent/ios/Classes/AndroidIntentPlugin.m deleted file mode 100644 index d708adf8c1d0..000000000000 --- a/packages/android_intent/ios/Classes/AndroidIntentPlugin.m +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "AndroidIntentPlugin.h" - -@implementation FLTAndroidIntentPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - FlutterMethodChannel* channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/android_intent" - binaryMessenger:[registrar messenger]]; - FLTAndroidIntentPlugin* instance = [[FLTAndroidIntentPlugin alloc] init]; - [registrar addMethodCallDelegate:instance channel:channel]; -} - -- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - result(FlutterMethodNotImplemented); -} - -@end diff --git a/packages/android_intent/ios/android_intent.podspec b/packages/android_intent/ios/android_intent.podspec deleted file mode 100644 index b3f9b6eb334f..000000000000 --- a/packages/android_intent/ios/android_intent.podspec +++ /dev/null @@ -1,24 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'android_intent' - s.version = '0.0.1' - s.summary = 'Android Intent Plugin for Flutter' - s.description = <<-DESC -This plugin allows Flutter apps to launch arbitrary intents when the platform is Android. -If the plugin is invoked on iOS, it will crash your app. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/android_intent' } - s.documentation_url = 'https://pub.dev/packages/android_intent' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } -end - diff --git a/packages/android_intent/lib/android_intent.dart b/packages/android_intent/lib/android_intent.dart index 9d701979b392..80208833c6be 100644 --- a/packages/android_intent/lib/android_intent.dart +++ b/packages/android_intent/lib/android_intent.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -36,7 +36,7 @@ class AndroidIntent { this.arguments, this.package, this.componentName, - Platform platform, + Platform? platform, this.type, }) : assert(action != null || componentName != null, 'action or component (or both) must be specified'), @@ -47,8 +47,8 @@ class AndroidIntent { /// app code, it may break without warning. @visibleForTesting AndroidIntent.private({ - @required Platform platform, - @required MethodChannel channel, + required Platform platform, + required MethodChannel channel, this.action, this.flags, this.category, @@ -66,47 +66,47 @@ class AndroidIntent { /// includes constants like `ACTION_VIEW`. /// /// See https://developer.android.com/reference/android/content/Intent.html#intent-structure. - final String action; + final String? action; /// Constants that can be set on an intent to tweak how it is finally handled. /// Some of the constants are mirrored to Dart via [Flag]. /// /// See https://developer.android.com/reference/android/content/Intent.html#setFlags(int). - final List flags; + final List? flags; /// An optional additional constant qualifying the given [action]. /// /// See https://developer.android.com/reference/android/content/Intent.html#intent-structure. - final String category; + final String? category; /// The Uri that the [action] is pointed towards. /// /// See https://developer.android.com/reference/android/content/Intent.html#intent-structure. - final String data; + final String? data; /// The equivalent of `extras`, a generic `Bundle` of data that the Intent can /// carry. This is a slot for extraneous data that the listener may use. /// /// See https://developer.android.com/reference/android/content/Intent.html#intent-structure. - final Map arguments; + final Map? arguments; /// Sets the [data] to only resolve within this given package. /// /// See https://developer.android.com/reference/android/content/Intent.html#setPackage(java.lang.String). - final String package; + final String? package; /// Set the exact `ComponentName` that should handle the intent. If this is /// set [package] should also be non-null. /// /// See https://developer.android.com/reference/android/content/Intent.html#setComponent(android.content.ComponentName). - final String componentName; + final String? componentName; final MethodChannel _channel; final Platform _platform; /// Set an explicit MIME data type. /// /// See https://developer.android.com/reference/android/content/Intent.html#intent-structure. - final String type; + final String? type; bool _isPowerOfTwo(int x) { /* First x in the below expression is for the case when x is 0 */ @@ -146,17 +146,18 @@ class AndroidIntent { return false; } - return await _channel.invokeMethod( + final result = await _channel.invokeMethod( 'canResolveActivity', _buildArguments(), ); + return result!; } /// Constructs the map of arguments which is passed to the plugin. Map _buildArguments() { return { if (action != null) 'action': action, - if (flags != null) 'flags': convertFlags(flags), + if (flags != null) 'flags': convertFlags(flags!), if (category != null) 'category': category, if (data != null) 'data': data, if (arguments != null) 'arguments': arguments, diff --git a/packages/android_intent/lib/flag.dart b/packages/android_intent/lib/flag.dart index e05aa6d12666..771a89ff83a7 100644 --- a/packages/android_intent/lib/flag.dart +++ b/packages/android_intent/lib/flag.dart @@ -1,3 +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. + /// Special flags that can be set on an intent to control how it is handled. /// /// See diff --git a/packages/android_intent/pubspec.yaml b/packages/android_intent/pubspec.yaml index 764719e3901a..793f82d4762d 100644 --- a/packages/android_intent/pubspec.yaml +++ b/packages/android_intent/pubspec.yaml @@ -1,10 +1,12 @@ name: android_intent description: Flutter plugin for launching Android Intents. Not supported on iOS. -homepage: https://github.com/flutter/plugins/tree/master/packages/android_intent -# 0.3.y+z is compatible with 1.0.0, if you land a breaking change bump -# the version to 2.0.0. -# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.3.7+5 +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: @@ -16,15 +18,12 @@ flutter: dependencies: flutter: sdk: flutter - platform: ">=2.0.0 <4.0.0" - meta: ^1.0.5 + platform: ^3.0.0 + meta: ^1.3.0 dev_dependencies: - test: ^1.3.0 - mockito: ^3.0.0 + test: ^1.16.3 + mockito: ^5.0.0 flutter_test: sdk: flutter - pedantic: ^1.8.0 - -environment: - sdk: ">=2.3.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + 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 index 311628853159..00bcc7664908 100644 --- a/packages/android_intent/test/android_intent_test.dart +++ b/packages/android_intent/test/android_intent_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -6,14 +6,23 @@ 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() { - AndroidIntent androidIntent; - MockMethodChannel mockChannel; + 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', () { @@ -173,5 +182,3 @@ void main() { }); }); } - -class MockMethodChannel extends Mock implements MethodChannel {} diff --git a/packages/android_intent/test/android_intent_test.mocks.dart b/packages/android_intent/test/android_intent_test.mocks.dart new file mode 100644 index 000000000000..fed1624ad069 --- /dev/null +++ b/packages/android_intent/test/android_intent_test.mocks.dart @@ -0,0 +1,64 @@ +// 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 new file mode 100644 index 000000000000..cda4f6e153e6 --- /dev/null +++ b/packages/battery/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options_legacy.yaml diff --git a/packages/battery/battery/AUTHORS b/packages/battery/battery/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/battery/battery/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/battery/battery/CHANGELOG.md b/packages/battery/battery/CHANGELOG.md index 8acfb5b075ad..b4f0cef94edb 100644 --- a/packages/battery/battery/CHANGELOG.md +++ b/packages/battery/battery/CHANGELOG.md @@ -1,3 +1,39 @@ +## 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. diff --git a/packages/battery/battery/LICENSE b/packages/battery/battery/LICENSE index a6d6c0749818..c6823b81eb84 100644 --- a/packages/battery/battery/LICENSE +++ b/packages/battery/battery/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017 The Chromium Authors. All rights reserved. +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: diff --git a/packages/battery/battery/README.md b/packages/battery/battery/README.md index 22ce5007acd7..5337978b4d79 100644 --- a/packages/battery/battery/README.md +++ b/packages/battery/battery/README.md @@ -1,11 +1,26 @@ # Battery -[![pub package](https://img.shields.io/pub/v/battery.svg)](https://pub.dartlang.org/packages/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.io/platform-plugins/). +To use this plugin, add `battery` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). ### Example diff --git a/packages/battery/battery/android/build.gradle b/packages/battery/battery/android/build.gradle index ff485fbc6b29..28d561f05652 100644 --- a/packages/battery/battery/android/build.gradle +++ b/packages/battery/battery/android/build.gradle @@ -4,7 +4,7 @@ version '1.0-SNAPSHOT' buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -15,14 +15,14 @@ buildscript { rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } apply plugin: 'com.android.library' android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { minSdkVersion 16 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 index f16d219291d1..7f2e1efbeede 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/battery/battery/example/README.md b/packages/battery/battery/example/README.md index dcb94ed1b616..ac3fc4d8a470 100644 --- a/packages/battery/battery/example/README.md +++ b/packages/battery/battery/example/README.md @@ -5,4 +5,4 @@ Demonstrates how to use the battery plugin. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). diff --git a/packages/battery/battery/example/android/app/build.gradle b/packages/battery/battery/example/android/app/build.gradle index e84c2c45889d..4fcd6ba0049e 100644 --- a/packages/battery/battery/example/android/app/build.gradle +++ b/packages/battery/battery/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 28 + compileSdkVersion 29 lintOptions { disable 'InvalidPackage' diff --git a/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/EmbedderV1ActivityTest.java b/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/EmbedderV1ActivityTest.java index 1bd860eb147c..c939be4281da 100644 --- a/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/EmbedderV1ActivityTest.java +++ b/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/EmbedderV1ActivityTest.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/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 index 636c716d7a91..267271f70f42 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/battery/battery/example/android/app/src/main/java/io/flutter/plugins/batteryexample/EmbedderV1Activity.java b/packages/battery/battery/example/android/app/src/main/java/io/flutter/plugins/batteryexample/EmbedderV1Activity.java index 5fa885cdd9d8..2b9e538bbe47 100644 --- a/packages/battery/battery/example/android/app/src/main/java/io/flutter/plugins/batteryexample/EmbedderV1Activity.java +++ b/packages/battery/battery/example/android/app/src/main/java/io/flutter/plugins/batteryexample/EmbedderV1Activity.java @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/battery/battery/example/android/build.gradle b/packages/battery/battery/example/android/build.gradle index 541636cc492a..e101ac08df55 100644 --- a/packages/battery/battery/example/android/build.gradle +++ b/packages/battery/battery/example/android/build.gradle @@ -1,7 +1,7 @@ buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -12,7 +12,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/packages/battery/battery/example/integration_test/battery_test.dart b/packages/battery/battery/example/integration_test/battery_test.dart new file mode 100644 index 000000000000..eced27e5a1cd --- /dev/null +++ b/packages/battery/battery/example/integration_test/battery_test.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. + +// @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/Podfile b/packages/battery/battery/example/ios/Podfile new file mode 100644 index 000000000000..f7d6a5e68c3a --- /dev/null +++ b/packages/battery/battery/example/ios/Podfile @@ -0,0 +1,38 @@ +# 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/battery/battery/example/ios/Runner.xcodeproj/project.pbxproj b/packages/battery/battery/example/ios/Runner.xcodeproj/project.pbxproj index aa42a8509346..ad48b12527b0 100644 --- a/packages/battery/battery/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/battery/battery/example/ios/Runner.xcodeproj/project.pbxproj @@ -177,7 +177,7 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; + ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; @@ -437,7 +437,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.batteryExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.batteryExample; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -458,7 +458,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.batteryExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.batteryExample; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; diff --git a/packages/battery/battery/example/ios/Runner/AppDelegate.h b/packages/battery/battery/example/ios/Runner/AppDelegate.h index d9e18e990f2e..0681d288bb70 100644 --- a/packages/battery/battery/example/ios/Runner/AppDelegate.h +++ b/packages/battery/battery/example/ios/Runner/AppDelegate.h @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/battery/battery/example/ios/Runner/AppDelegate.m b/packages/battery/battery/example/ios/Runner/AppDelegate.m index a4b51c88eb60..b790a0a52635 100644 --- a/packages/battery/battery/example/ios/Runner/AppDelegate.m +++ b/packages/battery/battery/example/ios/Runner/AppDelegate.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/battery/battery/example/ios/Runner/main.m b/packages/battery/battery/example/ios/Runner/main.m index bec320c0bee0..f97b9ef5c8a1 100644 --- a/packages/battery/battery/example/ios/Runner/main.m +++ b/packages/battery/battery/example/ios/Runner/main.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/battery/battery/example/lib/main.dart b/packages/battery/battery/example/lib/main.dart index 1c1dfcf252b6..b139d0d8e4be 100644 --- a/packages/battery/battery/example/lib/main.dart +++ b/packages/battery/battery/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -27,7 +27,7 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); + MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @@ -36,10 +36,10 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - Battery _battery = Battery(); + final Battery _battery = Battery(); - BatteryState _batteryState; - StreamSubscription _batteryStateSubscription; + BatteryState? _batteryState; + late StreamSubscription _batteryStateSubscription; @override void initState() { @@ -71,7 +71,7 @@ class _MyHomePageState extends State { builder: (_) => AlertDialog( content: Text('Battery: $batteryLevel%'), actions: [ - FlatButton( + TextButton( child: const Text('OK'), onPressed: () { Navigator.pop(context); diff --git a/packages/battery/battery/example/pubspec.yaml b/packages/battery/battery/example/pubspec.yaml index e7a9b1c8b74d..e33dda9b86a3 100644 --- a/packages/battery/battery/example/pubspec.yaml +++ b/packages/battery/battery/example/pubspec.yaml @@ -1,18 +1,28 @@ 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: - path: ../../../integration_test - pedantic: ^1.8.0 + 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 new file mode 100644 index 000000000000..6a0e6fa82dbe --- /dev/null +++ b/packages/battery/battery/example/test_driver/integration_test.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. + +// @dart=2.9 + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/battery/battery/integration_test/battery_test.dart b/packages/battery/battery/integration_test/battery_test.dart deleted file mode 100644 index ed7b6fe5a0e4..000000000000 --- a/packages/battery/battery/integration_test/battery_test.dart +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -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(); - final int batteryLevel = await battery.batteryLevel; - expect(batteryLevel, isNotNull); - }); -} diff --git a/packages/battery/battery/ios/Classes/FLTBatteryPlugin.h b/packages/battery/battery/ios/Classes/FLTBatteryPlugin.h index 9743ca501208..fd6a3e964d83 100644 --- a/packages/battery/battery/ios/Classes/FLTBatteryPlugin.h +++ b/packages/battery/battery/ios/Classes/FLTBatteryPlugin.h @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/battery/battery/ios/Classes/FLTBatteryPlugin.m b/packages/battery/battery/ios/Classes/FLTBatteryPlugin.m index f1e82a64eb1b..558d395bb9c0 100644 --- a/packages/battery/battery/ios/Classes/FLTBatteryPlugin.m +++ b/packages/battery/battery/ios/Classes/FLTBatteryPlugin.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/battery/battery/lib/battery.dart b/packages/battery/battery/lib/battery.dart index e3943e49599a..92e54be095fe 100644 --- a/packages/battery/battery/lib/battery.dart +++ b/packages/battery/battery/lib/battery.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/battery/battery/pubspec.yaml b/packages/battery/battery/pubspec.yaml index 147d62ae46cd..05226e3f8029 100644 --- a/packages/battery/battery/pubspec.yaml +++ b/packages/battery/battery/pubspec.yaml @@ -1,8 +1,13 @@ name: battery description: Flutter plugin for accessing information about the battery state (full, charging, discharging) on Android and iOS. -homepage: https://github.com/flutter/plugins/tree/master/packages/battery/battery -version: 1.0.6 +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: @@ -16,20 +21,14 @@ flutter: dependencies: flutter: sdk: flutter - meta: ^1.0.5 - battery_platform_interface: ^1.0.0 + meta: ^1.3.0 + battery_platform_interface: ^2.0.0 dev_dependencies: - async: ^2.0.8 - test: ^1.3.0 - mockito: ^4.1.1 flutter_test: sdk: flutter - plugin_platform_interface: ^1.0.0 + plugin_platform_interface: ^2.0.0 integration_test: - path: ../../integration_test - pedantic: ^1.8.0 - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + 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 index 5c789207d7eb..8870acb775de 100644 --- a/packages/battery/battery/test/battery_test.dart +++ b/packages/battery/battery/test/battery_test.dart @@ -1,18 +1,18 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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/test.dart'; -import 'package:battery/battery.dart'; -import 'package:mockito/mockito.dart'; +import 'package:test/fake.dart'; void main() { group('battery', () { - Battery battery; + late Battery battery; MockBatteryPlatform fakePlatform; setUp(() async { fakePlatform = MockBatteryPlatform(); @@ -30,7 +30,7 @@ void main() { }); } -class MockBatteryPlatform extends Mock +class MockBatteryPlatform extends Fake with MockPlatformInterfaceMixin implements BatteryPlatform { Future batteryLevel() async { diff --git a/packages/battery/battery_platform_interface/AUTHORS b/packages/battery/battery_platform_interface/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/battery/battery_platform_interface/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/battery/battery_platform_interface/CHANGELOG.md b/packages/battery/battery_platform_interface/CHANGELOG.md index 6fadda91b380..a9106dd78ce9 100644 --- a/packages/battery/battery_platform_interface/CHANGELOG.md +++ b/packages/battery/battery_platform_interface/CHANGELOG.md @@ -1,3 +1,15 @@ +## 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/LICENSE b/packages/battery/battery_platform_interface/LICENSE index a6d6c0749818..c6823b81eb84 100644 --- a/packages/battery/battery_platform_interface/LICENSE +++ b/packages/battery/battery_platform_interface/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017 The Chromium Authors. All rights reserved. +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: diff --git a/packages/battery/battery_platform_interface/lib/battery_platform_interface.dart b/packages/battery/battery_platform_interface/lib/battery_platform_interface.dart index f803c7aaa8fd..548edf8257f5 100644 --- a/packages/battery/battery_platform_interface/lib/battery_platform_interface.dart +++ b/packages/battery/battery_platform_interface/lib/battery_platform_interface.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/battery/battery_platform_interface/lib/enums/battery_state.dart b/packages/battery/battery_platform_interface/lib/enums/battery_state.dart index 7dd5e400faf2..a525e78ccdf5 100644 --- a/packages/battery/battery_platform_interface/lib/enums/battery_state.dart +++ b/packages/battery/battery_platform_interface/lib/enums/battery_state.dart @@ -1,3 +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. + /// Indicates the current battery state. enum BatteryState { /// The battery is completely full of energy. diff --git a/packages/battery/battery_platform_interface/lib/method_channel/method_channel_battery.dart b/packages/battery/battery_platform_interface/lib/method_channel/method_channel_battery.dart index 4a3365cc2475..1d0a8329c257 100644 --- a/packages/battery/battery_platform_interface/lib/method_channel/method_channel_battery.dart +++ b/packages/battery/battery_platform_interface/lib/method_channel/method_channel_battery.dart @@ -1,3 +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 'dart:async'; import 'package:flutter/services.dart'; @@ -11,11 +15,11 @@ import '../battery_platform_interface.dart'; class MethodChannelBattery extends BatteryPlatform { /// The method channel used to interact with the native platform. @visibleForTesting - MethodChannel channel = MethodChannel('plugins.flutter.io/battery'); + final MethodChannel channel = MethodChannel('plugins.flutter.io/battery'); /// The event channel used to interact with the native platform. @visibleForTesting - EventChannel eventChannel = EventChannel('plugins.flutter.io/charging'); + final EventChannel eventChannel = EventChannel('plugins.flutter.io/charging'); /// Method channel for getting battery level. Future batteryLevel() async { @@ -23,7 +27,7 @@ class MethodChannelBattery extends BatteryPlatform { } /// Stream variable for storing battery state. - Stream _onBatteryStateChanged; + Stream? _onBatteryStateChanged; /// Event channel for getting battery change state. Stream onBatteryStateChanged() { @@ -32,7 +36,8 @@ class MethodChannelBattery extends BatteryPlatform { .receiveBroadcastStream() .map((dynamic event) => _parseBatteryState(event)); } - return _onBatteryStateChanged; + + return _onBatteryStateChanged!; } } diff --git a/packages/battery/battery_platform_interface/pubspec.yaml b/packages/battery/battery_platform_interface/pubspec.yaml index 6c571debc7b0..461cd0bd88ea 100644 --- a/packages/battery/battery_platform_interface/pubspec.yaml +++ b/packages/battery/battery_platform_interface/pubspec.yaml @@ -1,22 +1,23 @@ name: battery_platform_interface description: A common platform interface for the battery plugin. -homepage: https://github.com/flutter/plugins/tree/master/packages/battery +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: 1.0.0 +version: 2.0.1 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: sdk: flutter - meta: ^1.1.8 - plugin_platform_interface: ^1.0.2 + meta: ^1.3.0 + plugin_platform_interface: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - mockito: ^4.1.1 - pedantic: ^1.8.0 - -environment: - sdk: ">=2.7.0 <3.0.0" - flutter: ">=1.9.1+hotfix.4 <2.0.0" + 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 index 65323e4044de..6a312ee73ef2 100644 --- a/packages/battery/battery_platform_interface/test/method_channel_battery_test.dart +++ b/packages/battery/battery_platform_interface/test/method_channel_battery_test.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -12,8 +12,8 @@ import 'package:battery_platform_interface/method_channel/method_channel_battery void main() { TestWidgetsFlutterBinding.ensureInitialized(); - group("$MethodChannelBattery", () { - MethodChannelBattery methodChannelBattery; + group('$MethodChannelBattery', () { + late MethodChannelBattery methodChannelBattery; setUp(() async { methodChannelBattery = MethodChannelBattery(); @@ -32,7 +32,7 @@ void main() { .setMockMethodCallHandler((MethodCall methodCall) async { switch (methodCall.method) { case 'listen': - await ServicesBinding.instance.defaultBinaryMessenger + await ServicesBinding.instance!.defaultBinaryMessenger .handlePlatformMessage( methodChannelBattery.eventChannel.name, methodChannelBattery.eventChannel.codec @@ -48,13 +48,13 @@ void main() { }); /// Test for batetry level call. - test("getBatteryLevel", () async { + test('getBatteryLevel', () async { final int result = await methodChannelBattery.batteryLevel(); expect(result, 90); }); /// Test for battery changed state call. - test("onBatteryChanged", () async { + test('onBatteryChanged', () async { final BatteryState result = await methodChannelBattery.onBatteryStateChanged().first; expect(result, BatteryState.full); diff --git a/packages/camera/CHANGELOG.md b/packages/camera/CHANGELOG.md deleted file mode 100644 index 81774679389d..000000000000 --- a/packages/camera/CHANGELOG.md +++ /dev/null @@ -1,296 +0,0 @@ -## 0.5.8+7 - -* Keep handling deprecated Android v1 classes for backward compatibility. - -## 0.5.8+6 - -* Avoiding uses or overrides a deprecated API in CameraPlugin.java. - -## 0.5.8+5 - -* Fix compilation/availability issues on iOS. - -## 0.5.8+4 - -* Fixed bug caused by casting a `CameraAccessException` on Android. - -## 0.5.8+3 - -* Fix bug in usage example in README.md - -## 0.5.8+2 - -* Post-v2 embedding cleanups. - -## 0.5.8+1 - -* Update lower bound of dart dependency to 2.1.0. - -## 0.5.8 - -* Remove Android dependencies fallback. -* Require Flutter SDK 1.12.13+hotfix.5 or greater. - -## 0.5.7+5 - -* Replace deprecated `getFlutterEngine` call on Android. - -## 0.5.7+4 - -* Add `pedantic` to dev_dependency. - -## 0.5.7+3 - -* Fix an Android crash when permissions are requested multiple times. - -## 0.5.7+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.5.7+1 - -* Fix example null exception. - -## 0.5.7 - -* Fix unawaited futures. - -## 0.5.6+4 - -* Android: Use CameraDevice.TEMPLATE_RECORD to improve image streaming. - -## 0.5.6+3 - -* Remove AndroidX warning. - -## 0.5.6+2 - -* Include lifecycle dependency as a compileOnly one on Android to resolve - potential version conflicts with other transitive libraries. - -## 0.5.6+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.5.6 - -* Add support for the v2 Android embedding. This shouldn't affect existing - functionality. - -## 0.5.5+1 - -* Fix event type check - -## 0.5.5 - -* Define clang modules for iOS. - -## 0.5.4+3 - -* Update and migrate iOS example project. - -## 0.5.4+2 - -* Fix Android NullPointerException on devices with only front-facing camera. - -## 0.5.4+1 - -* Fix Android pause and resume video crash when executing in APIs below 24. - -## 0.5.4 - -* Add feature to pause and resume video recording. - -## 0.5.3+1 - -* Fix too large request code for FragmentActivity users. - -## 0.5.3 - -* Added new quality presets. -* Now all quality presets can be used to control image capture quality. - -## 0.5.2+2 - -* Fix memory leak related to not unregistering stream handler in FlutterEventChannel when disposing camera. - -## 0.5.2+1 - -* Fix bug that prevented video recording with audio. - -## 0.5.2 - -* Added capability to disable audio for the `CameraController`. (e.g. `CameraController(_, _, - enableAudio: false);`) - -## 0.5.1 - -* Can now be compiled with earlier Android sdks below 21 when -`` has been added to the project -`AndroidManifest.xml`. For sdks below 21, the plugin won't be registered and calls to it will throw -a `MissingPluginException.` - -## 0.5.0 - -* **Breaking Change** This plugin no longer handles closing and opening the camera on Android - lifecycle changes. Please use `WidgetsBindingObserver` to control camera resources on lifecycle - changes. See example project for example using `WidgetsBindingObserver`. - -## 0.4.3+2 - -* Bump the minimum Flutter version to 1.2.0. -* Add template type parameter to `invokeMethod` calls. - -## 0.4.3+1 - -* Catch additional `Exception`s from Android and throw as `CameraException`s. - -## 0.4.3 - -* Add capability to prepare the capture session for video recording on iOS. - -## 0.4.2 - -* Add sensor orientation value to `CameraDescription`. - -## 0.4.1 - -* Camera methods are ran in a background thread on iOS. - -## 0.4.0+3 - -* Fixed a crash when the plugin is registered by a background FlutterView. - -## 0.4.0+2 - -* Fix orientation of captured photos when camera is used for the first time on Android. - -## 0.4.0+1 - -* Remove categories. - -## 0.4.0 - -* **Breaking Change** Change iOS image stream format to `ImageFormatGroup.bgra8888` from - `ImageFormatGroup.yuv420`. - -## 0.3.0+4 - -* Fixed bug causing black screen on some Android devices. - -## 0.3.0+3 - -* Log a more detailed warning at build time about the previous AndroidX - migration. - -## 0.3.0+2 - -* Fix issue with calculating iOS image orientation in certain edge cases. - -## 0.3.0+1 - -* Remove initial method call invocation from static camera method. - -## 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.9+1 - -* Fix a crash when failing to start preview. - -## 0.2.9 - -* Save photo orientation data on iOS. - -## 0.2.8 - -* Add access to the image stream from Dart. -* Use `cameraController.startImageStream(listener)` to process the images. - -## 0.2.7 - -* Fix issue with crash when the physical device's orientation is unknown. - -## 0.2.6 - -* Update the camera to use the physical device's orientation instead of the UI - orientation on Android. - -## 0.2.5 - -* Fix preview and video size with satisfying conditions of multiple outputs. - -## 0.2.4 - -* Unregister the activity lifecycle callbacks when disposing the camera. - -## 0.2.3 - -* Added path_provider and video_player as dev dependencies because the example uses them. -* Updated example path_provider version to get Dart 2 support. - -## 0.2.2 - -* iOS image capture is done in high quality (full camera size) - -## 0.2.1 - -* Updated Gradle tooling to match Android Studio 3.1.2. - -## 0.2.0 - -* Added support for video recording. -* Changed the example app to add video recording. - -A lot of **breaking changes** in this version: - -Getter changes: - - Removed `isStarted` - - Renamed `initialized` to `isInitialized` - - Added `isRecordingVideo` - -Method changes: - - Renamed `capture` to `takePicture` - - Removed `start` (the preview starts automatically when `initialize` is called) - - Added `startVideoRecording(String filePath)` - - Removed `stop` (the preview stops automatically when `dispose` is called) - - Added `stopVideoRecording` - -## 0.1.2 - -* Fix Dart 2 runtime errors. - -## 0.1.1 - -* Fix Dart 2 runtime error. - -## 0.1.0 - -* **Breaking change**. Set SDK constraints to match the Flutter beta release. - -## 0.0.4 - -* Revert regression of `CameraController.capture()` introduced in v. 0.0.3. - -## 0.0.3 - -* Improved resource cleanup on Android. Avoids crash on Activity restart. -* Made the Future returned by `CameraController.dispose()` and `CameraController.capture()` actually complete on - Android. - -## 0.0.2 - -* Simplified and upgraded Android project template to Android SDK 27. -* Moved Android package to io.flutter.plugins. -* Fixed warnings from the Dart 2.0 analyzer. - -## 0.0.1 - -* Initial release diff --git a/packages/camera/LICENSE b/packages/camera/LICENSE deleted file mode 100644 index a6d6c0749818..000000000000 --- a/packages/camera/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright 2017 The Chromium Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/camera/README.md b/packages/camera/README.md deleted file mode 100644 index fbd73929e6de..000000000000 --- a/packages/camera/README.md +++ /dev/null @@ -1,104 +0,0 @@ -# Camera Plugin - -[![pub package](https://img.shields.io/pub/v/camera.svg)](https://pub.dartlang.org/packages/camera) - -A Flutter plugin for iOS and Android 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) - -## Features: - -* Display live camera preview in a widget. -* Snapshots can be captured and saved to a file. -* Record video. -* Add access to the image stream from Dart. - -## Installation - -First, add `camera` as a [dependency in your pubspec.yaml file](https://flutter.io/using-packages/). - -### iOS - -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: - -```xml -NSCameraUsageDescription -Can I use the camera please? -NSMicrophoneUsageDescription -Can I use the mic please? -``` - -### Android - -Change the minimum Android sdk version to 21 (or higher) in your `android/app/build.gradle` file. - -``` -minSdkVersion 21 -``` - -### 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'; - -List cameras; - -Future main() async { - WidgetsFlutterBinding.ensureInitialized(); - cameras = await availableCameras(); - runApp(CameraApp()); -} - -class CameraApp extends StatefulWidget { - @override - _CameraAppState createState() => _CameraAppState(); -} - -class _CameraAppState extends State { - CameraController controller; - - @override - void initState() { - super.initState(); - controller = CameraController(cameras[0], ResolutionPreset.medium); - controller.initialize().then((_) { - if (!mounted) { - return; - } - setState(() {}); - }); - } - - @override - void dispose() { - controller?.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (!controller.value.isInitialized) { - return Container(); - } - return AspectRatio( - aspectRatio: - controller.value.aspectRatio, - child: CameraPreview(controller)); - } -} -``` - -For a more elaborate usage example see [here](https://github.com/flutter/plugins/tree/master/packages/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! diff --git a/packages/camera/analysis_options.yaml b/packages/camera/analysis_options.yaml index 8e4af76f0a30..cda4f6e153e6 100644 --- a/packages/camera/analysis_options.yaml +++ b/packages/camera/analysis_options.yaml @@ -1,10 +1 @@ -# This is a temporary file 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. - -include: ../../analysis_options.yaml - -analyzer: - errors: - public_member_api_docs: ignore +include: ../../analysis_options_legacy.yaml diff --git a/packages/camera/android/build.gradle b/packages/camera/android/build.gradle deleted file mode 100644 index 3ff98a8d7f47..000000000000 --- a/packages/camera/android/build.gradle +++ /dev/null @@ -1,54 +0,0 @@ -group 'io.flutter.plugins.camera' -version '1.0-SNAPSHOT' -def args = ["-Xlint:deprecation","-Xlint:unchecked","-Werror"] - -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' - } -} - -rootProject.allprojects { - repositories { - google() - jcenter() - } -} - -project.getTasks().withType(JavaCompile){ - options.compilerArgs.addAll(args) -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 28 - - defaultConfig { - minSdkVersion 21 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - } - compileOptions { - sourceCompatibility = '1.8' - targetCompatibility = '1.8' - } - dependencies { - implementation 'androidx.annotation:annotation:1.0.0' - implementation 'androidx.core:core:1.0.0' - } - testOptions { - unitTests.returnDefaultValues = true - } -} - -dependencies { - testImplementation 'junit:junit:4.12' -} diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java deleted file mode 100644 index 0fcda278d836..000000000000 --- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java +++ /dev/null @@ -1,520 +0,0 @@ -package io.flutter.plugins.camera; - -import static android.view.OrientationEventListener.ORIENTATION_UNKNOWN; -import static io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize; - -import android.annotation.SuppressLint; -import android.app.Activity; -import android.content.Context; -import android.graphics.ImageFormat; -import android.graphics.SurfaceTexture; -import android.hardware.camera2.CameraAccessException; -import android.hardware.camera2.CameraCaptureSession; -import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CameraDevice; -import android.hardware.camera2.CameraManager; -import android.hardware.camera2.CameraMetadata; -import android.hardware.camera2.CaptureFailure; -import android.hardware.camera2.CaptureRequest; -import android.hardware.camera2.params.StreamConfigurationMap; -import android.media.CamcorderProfile; -import android.media.Image; -import android.media.ImageReader; -import android.media.MediaRecorder; -import android.os.Build; -import android.util.Size; -import android.view.OrientationEventListener; -import android.view.Surface; -import androidx.annotation.NonNull; -import io.flutter.plugin.common.EventChannel; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.view.TextureRegistry.SurfaceTextureEntry; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class Camera { - private final SurfaceTextureEntry flutterTexture; - private final CameraManager cameraManager; - private final OrientationEventListener orientationEventListener; - private final boolean isFrontFacing; - private final int sensorOrientation; - private final String cameraName; - private final Size captureSize; - private final Size previewSize; - private final boolean enableAudio; - - private CameraDevice cameraDevice; - private CameraCaptureSession cameraCaptureSession; - private ImageReader pictureImageReader; - private ImageReader imageStreamReader; - private DartMessenger dartMessenger; - private CaptureRequest.Builder captureRequestBuilder; - private MediaRecorder mediaRecorder; - private boolean recordingVideo; - private CamcorderProfile recordingProfile; - private int currentOrientation = ORIENTATION_UNKNOWN; - - // Mirrors camera.dart - public enum ResolutionPreset { - low, - medium, - high, - veryHigh, - ultraHigh, - max, - } - - public Camera( - final Activity activity, - final SurfaceTextureEntry flutterTexture, - final DartMessenger dartMessenger, - final String cameraName, - final String resolutionPreset, - final boolean enableAudio) - throws CameraAccessException { - if (activity == null) { - throw new IllegalStateException("No activity available!"); - } - - this.cameraName = cameraName; - this.enableAudio = enableAudio; - this.flutterTexture = flutterTexture; - this.dartMessenger = dartMessenger; - this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); - orientationEventListener = - new OrientationEventListener(activity.getApplicationContext()) { - @Override - public void onOrientationChanged(int i) { - if (i == ORIENTATION_UNKNOWN) { - return; - } - // Convert the raw deg angle to the nearest multiple of 90. - currentOrientation = (int) Math.round(i / 90.0) * 90; - } - }; - orientationEventListener.enable(); - - CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName); - StreamConfigurationMap streamConfigurationMap = - characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); - //noinspection ConstantConditions - sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); - //noinspection ConstantConditions - isFrontFacing = - characteristics.get(CameraCharacteristics.LENS_FACING) == CameraMetadata.LENS_FACING_FRONT; - ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset); - recordingProfile = - CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); - captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); - previewSize = computeBestPreviewSize(cameraName, preset); - } - - private void prepareMediaRecorder(String outputFilePath) throws IOException { - if (mediaRecorder != null) { - mediaRecorder.release(); - } - mediaRecorder = new MediaRecorder(); - - // There's a specific order that mediaRecorder expects. Do not change the order - // of these function calls. - if (enableAudio) mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); - mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE); - mediaRecorder.setOutputFormat(recordingProfile.fileFormat); - if (enableAudio) mediaRecorder.setAudioEncoder(recordingProfile.audioCodec); - mediaRecorder.setVideoEncoder(recordingProfile.videoCodec); - mediaRecorder.setVideoEncodingBitRate(recordingProfile.videoBitRate); - if (enableAudio) mediaRecorder.setAudioSamplingRate(recordingProfile.audioSampleRate); - mediaRecorder.setVideoFrameRate(recordingProfile.videoFrameRate); - mediaRecorder.setVideoSize(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); - mediaRecorder.setOutputFile(outputFilePath); - mediaRecorder.setOrientationHint(getMediaOrientation()); - - mediaRecorder.prepare(); - } - - @SuppressLint("MissingPermission") - public void open(@NonNull final Result result) throws CameraAccessException { - pictureImageReader = - ImageReader.newInstance( - captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); - - // Used to steam image byte data to dart side. - imageStreamReader = - ImageReader.newInstance( - previewSize.getWidth(), previewSize.getHeight(), ImageFormat.YUV_420_888, 2); - - cameraManager.openCamera( - cameraName, - new CameraDevice.StateCallback() { - @Override - public void onOpened(@NonNull CameraDevice device) { - cameraDevice = device; - try { - startPreview(); - } catch (CameraAccessException e) { - result.error("CameraAccess", e.getMessage(), null); - close(); - return; - } - Map reply = new HashMap<>(); - reply.put("textureId", flutterTexture.id()); - reply.put("previewWidth", previewSize.getWidth()); - reply.put("previewHeight", previewSize.getHeight()); - result.success(reply); - } - - @Override - public void onClosed(@NonNull CameraDevice camera) { - dartMessenger.sendCameraClosingEvent(); - super.onClosed(camera); - } - - @Override - public void onDisconnected(@NonNull CameraDevice cameraDevice) { - close(); - dartMessenger.send(DartMessenger.EventType.ERROR, "The camera was disconnected."); - } - - @Override - public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { - close(); - String errorDescription; - switch (errorCode) { - case ERROR_CAMERA_IN_USE: - errorDescription = "The camera device is in use already."; - break; - case ERROR_MAX_CAMERAS_IN_USE: - errorDescription = "Max cameras in use"; - break; - case ERROR_CAMERA_DISABLED: - errorDescription = "The camera device could not be opened due to a device policy."; - break; - case ERROR_CAMERA_DEVICE: - errorDescription = "The camera device has encountered a fatal error"; - break; - case ERROR_CAMERA_SERVICE: - errorDescription = "The camera service has encountered a fatal error."; - break; - default: - errorDescription = "Unknown camera error"; - } - dartMessenger.send(DartMessenger.EventType.ERROR, errorDescription); - } - }, - null); - } - - private void writeToFile(ByteBuffer buffer, File file) throws IOException { - try (FileOutputStream outputStream = new FileOutputStream(file)) { - while (0 < buffer.remaining()) { - outputStream.getChannel().write(buffer); - } - } - } - - SurfaceTextureEntry getFlutterTexture() { - return flutterTexture; - } - - public void takePicture(String filePath, @NonNull final Result result) { - final File file = new File(filePath); - - if (file.exists()) { - result.error( - "fileExists", "File at path '" + filePath + "' already exists. Cannot overwrite.", null); - return; - } - - pictureImageReader.setOnImageAvailableListener( - reader -> { - try (Image image = reader.acquireLatestImage()) { - ByteBuffer buffer = image.getPlanes()[0].getBuffer(); - writeToFile(buffer, file); - result.success(null); - } catch (IOException e) { - result.error("IOError", "Failed saving image", null); - } - }, - null); - - try { - final CaptureRequest.Builder captureBuilder = - cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); - captureBuilder.addTarget(pictureImageReader.getSurface()); - captureBuilder.set(CaptureRequest.JPEG_ORIENTATION, getMediaOrientation()); - - cameraCaptureSession.capture( - captureBuilder.build(), - new CameraCaptureSession.CaptureCallback() { - @Override - public void onCaptureFailed( - @NonNull CameraCaptureSession session, - @NonNull CaptureRequest request, - @NonNull CaptureFailure failure) { - String reason; - switch (failure.getReason()) { - case CaptureFailure.REASON_ERROR: - reason = "An error happened in the framework"; - break; - case CaptureFailure.REASON_FLUSHED: - reason = "The capture has failed due to an abortCaptures() call"; - break; - default: - reason = "Unknown reason"; - } - result.error("captureFailure", reason, null); - } - }, - null); - } catch (CameraAccessException e) { - result.error("cameraAccess", e.getMessage(), null); - } - } - - private void createCaptureSession(int templateType, Surface... surfaces) - throws CameraAccessException { - createCaptureSession(templateType, null, surfaces); - } - - private void createCaptureSession( - int templateType, Runnable onSuccessCallback, Surface... surfaces) - throws CameraAccessException { - // Close any existing capture session. - closeCaptureSession(); - - // Create a new capture builder. - captureRequestBuilder = cameraDevice.createCaptureRequest(templateType); - - // Build Flutter surface to render to - SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); - surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); - Surface flutterSurface = new Surface(surfaceTexture); - captureRequestBuilder.addTarget(flutterSurface); - - List remainingSurfaces = Arrays.asList(surfaces); - if (templateType != CameraDevice.TEMPLATE_PREVIEW) { - // If it is not preview mode, add all surfaces as targets. - for (Surface surface : remainingSurfaces) { - captureRequestBuilder.addTarget(surface); - } - } - - // Prepare the callback - CameraCaptureSession.StateCallback callback = - new CameraCaptureSession.StateCallback() { - @Override - public void onConfigured(@NonNull CameraCaptureSession session) { - try { - if (cameraDevice == null) { - dartMessenger.send( - DartMessenger.EventType.ERROR, "The camera was closed during configuration."); - return; - } - cameraCaptureSession = session; - captureRequestBuilder.set( - CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO); - cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); - if (onSuccessCallback != null) { - onSuccessCallback.run(); - } - } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { - dartMessenger.send(DartMessenger.EventType.ERROR, e.getMessage()); - } - } - - @Override - public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { - dartMessenger.send( - DartMessenger.EventType.ERROR, "Failed to configure camera session."); - } - }; - - // Collect all surfaces we want to render to. - List surfaceList = new ArrayList<>(); - surfaceList.add(flutterSurface); - surfaceList.addAll(remainingSurfaces); - // Start the session - cameraDevice.createCaptureSession(surfaceList, callback, null); - } - - public void startVideoRecording(String filePath, Result result) { - if (new File(filePath).exists()) { - result.error("fileExists", "File at path '" + filePath + "' already exists.", null); - return; - } - try { - prepareMediaRecorder(filePath); - recordingVideo = true; - createCaptureSession( - CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); - result.success(null); - } catch (CameraAccessException | IOException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - } - } - - public void stopVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; - } - - try { - recordingVideo = false; - mediaRecorder.stop(); - mediaRecorder.reset(); - startPreview(); - result.success(null); - } catch (CameraAccessException | IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - } - } - - public void pauseVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; - } - - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - mediaRecorder.pause(); - } else { - result.error("videoRecordingFailed", "pauseVideoRecording requires Android API +24.", null); - return; - } - } catch (IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - return; - } - - result.success(null); - } - - public void resumeVideoRecording(@NonNull final Result result) { - if (!recordingVideo) { - result.success(null); - return; - } - - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - mediaRecorder.resume(); - } else { - result.error( - "videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); - return; - } - } catch (IllegalStateException e) { - result.error("videoRecordingFailed", e.getMessage(), null); - return; - } - - result.success(null); - } - - public void startPreview() throws CameraAccessException { - createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); - } - - public void startPreviewWithImageStream(EventChannel imageStreamChannel) - throws CameraAccessException { - createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); - - imageStreamChannel.setStreamHandler( - new EventChannel.StreamHandler() { - @Override - public void onListen(Object o, EventChannel.EventSink imageStreamSink) { - setImageStreamImageAvailableListener(imageStreamSink); - } - - @Override - public void onCancel(Object o) { - imageStreamReader.setOnImageAvailableListener(null, null); - } - }); - } - - private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { - imageStreamReader.setOnImageAvailableListener( - reader -> { - Image img = reader.acquireLatestImage(); - if (img == null) return; - - List> planes = new ArrayList<>(); - for (Image.Plane plane : img.getPlanes()) { - ByteBuffer buffer = plane.getBuffer(); - - byte[] bytes = new byte[buffer.remaining()]; - buffer.get(bytes, 0, bytes.length); - - Map planeBuffer = new HashMap<>(); - planeBuffer.put("bytesPerRow", plane.getRowStride()); - planeBuffer.put("bytesPerPixel", plane.getPixelStride()); - planeBuffer.put("bytes", bytes); - - planes.add(planeBuffer); - } - - Map imageBuffer = new HashMap<>(); - imageBuffer.put("width", img.getWidth()); - imageBuffer.put("height", img.getHeight()); - imageBuffer.put("format", img.getFormat()); - imageBuffer.put("planes", planes); - - imageStreamSink.success(imageBuffer); - img.close(); - }, - null); - } - - private void closeCaptureSession() { - if (cameraCaptureSession != null) { - cameraCaptureSession.close(); - cameraCaptureSession = null; - } - } - - public void close() { - closeCaptureSession(); - - if (cameraDevice != null) { - cameraDevice.close(); - cameraDevice = null; - } - if (pictureImageReader != null) { - pictureImageReader.close(); - pictureImageReader = null; - } - if (imageStreamReader != null) { - imageStreamReader.close(); - imageStreamReader = null; - } - if (mediaRecorder != null) { - mediaRecorder.reset(); - mediaRecorder.release(); - mediaRecorder = null; - } - } - - public void dispose() { - close(); - flutterTexture.release(); - orientationEventListener.disable(); - } - - private int getMediaOrientation() { - final int sensorOrientationOffset = - (currentOrientation == ORIENTATION_UNKNOWN) - ? 0 - : (isFrontFacing) ? -currentOrientation : currentOrientation; - return (sensorOrientationOffset + sensorOrientation + 360) % 360; - } -} diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java deleted file mode 100644 index a7bb3b7d4914..000000000000 --- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java +++ /dev/null @@ -1,120 +0,0 @@ -package io.flutter.plugins.camera; - -import android.app.Activity; -import android.content.Context; -import android.graphics.ImageFormat; -import android.hardware.camera2.CameraAccessException; -import android.hardware.camera2.CameraCharacteristics; -import android.hardware.camera2.CameraManager; -import android.hardware.camera2.CameraMetadata; -import android.hardware.camera2.params.StreamConfigurationMap; -import android.media.CamcorderProfile; -import android.util.Size; -import io.flutter.plugins.camera.Camera.ResolutionPreset; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** Provides various utilities for camera. */ -public final class CameraUtils { - - private CameraUtils() {} - - static Size computeBestPreviewSize(String cameraName, ResolutionPreset preset) { - if (preset.ordinal() > ResolutionPreset.high.ordinal()) { - preset = ResolutionPreset.high; - } - - CamcorderProfile profile = - getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); - return new Size(profile.videoFrameWidth, profile.videoFrameHeight); - } - - static Size computeBestCaptureSize(StreamConfigurationMap streamConfigurationMap) { - // For still image captures, we use the largest available size. - return Collections.max( - Arrays.asList(streamConfigurationMap.getOutputSizes(ImageFormat.JPEG)), - new CompareSizesByArea()); - } - - public static List> getAvailableCameras(Activity activity) - throws CameraAccessException { - CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); - String[] cameraNames = cameraManager.getCameraIdList(); - List> cameras = new ArrayList<>(); - for (String cameraName : cameraNames) { - HashMap details = new HashMap<>(); - CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName); - details.put("name", cameraName); - int sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); - details.put("sensorOrientation", sensorOrientation); - - int lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING); - switch (lensFacing) { - case CameraMetadata.LENS_FACING_FRONT: - details.put("lensFacing", "front"); - break; - case CameraMetadata.LENS_FACING_BACK: - details.put("lensFacing", "back"); - break; - case CameraMetadata.LENS_FACING_EXTERNAL: - details.put("lensFacing", "external"); - break; - } - cameras.add(details); - } - return cameras; - } - - static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( - String cameraName, ResolutionPreset preset) { - int cameraId = Integer.parseInt(cameraName); - 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 static class CompareSizesByArea implements Comparator { - @Override - public int compare(Size lhs, Size rhs) { - // We cast here to ensure the multiplications won't overflow. - return Long.signum( - (long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight()); - } - } -} diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java deleted file mode 100644 index fe385bef7818..000000000000 --- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java +++ /dev/null @@ -1,51 +0,0 @@ -package io.flutter.plugins.camera; - -import android.text.TextUtils; -import androidx.annotation.Nullable; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.EventChannel; -import java.util.HashMap; -import java.util.Map; - -class DartMessenger { - @Nullable private EventChannel.EventSink eventSink; - - enum EventType { - ERROR, - CAMERA_CLOSING, - } - - DartMessenger(BinaryMessenger messenger, long eventChannelId) { - new EventChannel(messenger, "flutter.io/cameraPlugin/cameraEvents" + eventChannelId) - .setStreamHandler( - new EventChannel.StreamHandler() { - @Override - public void onListen(Object arguments, EventChannel.EventSink sink) { - eventSink = sink; - } - - @Override - public void onCancel(Object arguments) { - eventSink = null; - } - }); - } - - void sendCameraClosingEvent() { - send(EventType.CAMERA_CLOSING, null); - } - - void send(EventType eventType, @Nullable String description) { - if (eventSink == null) { - return; - } - - Map event = new HashMap<>(); - event.put("eventType", eventType.toString().toLowerCase()); - // Only errors have a description. - if (eventType == EventType.ERROR && !TextUtils.isEmpty(description)) { - event.put("errorDescription", description); - } - eventSink.success(event); - } -} diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java deleted file mode 100644 index 132075555f26..000000000000 --- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ /dev/null @@ -1,176 +0,0 @@ -package io.flutter.plugins.camera; - -import android.app.Activity; -import android.hardware.camera2.CameraAccessException; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import io.flutter.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.view.TextureRegistry; - -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"); - 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 "initialize": - { - 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 "takePicture": - { - camera.takePicture(call.argument("path"), result); - break; - } - case "prepareForVideoRecording": - { - // This optimization is not required for Android. - result.success(null); - break; - } - case "startVideoRecording": - { - camera.startVideoRecording(call.argument("filePath"), result); - break; - } - case "stopVideoRecording": - { - camera.stopVideoRecording(result); - break; - } - case "pauseVideoRecording": - { - camera.pauseVideoRecording(result); - break; - } - case "resumeVideoRecording": - { - camera.resumeVideoRecording(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 "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 resolutionPreset = call.argument("resolutionPreset"); - boolean enableAudio = call.argument("enableAudio"); - TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture = - textureRegistry.createSurfaceTexture(); - DartMessenger dartMessenger = new DartMessenger(messenger, flutterSurfaceTexture.id()); - camera = - new Camera( - activity, - flutterSurfaceTexture, - dartMessenger, - cameraName, - resolutionPreset, - enableAudio); - - camera.open(result); - } - - // 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/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java b/packages/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java deleted file mode 100644 index db89eb279f41..000000000000 --- a/packages/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java +++ /dev/null @@ -1,107 +0,0 @@ -package io.flutter.plugins.camera; - -import static junit.framework.TestCase.assertNull; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.StandardMethodCodec; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import org.junit.Before; -import org.junit.Test; - -public class DartMessengerTest { - /** A {@link BinaryMessenger} implementation that does nothing but save its messages. */ - private static class FakeBinaryMessenger implements BinaryMessenger { - private BinaryMessageHandler handler; - private final List sentMessages = new ArrayList<>(); - - @Override - public void send(String channel, ByteBuffer message) { - sentMessages.add(message); - } - - @Override - public void send(String channel, ByteBuffer message, BinaryReply callback) { - send(channel, message); - } - - @Override - public void setMessageHandler(String channel, BinaryMessageHandler handler) { - this.handler = handler; - } - - BinaryMessageHandler getMessageHandler() { - return handler; - } - - List getMessages() { - return new ArrayList<>(sentMessages); - } - } - - private DartMessenger dartMessenger; - private FakeBinaryMessenger fakeBinaryMessenger; - - @Before - public void setUp() { - fakeBinaryMessenger = new FakeBinaryMessenger(); - dartMessenger = new DartMessenger(fakeBinaryMessenger, 0); - } - - @Test - public void setsStreamHandler() { - assertNotNull(fakeBinaryMessenger.getMessageHandler()); - } - - @Test - public void send_handlesNullEventSinks() { - dartMessenger.send(DartMessenger.EventType.ERROR, "error description"); - - List sentMessages = fakeBinaryMessenger.getMessages(); - assertEquals(0, sentMessages.size()); - } - - @Test - public void send_includesErrorDescriptions() { - initializeEventSink(); - - dartMessenger.send(DartMessenger.EventType.ERROR, "error description"); - - List sentMessages = fakeBinaryMessenger.getMessages(); - assertEquals(1, sentMessages.size()); - Map event = decodeSentMessage(sentMessages.get(0)); - assertEquals(DartMessenger.EventType.ERROR.toString().toLowerCase(), event.get("eventType")); - assertEquals("error description", event.get("errorDescription")); - } - - @Test - public void sendCameraClosingEvent() { - initializeEventSink(); - - dartMessenger.sendCameraClosingEvent(); - - List sentMessages = fakeBinaryMessenger.getMessages(); - assertEquals(1, sentMessages.size()); - Map event = decodeSentMessage(sentMessages.get(0)); - assertEquals( - DartMessenger.EventType.CAMERA_CLOSING.toString().toLowerCase(), event.get("eventType")); - assertNull(event.get("errorDescription")); - } - - private Map decodeSentMessage(ByteBuffer sentMessage) { - sentMessage.position(0); - return (Map) StandardMethodCodec.INSTANCE.decodeEnvelope(sentMessage); - } - - private void initializeEventSink() { - MethodCall call = new MethodCall("listen", null); - ByteBuffer encodedCall = StandardMethodCodec.INSTANCE.encodeMethodCall(call); - encodedCall.position(0); - fakeBinaryMessenger.getMessageHandler().onMessage(encodedCall, reply -> {}); - } -} diff --git a/packages/camera/camera/AUTHORS b/packages/camera/camera/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/camera/camera/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/camera/camera/CHANGELOG.md b/packages/camera/camera/CHANGELOG.md new file mode 100644 index 000000000000..2cab8e123ae6 --- /dev/null +++ b/packages/camera/camera/CHANGELOG.md @@ -0,0 +1,480 @@ +## 0.8.1+3 + +* Do not change camera orientation when iOS device is flat. + +## 0.8.1+2 + +* Fix iOS crash when selecting an unsupported FocusMode. + +## 0.8.1+1 + +* Migrate maven repository from jcenter to mavenCentral. + +## 0.8.1 + +* Solved a rotation issue on iOS which caused the default preview to be displayed as landscape right instead of portrait. + +## 0.8.0 + +* Stable null safety release. +* Solved delay when using the zoom feature on iOS. +* Added a timeout to the pre-capture sequence on Android to prevent crashes when the camera cannot get a focus. +* Updates the example code listed in the [README.md](README.md), so it runs without errors when you simply copy/ paste it into a Flutter App. + +## 0.7.0+4 + +* Fix crash when taking picture with orientation lock + +## 0.7.0+3 + +* Clockwise rotation of focus point in android + +## 0.7.0+2 + +* Fix example reference in README. +* Revert compileSdkVersion back to 29 (from 30) as this is causing problems with add-to-app configurations. + +## 0.7.0+1 + +* Ensure communication from JAVA to Dart is done on the main UI thread. + +## 0.7.0 + +* BREAKING CHANGE: `CameraValue.aspectRatio` now returns `width / height` rather than `height / width`. [(commit)](https://github.com/flutter/plugins/commit/100c7470d4066b1d0f8f7e4ec6d7c943e736f970) + * Added support for capture orientation locking on Android and iOS. + * Fixed camera preview not rotating correctly on Android and iOS. + * Fixed camera preview sometimes appearing stretched on Android and iOS. + * Fixed videos & photos saving with the incorrect rotation on iOS. +* New Features: + * Adds auto focus support for Android and iOS implementations. [(commmit)](https://github.com/flutter/plugins/commit/71a831790220f898bf8120c8a23840ac6e742db5) + * Adds ImageFormat selection for ImageStream and Video(iOS only). [(commit)](https://github.com/flutter/plugins/commit/da1b4638b750a5ff832d7be86a42831c42c6d6c0) +* Bug Fixes: + * Fixes crash when taking a picture on iOS devices without flash. [(commit)](https://github.com/flutter/plugins/commit/831344490984b1feec007afc9c8595d80b6c13f4) + * Make sure the configured zoom scale is copied over to the final capture builder on Android. Fixes the issue where the preview is zoomed but the final picture is not. [(commit)](https://github.com/flutter/plugins/commit/5916f55664e1772a4c3f0c02c5c71fc11e491b76) + * Fixes crash with using inner camera on some Android devices. [(commit)](https://github.com/flutter/plugins/commit/980b674cb4020c1927917426211a87e275346d5e) + * Improved error feedback by differentiating between uninitialized and disposed camera controllers. [(commit)](https://github.com/flutter/plugins/commit/d0b7109f6b00a0eda03506fed2c74cc123ffc6f3) + * Fixes picture captures causing a crash on some Huawei devices. [(commit)](https://github.com/flutter/plugins/commit/6d18db83f00f4861ffe485aba2d1f8aa08845ce6) + +## 0.6.4+5 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 0.6.4+4 + +* Set camera auto focus enabled by default. + +## 0.6.4+3 + +* Detect if selected camera supports auto focus and act accordingly on Android. This solves a problem where front facing cameras are not capturing the picture because auto focus is not supported. + +## 0.6.4+2 + +* Set ImageStreamReader listener to null to prevent stale images when streaming images. + +## 0.6.4+1 + +* Added closeCaptureSession() to stopVideoRecording in Camera.java to fix an Android 6 crash. + +## 0.6.4 + +* Adds auto exposure support for Android and iOS implementations. + +## 0.6.3+4 + +* Revert previous dependency update: Changed dependency on camera_platform_interface to >=1.04 <1.1.0. + +## 0.6.3+3 + +* Updated dependency on camera_platform_interface to ^1.2.0. + +## 0.6.3+2 + +* Fixes crash on Android which occurs after video recording has stopped just before taking a picture. + +## 0.6.3+1 + +* Fixes flash & torch modes not working on some Android devices. + +## 0.6.3 + +* Adds torch mode as a flash mode for Android and iOS implementations. + +## 0.6.2+1 + +* Fix the API documentation for the `CameraController.takePicture` method. + +## 0.6.2 + +* Add zoom support for Android and iOS implementations. + +## 0.6.1+1 + +* Added implementation of the `didFinishProcessingPhoto` on iOS which allows saving image metadata (EXIF) on iOS 11 and up. + +## 0.6.1 + +* Add flash support for Android and iOS implementations. + +## 0.6.0+2 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 0.6.0+1 + +Updated README to inform users that iOS 10.0+ is needed for use + +## 0.6.0 + +As part of implementing federated architecture and making the interface compatible with the web this version contains the following **breaking changes**: + +Method changes in `CameraController`: +- The `takePicture` method no longer accepts the `path` parameter, but instead returns the captured image as an instance of the `XFile` class; +- The `startVideoRecording` method no longer accepts the `filePath`. Instead the recorded video is now returned as a `XFile` instance when the `stopVideoRecording` method completes; +- The `stopVideoRecording` method now returns the captured video when it completes; +- Added the `buildPreview` method which is now used to implement the CameraPreview widget. + +## 0.5.8+19 + +* Update Flutter SDK constraint. + +## 0.5.8+18 + +* Suppress unchecked warning in Android tests which prevented the tests to compile. + +## 0.5.8+17 + +* Added Android 30 support. + +## 0.5.8+16 + +* Moved package to camera/camera subdir, to allow for federated implementations. + +## 0.5.8+15 + +* Added the `debugCheckIsDisposed` method which can be used in debug mode to validate if the `CameraController` class has been disposed. + +## 0.5.8+14 + +* Changed the order of the setters for `mediaRecorder` in `MediaRecorderBuilder.java` to make it more readable. + +## 0.5.8+13 + +* Added Dartdocs for all public APIs. + +## 0.5.8+12 + +* Added information of video not working correctly on Android emulators to `README.md`. + +## 0.5.8+11 + +* Fix rare nullptr exception on Android. +* Updated README.md with information about handling App lifecycle changes. + +## 0.5.8+10 + +* Suppress the `deprecated_member_use` warning in the example app for `ScaffoldMessenger.showSnackBar`. + +## 0.5.8+9 + +* Update android compileSdkVersion to 29. + +## 0.5.8+8 + +* Fixed garbled audio (in video) by setting audio encoding bitrate. + +## 0.5.8+7 + +* Keep handling deprecated Android v1 classes for backward compatibility. + +## 0.5.8+6 + +* Avoiding uses or overrides a deprecated API in CameraPlugin.java. + +## 0.5.8+5 + +* Fix compilation/availability issues on iOS. + +## 0.5.8+4 + +* Fixed bug caused by casting a `CameraAccessException` on Android. + +## 0.5.8+3 + +* Fix bug in usage example in README.md + +## 0.5.8+2 + +* Post-v2 embedding cleanups. + +## 0.5.8+1 + +* Update lower bound of dart dependency to 2.1.0. + +## 0.5.8 + +* Remove Android dependencies fallback. +* Require Flutter SDK 1.12.13+hotfix.5 or greater. + +## 0.5.7+5 + +* Replace deprecated `getFlutterEngine` call on Android. + +## 0.5.7+4 + +* Add `pedantic` to dev_dependency. + +## 0.5.7+3 + +* Fix an Android crash when permissions are requested multiple times. + +## 0.5.7+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.5.7+1 + +* Fix example null exception. + +## 0.5.7 + +* Fix unawaited futures. + +## 0.5.6+4 + +* Android: Use CameraDevice.TEMPLATE_RECORD to improve image streaming. + +## 0.5.6+3 + +* Remove AndroidX warning. + +## 0.5.6+2 + +* Include lifecycle dependency as a compileOnly one on Android to resolve + potential version conflicts with other transitive libraries. + +## 0.5.6+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.5.6 + +* Add support for the v2 Android embedding. This shouldn't affect existing + functionality. + +## 0.5.5+1 + +* Fix event type check + +## 0.5.5 + +* Define clang modules for iOS. + +## 0.5.4+3 + +* Update and migrate iOS example project. + +## 0.5.4+2 + +* Fix Android NullPointerException on devices with only front-facing camera. + +## 0.5.4+1 + +* Fix Android pause and resume video crash when executing in APIs below 24. + +## 0.5.4 + +* Add feature to pause and resume video recording. + +## 0.5.3+1 + +* Fix too large request code for FragmentActivity users. + +## 0.5.3 + +* Added new quality presets. +* Now all quality presets can be used to control image capture quality. + +## 0.5.2+2 + +* Fix memory leak related to not unregistering stream handler in FlutterEventChannel when disposing camera. + +## 0.5.2+1 + +* Fix bug that prevented video recording with audio. + +## 0.5.2 + +* Added capability to disable audio for the `CameraController`. (e.g. `CameraController(_, _, + enableAudio: false);`) + +## 0.5.1 + +* Can now be compiled with earlier Android sdks below 21 when +`` has been added to the project +`AndroidManifest.xml`. For sdks below 21, the plugin won't be registered and calls to it will throw +a `MissingPluginException.` + +## 0.5.0 + +* **Breaking Change** This plugin no longer handles closing and opening the camera on Android + lifecycle changes. Please use `WidgetsBindingObserver` to control camera resources on lifecycle + changes. See example project for example using `WidgetsBindingObserver`. + +## 0.4.3+2 + +* Bump the minimum Flutter version to 1.2.0. +* Add template type parameter to `invokeMethod` calls. + +## 0.4.3+1 + +* Catch additional `Exception`s from Android and throw as `CameraException`s. + +## 0.4.3 + +* Add capability to prepare the capture session for video recording on iOS. + +## 0.4.2 + +* Add sensor orientation value to `CameraDescription`. + +## 0.4.1 + +* Camera methods are ran in a background thread on iOS. + +## 0.4.0+3 + +* Fixed a crash when the plugin is registered by a background FlutterView. + +## 0.4.0+2 + +* Fix orientation of captured photos when camera is used for the first time on Android. + +## 0.4.0+1 + +* Remove categories. + +## 0.4.0 + +* **Breaking Change** Change iOS image stream format to `ImageFormatGroup.bgra8888` from + `ImageFormatGroup.yuv420`. + +## 0.3.0+4 + +* Fixed bug causing black screen on some Android devices. + +## 0.3.0+3 + +* Log a more detailed warning at build time about the previous AndroidX + migration. + +## 0.3.0+2 + +* Fix issue with calculating iOS image orientation in certain edge cases. + +## 0.3.0+1 + +* Remove initial method call invocation from static camera method. + +## 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.9+1 + +* Fix a crash when failing to start preview. + +## 0.2.9 + +* Save photo orientation data on iOS. + +## 0.2.8 + +* Add access to the image stream from Dart. +* Use `cameraController.startImageStream(listener)` to process the images. + +## 0.2.7 + +* Fix issue with crash when the physical device's orientation is unknown. + +## 0.2.6 + +* Update the camera to use the physical device's orientation instead of the UI + orientation on Android. + +## 0.2.5 + +* Fix preview and video size with satisfying conditions of multiple outputs. + +## 0.2.4 + +* Unregister the activity lifecycle callbacks when disposing the camera. + +## 0.2.3 + +* Added path_provider and video_player as dev dependencies because the example uses them. +* Updated example path_provider version to get Dart 2 support. + +## 0.2.2 + +* iOS image capture is done in high quality (full camera size) + +## 0.2.1 + +* Updated Gradle tooling to match Android Studio 3.1.2. + +## 0.2.0 + +* Added support for video recording. +* Changed the example app to add video recording. + +A lot of **breaking changes** in this version: + +Getter changes: + - Removed `isStarted` + - Renamed `initialized` to `isInitialized` + - Added `isRecordingVideo` + +Method changes: + - Renamed `capture` to `takePicture` + - Removed `start` (the preview starts automatically when `initialize` is called) + - Added `startVideoRecording(String filePath)` + - Removed `stop` (the preview stops automatically when `dispose` is called) + - Added `stopVideoRecording` + +## 0.1.2 + +* Fix Dart 2 runtime errors. + +## 0.1.1 + +* Fix Dart 2 runtime error. + +## 0.1.0 + +* **Breaking change**. Set SDK constraints to match the Flutter beta release. + +## 0.0.4 + +* Revert regression of `CameraController.capture()` introduced in v. 0.0.3. + +## 0.0.3 + +* Improved resource cleanup on Android. Avoids crash on Activity restart. +* Made the Future returned by `CameraController.dispose()` and `CameraController.capture()` actually complete on + Android. + +## 0.0.2 + +* Simplified and upgraded Android project template to Android SDK 27. +* Moved Android package to io.flutter.plugins. +* Fixed warnings from the Dart 2.0 analyzer. + +## 0.0.1 + +* Initial release diff --git a/packages/camera/camera/LICENSE b/packages/camera/camera/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/camera/camera/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/camera/camera/README.md b/packages/camera/camera/README.md new file mode 100644 index 000000000000..fb6144face9b --- /dev/null +++ b/packages/camera/camera/README.md @@ -0,0 +1,130 @@ +# Camera Plugin + +[![pub package](https://img.shields.io/pub/v/camera.svg)](https://pub.dev/packages/camera) + +A Flutter plugin for iOS and Android allowing access to the device cameras. + +*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) + +## Features: + +* Display live camera preview in a widget. +* Snapshots can be captured and saved to a file. +* Record video. +* Add access to the image stream from Dart. + +## Installation + +First, add `camera` as a [dependency in your pubspec.yaml file](https://flutter.dev/using-packages/). + +### iOS + +iOS 10.0 of higher is needed to use the camera plugin. If compiling for any version lower than 10.0 make sure to check the iOS version before using the camera plugin. For example, using the [device_info](https://pub.dev/packages/device_info) plugin. + +Add two rows to the `ios/Runner/Info.plist`: + +* one with the key `Privacy - Camera Usage Description` and a usage description. +* and one with the key `Privacy - Microphone Usage Description` and a usage description. + +Or in text format add the key: + +```xml +NSCameraUsageDescription +Can I use the camera please? +NSMicrophoneUsageDescription +Can I use the mic please? +``` + +### Android + +Change the minimum Android sdk version to 21 (or higher) in your `android/app/build.gradle` file. + +``` +minSdkVersion 21 +``` + +It's important to note that the `MediaRecorder` class is not working properly on emulators, as stated in the documentation: https://developer.android.com/reference/android/media/MediaRecorder. Specifically, when recording a video with sound enabled and trying to play it back, the duration won't be correct and you will only see the first frame. + +### 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: + +```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); + } + } + } +``` + +### 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'; + +List cameras; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + + cameras = await availableCameras(); + runApp(CameraApp()); +} + +class CameraApp extends StatefulWidget { + @override + _CameraAppState createState() => _CameraAppState(); +} + +class _CameraAppState extends State { + CameraController controller; + + @override + void initState() { + super.initState(); + controller = CameraController(cameras[0], ResolutionPreset.max); + controller.initialize().then((_) { + if (!mounted) { + return; + } + setState(() {}); + }); + } + + @override + void dispose() { + controller?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (!controller.value.isInitialized) { + return Container(); + } + return MaterialApp( + home: CameraPreview(controller), + ); + } +} + +``` + +For a more elaborate usage example see [here](https://github.com/flutter/plugins/tree/master/packages/camera/camera/example). + +*Note*: This plugin is still under development, and some APIs might not be available yet. +[Feedback welcome](https://github.com/flutter/flutter/issues) and +[Pull Requests](https://github.com/flutter/plugins/pulls) are most welcome! diff --git a/packages/camera/camera/android/build.gradle b/packages/camera/camera/android/build.gradle new file mode 100644 index 000000000000..0907c1eeecc9 --- /dev/null +++ b/packages/camera/camera/android/build.gradle @@ -0,0 +1,55 @@ +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' + } + compileOptions { + sourceCompatibility = '1.8' + targetCompatibility = '1.8' + } + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + } +} + +dependencies { + compileOnly 'androidx.annotation:annotation:1.1.0' + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-inline:3.5.13' + testImplementation 'androidx.test:core:1.3.0' + testImplementation 'org.robolectric:robolectric:4.3' +} diff --git a/packages/camera/android/gradle.properties b/packages/camera/camera/android/gradle.properties similarity index 100% rename from packages/camera/android/gradle.properties rename to packages/camera/camera/android/gradle.properties diff --git a/packages/camera/android/settings.gradle b/packages/camera/camera/android/settings.gradle similarity index 100% rename from packages/camera/android/settings.gradle rename to packages/camera/camera/android/settings.gradle diff --git a/packages/camera/android/src/main/AndroidManifest.xml b/packages/camera/camera/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/camera/android/src/main/AndroidManifest.xml rename to packages/camera/camera/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/src/main/java/io/flutter/plugins/camera/Camera.java new file mode 100644 index 000000000000..4c1370f2f3cb --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -0,0 +1,1203 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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 io.flutter.plugins.camera.CameraUtils.computeBestPreviewSize; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.graphics.ImageFormat; +import android.graphics.Rect; +import android.graphics.SurfaceTexture; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCaptureSession.CaptureCallback; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraDevice; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.CaptureFailure; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.TotalCaptureResult; +import android.hardware.camera2.params.MeteringRectangle; +import android.hardware.camera2.params.OutputConfiguration; +import android.hardware.camera2.params.SessionConfiguration; +import android.media.CamcorderProfile; +import android.media.Image; +import android.media.ImageReader; +import android.media.MediaRecorder; +import android.os.Build; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.os.Handler; +import android.os.Looper; +import android.os.SystemClock; +import android.util.Log; +import android.util.Range; +import android.util.Rational; +import android.util.Size; +import android.view.Surface; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugins.camera.PictureCaptureRequest.State; +import io.flutter.plugins.camera.media.MediaRecorderBuilder; +import io.flutter.plugins.camera.types.ExposureMode; +import io.flutter.plugins.camera.types.FlashMode; +import io.flutter.plugins.camera.types.FocusMode; +import io.flutter.plugins.camera.types.ResolutionPreset; +import io.flutter.view.TextureRegistry.SurfaceTextureEntry; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.Executors; + +@FunctionalInterface +interface ErrorCallback { + void onError(String errorCode, String errorMessage); +} + +public class Camera { + private static final String TAG = "Camera"; + + /** Timeout for the pre-capture sequence. */ + private static final long PRECAPTURE_TIMEOUT_MS = 1000; + + private final SurfaceTextureEntry flutterTexture; + private final CameraManager cameraManager; + private final DeviceOrientationManager deviceOrientationListener; + private final boolean isFrontFacing; + private final int sensorOrientation; + private final String cameraName; + private final Size captureSize; + private final Size previewSize; + private final boolean enableAudio; + private final Context applicationContext; + private final CamcorderProfile recordingProfile; + private final DartMessenger dartMessenger; + private final CameraZoom cameraZoom; + private final CameraCharacteristics cameraCharacteristics; + + private CameraDevice cameraDevice; + private CameraCaptureSession cameraCaptureSession; + private ImageReader pictureImageReader; + private ImageReader imageStreamReader; + private CaptureRequest.Builder captureRequestBuilder; + private MediaRecorder mediaRecorder; + private boolean recordingVideo; + private File videoRecordingFile; + private FlashMode flashMode; + private ExposureMode exposureMode; + private FocusMode focusMode; + private PictureCaptureRequest pictureCaptureRequest; + private CameraRegions cameraRegions; + private int exposureOffset; + private boolean useAutoFocus = true; + private Range fpsRange; + private PlatformChannel.DeviceOrientation lockedCaptureOrientation; + private long preCaptureStartTime; + + private static final HashMap supportedImageFormats; + // Current supported outputs + static { + supportedImageFormats = new HashMap<>(); + supportedImageFormats.put("yuv420", 35); + supportedImageFormats.put("jpeg", 256); + } + + public Camera( + final Activity activity, + final SurfaceTextureEntry flutterTexture, + final DartMessenger dartMessenger, + final String cameraName, + final String resolutionPreset, + final boolean enableAudio) + throws CameraAccessException { + if (activity == null) { + throw new IllegalStateException("No activity available!"); + } + this.cameraName = cameraName; + this.enableAudio = enableAudio; + this.flutterTexture = flutterTexture; + this.dartMessenger = dartMessenger; + this.cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); + this.applicationContext = activity.getApplicationContext(); + this.flashMode = FlashMode.auto; + this.exposureMode = ExposureMode.auto; + this.focusMode = FocusMode.auto; + this.exposureOffset = 0; + + cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); + initFps(cameraCharacteristics); + sensorOrientation = cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); + isFrontFacing = + cameraCharacteristics.get(CameraCharacteristics.LENS_FACING) + == CameraMetadata.LENS_FACING_FRONT; + ResolutionPreset preset = ResolutionPreset.valueOf(resolutionPreset); + recordingProfile = + CameraUtils.getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); + captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); + previewSize = computeBestPreviewSize(cameraName, preset); + cameraZoom = + new CameraZoom( + cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE), + cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)); + + deviceOrientationListener = + new DeviceOrientationManager(activity, dartMessenger, isFrontFacing, sensorOrientation); + deviceOrientationListener.start(); + } + + private void initFps(CameraCharacteristics cameraCharacteristics) { + try { + Range[] ranges = + cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + if (ranges != null) { + for (Range range : ranges) { + int upper = range.getUpper(); + Log.i("Camera", "[FPS Range Available] is:" + range); + if (upper >= 10) { + if (fpsRange == null || upper > fpsRange.getUpper()) { + fpsRange = range; + } + } + } + } + } catch (Exception e) { + e.printStackTrace(); + } + Log.i("Camera", "[FPS Range] is:" + fpsRange); + } + + private void prepareMediaRecorder(String outputFilePath) throws IOException { + if (mediaRecorder != null) { + mediaRecorder.release(); + } + + mediaRecorder = + new MediaRecorderBuilder(recordingProfile, outputFilePath) + .setEnableAudio(enableAudio) + .setMediaOrientation( + lockedCaptureOrientation == null + ? deviceOrientationListener.getMediaOrientation() + : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)) + .build(); + } + + @SuppressLint("MissingPermission") + public void open(String imageFormatGroup) throws CameraAccessException { + pictureImageReader = + ImageReader.newInstance( + captureSize.getWidth(), captureSize.getHeight(), ImageFormat.JPEG, 2); + + Integer imageFormat = supportedImageFormats.get(imageFormatGroup); + if (imageFormat == null) { + Log.w(TAG, "The selected imageFormatGroup is not supported by Android. Defaulting to yuv420"); + imageFormat = ImageFormat.YUV_420_888; + } + + // Used to steam image byte data to dart side. + imageStreamReader = + ImageReader.newInstance(previewSize.getWidth(), previewSize.getHeight(), imageFormat, 2); + + cameraManager.openCamera( + cameraName, + new CameraDevice.StateCallback() { + @Override + public void onOpened(@NonNull CameraDevice device) { + cameraDevice = device; + try { + startPreview(); + dartMessenger.sendCameraInitializedEvent( + previewSize.getWidth(), + previewSize.getHeight(), + exposureMode, + focusMode, + isExposurePointSupported(), + isFocusPointSupported()); + } catch (CameraAccessException e) { + dartMessenger.sendCameraErrorEvent(e.getMessage()); + close(); + } + } + + @Override + public void onClosed(@NonNull CameraDevice camera) { + dartMessenger.sendCameraClosingEvent(); + super.onClosed(camera); + } + + @Override + public void onDisconnected(@NonNull CameraDevice cameraDevice) { + close(); + dartMessenger.sendCameraErrorEvent("The camera was disconnected."); + } + + @Override + public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { + close(); + String errorDescription; + switch (errorCode) { + case ERROR_CAMERA_IN_USE: + errorDescription = "The camera device is in use already."; + break; + case ERROR_MAX_CAMERAS_IN_USE: + errorDescription = "Max cameras in use"; + break; + case ERROR_CAMERA_DISABLED: + errorDescription = "The camera device could not be opened due to a device policy."; + break; + case ERROR_CAMERA_DEVICE: + errorDescription = "The camera device has encountered a fatal error"; + break; + case ERROR_CAMERA_SERVICE: + errorDescription = "The camera service has encountered a fatal error."; + break; + default: + errorDescription = "Unknown camera error"; + } + dartMessenger.sendCameraErrorEvent(errorDescription); + } + }, + null); + } + + private void createCaptureSession(int templateType, Surface... surfaces) + throws CameraAccessException { + createCaptureSession(templateType, null, surfaces); + } + + private void createCaptureSession( + int templateType, Runnable onSuccessCallback, Surface... surfaces) + throws CameraAccessException { + // Close any existing capture session. + closeCaptureSession(); + + // Create a new capture builder. + captureRequestBuilder = cameraDevice.createCaptureRequest(templateType); + + // Build Flutter surface to render to + SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); + surfaceTexture.setDefaultBufferSize(previewSize.getWidth(), previewSize.getHeight()); + Surface flutterSurface = new Surface(surfaceTexture); + captureRequestBuilder.addTarget(flutterSurface); + + List remainingSurfaces = Arrays.asList(surfaces); + if (templateType != CameraDevice.TEMPLATE_PREVIEW) { + // If it is not preview mode, add all surfaces as targets. + for (Surface surface : remainingSurfaces) { + captureRequestBuilder.addTarget(surface); + } + } + + cameraRegions = new CameraRegions(getRegionBoundaries()); + + // Prepare the callback + CameraCaptureSession.StateCallback callback = + new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(@NonNull CameraCaptureSession session) { + if (cameraDevice == null) { + dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); + return; + } + cameraCaptureSession = session; + + updateFpsRange(); + updateFocus(focusMode); + updateFlash(flashMode); + updateExposure(exposureMode); + + refreshPreviewCaptureSession( + onSuccessCallback, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); + } + + @Override + public void onConfigureFailed(@NonNull CameraCaptureSession cameraCaptureSession) { + dartMessenger.sendCameraErrorEvent("Failed to configure camera session."); + } + }; + + // Start the session + if (VERSION.SDK_INT >= VERSION_CODES.P) { + // Collect all surfaces we want to render to. + List configs = new ArrayList<>(); + configs.add(new OutputConfiguration(flutterSurface)); + for (Surface surface : remainingSurfaces) { + configs.add(new OutputConfiguration(surface)); + } + createCaptureSessionWithSessionConfig(configs, callback); + } else { + // Collect all surfaces we want to render to. + List surfaceList = new ArrayList<>(); + surfaceList.add(flutterSurface); + surfaceList.addAll(remainingSurfaces); + createCaptureSession(surfaceList, callback); + } + } + + @TargetApi(VERSION_CODES.P) + private void createCaptureSessionWithSessionConfig( + List outputConfigs, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { + cameraDevice.createCaptureSession( + new SessionConfiguration( + SessionConfiguration.SESSION_REGULAR, + outputConfigs, + Executors.newSingleThreadExecutor(), + callback)); + } + + @TargetApi(VERSION_CODES.LOLLIPOP) + @SuppressWarnings("deprecation") + private void createCaptureSession( + List surfaces, CameraCaptureSession.StateCallback callback) + throws CameraAccessException { + cameraDevice.createCaptureSession(surfaces, callback, null); + } + + private void refreshPreviewCaptureSession( + @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { + if (cameraCaptureSession == null) { + return; + } + + try { + cameraCaptureSession.setRepeatingRequest( + captureRequestBuilder.build(), + pictureCaptureCallback, + new Handler(Looper.getMainLooper())); + + if (onSuccessCallback != null) { + onSuccessCallback.run(); + } + } catch (CameraAccessException | IllegalStateException | IllegalArgumentException e) { + onErrorCallback.onError("cameraAccess", e.getMessage()); + } + } + + private void writeToFile(ByteBuffer buffer, File file) throws IOException { + try (FileOutputStream outputStream = new FileOutputStream(file)) { + while (0 < buffer.remaining()) { + outputStream.getChannel().write(buffer); + } + } + } + + public void takePicture(@NonNull final Result result) { + // Only take 1 picture at a time + if (pictureCaptureRequest != null && !pictureCaptureRequest.isFinished()) { + result.error("captureAlreadyActive", "Picture is currently already being captured", null); + return; + } + // Store the result + this.pictureCaptureRequest = new PictureCaptureRequest(result); + + // Create temporary file + final File outputDir = applicationContext.getCacheDir(); + final File file; + try { + file = File.createTempFile("CAP", ".jpg", outputDir); + } catch (IOException | SecurityException e) { + pictureCaptureRequest.error("cannotCreateFile", e.getMessage(), null); + return; + } + + // Listen for picture being taken + pictureImageReader.setOnImageAvailableListener( + reader -> { + try (Image image = reader.acquireLatestImage()) { + ByteBuffer buffer = image.getPlanes()[0].getBuffer(); + writeToFile(buffer, file); + pictureCaptureRequest.finish(file.getAbsolutePath()); + } catch (IOException e) { + pictureCaptureRequest.error("IOError", "Failed saving image", null); + } + }, + null); + + if (useAutoFocus) { + runPictureAutoFocus(); + } else { + runPicturePreCapture(); + } + } + + private final CameraCaptureSession.CaptureCallback pictureCaptureCallback = + new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + processCapture(result); + } + + @Override + public void onCaptureProgressed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureResult partialResult) { + processCapture(partialResult); + } + + @Override + public void onCaptureFailed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureFailure failure) { + if (pictureCaptureRequest == null || pictureCaptureRequest.isFinished()) { + return; + } + String reason; + boolean fatalFailure = false; + switch (failure.getReason()) { + case CaptureFailure.REASON_ERROR: + reason = "An error happened in the framework"; + break; + case CaptureFailure.REASON_FLUSHED: + reason = "The capture has failed due to an abortCaptures() call"; + fatalFailure = true; + break; + default: + reason = "Unknown reason"; + } + Log.w("Camera", "pictureCaptureCallback.onCaptureFailed(): " + reason); + if (fatalFailure) pictureCaptureRequest.error("captureFailure", reason, null); + } + + private void processCapture(CaptureResult result) { + if (pictureCaptureRequest == null) { + return; + } + + Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); + Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); + switch (pictureCaptureRequest.getState()) { + case focusing: + if (afState == null) { + return; + } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED + || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + // Some devices might return null here, in which case we will also continue. + if (aeState == null || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) { + runPictureCapture(); + } else { + runPicturePreCapture(); + } + } + break; + case preCapture: + // Some devices might return null here, in which case we will also continue. + if (aeState == null + || aeState == CaptureRequest.CONTROL_AE_STATE_PRECAPTURE + || aeState == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED + || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { + pictureCaptureRequest.setState(State.waitingPreCaptureReady); + setPreCaptureStartTime(); + } + break; + case waitingPreCaptureReady: + if (aeState == null || aeState != CaptureRequest.CONTROL_AE_STATE_PRECAPTURE) { + runPictureCapture(); + } else { + if (hitPreCaptureTimeout()) { + unlockAutoFocus(); + } + } + } + } + }; + + private void runPictureAutoFocus() { + assert (pictureCaptureRequest != null); + + pictureCaptureRequest.setState(PictureCaptureRequest.State.focusing); + lockAutoFocus(pictureCaptureCallback); + } + + private void runPicturePreCapture() { + assert (pictureCaptureRequest != null); + pictureCaptureRequest.setState(PictureCaptureRequest.State.preCapture); + + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + + refreshPreviewCaptureSession( + () -> + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE), + (code, message) -> pictureCaptureRequest.error(code, message, null)); + } + + private void runPictureCapture() { + assert (pictureCaptureRequest != null); + pictureCaptureRequest.setState(PictureCaptureRequest.State.capturing); + try { + final CaptureRequest.Builder captureBuilder = + cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + captureBuilder.addTarget(pictureImageReader.getSurface()); + captureBuilder.set( + CaptureRequest.SCALER_CROP_REGION, + captureRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); + captureBuilder.set( + CaptureRequest.JPEG_ORIENTATION, + lockedCaptureOrientation == null + ? deviceOrientationListener.getMediaOrientation() + : deviceOrientationListener.getMediaOrientation(lockedCaptureOrientation)); + + switch (flashMode) { + case off: + captureBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + captureBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + case auto: + captureBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + break; + case always: + default: + captureBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + break; + } + cameraCaptureSession.stopRepeating(); + cameraCaptureSession.capture( + captureBuilder.build(), + new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + unlockAutoFocus(); + } + }, + null); + } catch (CameraAccessException e) { + pictureCaptureRequest.error("cameraAccess", e.getMessage(), null); + } + } + + private void lockAutoFocus(CaptureCallback callback) { + captureRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + + refreshPreviewCaptureSession( + null, (code, message) -> pictureCaptureRequest.error(code, message, null)); + } + + private void unlockAutoFocus() { + captureRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + updateFocus(focusMode); + try { + cameraCaptureSession.capture(captureRequestBuilder.build(), null, null); + } catch (CameraAccessException ignored) { + } + captureRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_IDLE); + + refreshPreviewCaptureSession( + null, + (errorCode, errorMessage) -> pictureCaptureRequest.error(errorCode, errorMessage, null)); + } + + public void startVideoRecording(Result result) { + final File outputDir = applicationContext.getCacheDir(); + try { + videoRecordingFile = File.createTempFile("REC", ".mp4", outputDir); + } catch (IOException | SecurityException e) { + result.error("cannotCreateFile", e.getMessage(), null); + return; + } + + try { + prepareMediaRecorder(videoRecordingFile.getAbsolutePath()); + recordingVideo = true; + createCaptureSession( + CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); + result.success(null); + } catch (CameraAccessException | IOException e) { + recordingVideo = false; + videoRecordingFile = null; + result.error("videoRecordingFailed", e.getMessage(), null); + } + } + + public void stopVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; + } + + try { + recordingVideo = false; + + try { + cameraCaptureSession.abortCaptures(); + mediaRecorder.stop(); + } catch (CameraAccessException | IllegalStateException e) { + // Ignore exceptions and try to continue (changes are camera session already aborted capture) + } + + mediaRecorder.reset(); + startPreview(); + result.success(videoRecordingFile.getAbsolutePath()); + videoRecordingFile = null; + } catch (CameraAccessException | IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + } + } + + public void pauseVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; + } + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mediaRecorder.pause(); + } else { + result.error("videoRecordingFailed", "pauseVideoRecording requires Android API +24.", null); + return; + } + } catch (IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + return; + } + + result.success(null); + } + + public void resumeVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; + } + + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + mediaRecorder.resume(); + } else { + result.error( + "videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); + return; + } + } catch (IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + return; + } + + result.success(null); + } + + public void setFlashMode(@NonNull final Result result, FlashMode mode) + throws CameraAccessException { + // Get the flash availability + Boolean flashAvailable = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + + // Check if flash is available. + if (flashAvailable == null || !flashAvailable) { + result.error("setFlashModeFailed", "Device does not have flash capabilities", null); + return; + } + + // If switching directly from torch to auto or on, make sure we turn off the torch. + if (flashMode == FlashMode.torch && mode != FlashMode.torch && mode != FlashMode.off) { + updateFlash(FlashMode.off); + + this.cameraCaptureSession.setRepeatingRequest( + captureRequestBuilder.build(), + new CaptureCallback() { + private boolean isFinished = false; + + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult captureResult) { + if (isFinished) { + return; + } + + updateFlash(mode); + refreshPreviewCaptureSession( + () -> { + result.success(null); + isFinished = true; + }, + (code, message) -> + result.error("setFlashModeFailed", "Could not set flash mode.", null)); + } + + @Override + public void onCaptureFailed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureFailure failure) { + if (isFinished) { + return; + } + + result.error("setFlashModeFailed", "Could not set flash mode.", null); + isFinished = true; + } + }, + null); + } else { + updateFlash(mode); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); + } + } + + public void setExposureMode(@NonNull final Result result, ExposureMode mode) + throws CameraAccessException { + updateExposure(mode); + cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + result.success(null); + } + + public void setExposurePoint(@NonNull final Result result, Double x, Double y) + throws CameraAccessException { + // Check if exposure point functionality is available. + if (!isExposurePointSupported()) { + result.error( + "setExposurePointFailed", "Device does not have exposure point capabilities", null); + return; + } + // Check if the current region boundaries are known + if (cameraRegions.getMaxBoundaries() == null) { + result.error("setExposurePointFailed", "Could not determine max region boundaries", null); + return; + } + // Set the metering rectangle + if (x == null || y == null) cameraRegions.resetAutoExposureMeteringRectangle(); + else cameraRegions.setAutoExposureMeteringRectangleFromPoint(y, 1 - x); + // Apply it + updateExposure(exposureMode); + refreshPreviewCaptureSession( + () -> result.success(null), (code, message) -> result.error("CameraAccess", message, null)); + } + + public void setFocusMode(@NonNull final Result result, FocusMode mode) + throws CameraAccessException { + this.focusMode = mode; + + updateFocus(mode); + + switch (mode) { + case auto: + refreshPreviewCaptureSession( + null, (code, message) -> result.error("setFocusMode", message, null)); + break; + case locked: + lockAutoFocus( + new CaptureCallback() { + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + unlockAutoFocus(); + } + }); + break; + } + result.success(null); + } + + public void setFocusPoint(@NonNull final Result result, Double x, Double y) + throws CameraAccessException { + // Check if focus point functionality is available. + if (!isFocusPointSupported()) { + result.error("setFocusPointFailed", "Device does not have focus point capabilities", null); + return; + } + + // Check if the current region boundaries are known + if (cameraRegions.getMaxBoundaries() == null) { + result.error("setFocusPointFailed", "Could not determine max region boundaries", null); + return; + } + + // Set the metering rectangle + if (x == null || y == null) { + cameraRegions.resetAutoFocusMeteringRectangle(); + } else { + cameraRegions.setAutoFocusMeteringRectangleFromPoint(y, 1 - x); + } + + // Apply the new metering rectangle + setFocusMode(result, focusMode); + } + + @TargetApi(VERSION_CODES.P) + private boolean supportsDistortionCorrection() throws CameraAccessException { + int[] availableDistortionCorrectionModes = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); + if (availableDistortionCorrectionModes == null) availableDistortionCorrectionModes = new int[0]; + long nonOffModesSupported = + Arrays.stream(availableDistortionCorrectionModes) + .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) + .count(); + return nonOffModesSupported > 0; + } + + private Size getRegionBoundaries() throws CameraAccessException { + // No distortion correction support + if (android.os.Build.VERSION.SDK_INT < VERSION_CODES.P || !supportsDistortionCorrection()) { + return cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); + } + // Get the current distortion correction mode + Integer distortionCorrectionMode = + captureRequestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); + // Return the correct boundaries depending on the mode + android.graphics.Rect rect; + if (distortionCorrectionMode == null + || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { + rect = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); + } else { + rect = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + } + return rect == null ? null : new Size(rect.width(), rect.height()); + } + + private boolean isExposurePointSupported() throws CameraAccessException { + Integer supportedRegions = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); + return supportedRegions != null && supportedRegions > 0; + } + + private boolean isFocusPointSupported() throws CameraAccessException { + Integer supportedRegions = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); + return supportedRegions != null && supportedRegions > 0; + } + + public double getMinExposureOffset() throws CameraAccessException { + Range range = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + double minStepped = range == null ? 0 : range.getLower(); + double stepSize = getExposureOffsetStepSize(); + return minStepped * stepSize; + } + + public double getMaxExposureOffset() throws CameraAccessException { + Range range = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + double maxStepped = range == null ? 0 : range.getUpper(); + double stepSize = getExposureOffsetStepSize(); + return maxStepped * stepSize; + } + + public double getExposureOffsetStepSize() throws CameraAccessException { + Rational stepSize = + cameraManager + .getCameraCharacteristics(cameraDevice.getId()) + .get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + return stepSize == null ? 0.0 : stepSize.doubleValue(); + } + + public void setExposureOffset(@NonNull final Result result, double offset) + throws CameraAccessException { + // Set the exposure offset + double stepSize = getExposureOffsetStepSize(); + exposureOffset = (int) (offset / stepSize); + // Apply it + updateExposure(exposureMode); + this.cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + result.success(offset); + } + + public float getMaxZoomLevel() { + return cameraZoom.maxZoom; + } + + public float getMinZoomLevel() { + return CameraZoom.DEFAULT_ZOOM_FACTOR; + } + + public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { + float maxZoom = cameraZoom.maxZoom; + float minZoom = CameraZoom.DEFAULT_ZOOM_FACTOR; + + if (zoom > maxZoom || zoom < minZoom) { + String errorMessage = + String.format( + Locale.ENGLISH, + "Zoom level out of bounds (zoom level should be between %f and %f).", + minZoom, + maxZoom); + result.error("ZOOM_ERROR", errorMessage, null); + return; + } + + //Zoom area is calculated relative to sensor area (activeRect) + if (captureRequestBuilder != null) { + final Rect computedZoom = cameraZoom.computeZoom(zoom); + captureRequestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); + cameraCaptureSession.setRepeatingRequest(captureRequestBuilder.build(), null, null); + } + + result.success(null); + } + + public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { + this.lockedCaptureOrientation = orientation; + } + + public void unlockCaptureOrientation() { + this.lockedCaptureOrientation = null; + } + + private void updateFpsRange() { + if (fpsRange == null) { + return; + } + + captureRequestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, fpsRange); + } + + private void updateFocus(FocusMode mode) { + if (useAutoFocus) { + int[] modes = cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); + // Auto focus is not supported + if (modes == null + || modes.length == 0 + || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)) { + useAutoFocus = false; + captureRequestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); + } else { + // Applying auto focus + switch (mode) { + case locked: + captureRequestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); + break; + case auto: + captureRequestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, + recordingVideo + ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO + : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + default: + break; + } + MeteringRectangle afRect = cameraRegions.getAFMeteringRectangle(); + captureRequestBuilder.set( + CaptureRequest.CONTROL_AF_REGIONS, + afRect == null ? null : new MeteringRectangle[] {afRect}); + } + } else { + captureRequestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_OFF); + } + } + + private void updateExposure(ExposureMode mode) { + exposureMode = mode; + + // Applying auto exposure + MeteringRectangle aeRect = cameraRegions.getAEMeteringRectangle(); + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_REGIONS, + aeRect == null ? null : new MeteringRectangle[] {cameraRegions.getAEMeteringRectangle()}); + + switch (mode) { + case locked: + captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, true); + break; + case auto: + default: + captureRequestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, false); + break; + } + + captureRequestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, exposureOffset); + } + + private void updateFlash(FlashMode mode) { + // Get flash + flashMode = mode; + + // Applying flash modes + switch (flashMode) { + case off: + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + case auto: + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + case always: + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + case torch: + default: + captureRequestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + captureRequestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); + break; + } + } + + public void startPreview() throws CameraAccessException { + if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; + + createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); + } + + public void startPreviewWithImageStream(EventChannel imageStreamChannel) + throws CameraAccessException { + createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); + + imageStreamChannel.setStreamHandler( + new EventChannel.StreamHandler() { + @Override + public void onListen(Object o, EventChannel.EventSink imageStreamSink) { + setImageStreamImageAvailableListener(imageStreamSink); + } + + @Override + public void onCancel(Object o) { + imageStreamReader.setOnImageAvailableListener(null, null); + } + }); + } + + private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { + imageStreamReader.setOnImageAvailableListener( + reader -> { + Image img = reader.acquireLatestImage(); + if (img == null) return; + + List> planes = new ArrayList<>(); + for (Image.Plane plane : img.getPlanes()) { + ByteBuffer buffer = plane.getBuffer(); + + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes, 0, bytes.length); + + Map planeBuffer = new HashMap<>(); + planeBuffer.put("bytesPerRow", plane.getRowStride()); + planeBuffer.put("bytesPerPixel", plane.getPixelStride()); + planeBuffer.put("bytes", bytes); + + planes.add(planeBuffer); + } + + Map imageBuffer = new HashMap<>(); + imageBuffer.put("width", img.getWidth()); + imageBuffer.put("height", img.getHeight()); + imageBuffer.put("format", img.getFormat()); + imageBuffer.put("planes", planes); + + imageStreamSink.success(imageBuffer); + img.close(); + }, + null); + } + + public void stopImageStream() throws CameraAccessException { + if (imageStreamReader != null) { + imageStreamReader.setOnImageAvailableListener(null, null); + } + startPreview(); + } + + /** Sets the time the pre-capture sequence started. */ + private void setPreCaptureStartTime() { + preCaptureStartTime = SystemClock.elapsedRealtime(); + } + + /** + * Check if the timeout for the pre-capture sequence has been reached. + * + * @return true if the timeout is reached; otherwise false is returned. + */ + private boolean hitPreCaptureTimeout() { + return (SystemClock.elapsedRealtime() - preCaptureStartTime) > PRECAPTURE_TIMEOUT_MS; + } + + private void closeCaptureSession() { + if (cameraCaptureSession != null) { + cameraCaptureSession.close(); + cameraCaptureSession = null; + } + } + + public void close() { + closeCaptureSession(); + + if (cameraDevice != null) { + cameraDevice.close(); + cameraDevice = null; + } + if (pictureImageReader != null) { + pictureImageReader.close(); + pictureImageReader = null; + } + if (imageStreamReader != null) { + imageStreamReader.close(); + imageStreamReader = null; + } + if (mediaRecorder != null) { + mediaRecorder.reset(); + mediaRecorder.release(); + mediaRecorder = null; + } + } + + public void dispose() { + close(); + flutterTexture.release(); + deviceOrientationListener.stop(); + } +} diff --git a/packages/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 similarity index 95% rename from packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java rename to packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java index b4569d2fec07..7d60e0fffa5c 100644 --- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPermissions.java @@ -1,3 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera; import android.Manifest; diff --git a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java similarity index 98% rename from packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java rename to packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java index 93183bb7c0a7..75730ab41711 100644 --- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraPlugin.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java new file mode 100644 index 000000000000..a69ddd0410d4 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraProperties.java @@ -0,0 +1,350 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import android.graphics.Rect; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; +import android.os.Build.VERSION_CODES; +import android.util.Range; +import android.util.Rational; +import android.util.Size; +import androidx.annotation.RequiresApi; + +/** An interface allowing access to the different characteristics of the device's camera. */ +public interface CameraProperties { + + /** + * Returns the name (or identifier) of the camera device. + * + * @return String The name of the camera device. + */ + String getCameraName(); + + /** + * Returns the list of frame rate ranges for @see android.control.aeTargetFpsRange supported by + * this camera device. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#CONTROL_AE_TARGET_FPS_RANGE key. + * + * @return android.util.Range[] List of frame rate ranges supported by this camera + * device. + */ + Range[] getControlAutoExposureAvailableTargetFpsRanges(); + + /** + * Returns the maximum and minimum exposure compensation values for @see + * android.control.aeExposureCompensation, in counts of @see android.control.aeCompensationStep, + * that are supported by this camera device. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#CONTROL_AE_COMPENSATION_RANGE key. + * + * @return android.util.Range Maximum and minimum exposure compensation supported by this + * camera device. + */ + Range getControlAutoExposureCompensationRange(); + + /** + * Returns the smallest step by which the exposure compensation can be changed. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#CONTROL_AE_COMPENSATION_STEP key. + * + * @return double Smallest step by which the exposure compensation can be changed. + */ + double getControlAutoExposureCompensationStep(); + + /** + * Returns a list of auto-focus modes for @see android.control.afMode that are supported by this + * camera device. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#CONTROL_AF_AVAILABLE_MODES key. + * + * @return int[] List of auto-focus modes supported by this camera device. + */ + int[] getControlAutoFocusAvailableModes(); + + /** + * Returns the maximum number of metering regions that can be used by the auto-exposure routine. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#CONTROL_MAX_REGIONS_AE key. + * + * @return Integer Maximum number of metering regions that can be used by the auto-exposure + * routine. + */ + Integer getControlMaxRegionsAutoExposure(); + + /** + * Returns the maximum number of metering regions that can be used by the auto-focus routine. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#CONTROL_MAX_REGIONS_AF key. + * + * @return Integer Maximum number of metering regions that can be used by the auto-focus routine. + */ + Integer getControlMaxRegionsAutoFocus(); + + /** + * Returns a list of distortion correction modes for @see android.distortionCorrection.mode that + * are supported by this camera device. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#DISTORTION_CORRECTION_AVAILABLE_MODES key. + * + * @return int[] List of distortion correction modes supported by this camera device. + */ + @RequiresApi(api = VERSION_CODES.P) + int[] getDistortionCorrectionAvailableModes(); + + /** + * Returns whether this camera device has a flash unit. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#FLASH_INFO_AVAILABLE key. + * + * @return Boolean Whether this camera device has a flash unit. + */ + Boolean getFlashInfoAvailable(); + + /** + * Returns the direction the camera faces relative to device screen. + * + *

Possible values: + * + *

    + *
  • @see android.hardware.camera2.CameraMetadata.LENS_FACING_FRONT + *
  • @see android.hardware.camera2.CameraMetadata.LENS_FACING_BACK + *
  • @see android.hardware.camera2.CameraMetadata.LENS_FACING_EXTERNAL + *
+ * + * By default maps to the @see android.hardware.camera2.CameraCharacteristics.LENS_FACING key. + * + * @return int Direction the camera faces relative to device screen. + */ + int getLensFacing(); + + /** + * Returns the shortest distance from front most surface of the lens that can be brought into + * sharp focus. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE key. + * + * @return Float Shortest distance from front most surface of the lens that can be brought into + * sharp focus. + */ + Float getLensInfoMinimumFocusDistance(); + + /** + * Returns the maximum ratio between both active area width and crop region width, and active area + * height and crop region height, for @see android.scaler.cropRegion. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#SCALER_AVAILABLE_MAX_DIGITAL_ZOOM key. + * + * @return Float Maximum ratio between both active area width and crop region width, and active + * area height and crop region height + */ + Float getScalerAvailableMaxDigitalZoom(); + + /** + * Returns the area of the image sensor which corresponds to active pixels after any geometric + * distortion correction has been applied. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#SENSOR_INFO_ACTIVE_ARRAY_SIZE key. + * + * @return android.graphics.Rect area of the image sensor which corresponds to active pixels after + * any geometric distortion correction has been applied. + */ + Rect getSensorInfoActiveArraySize(); + + /** + * Returns the dimensions of the full pixel array, possibly including black calibration pixels. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#SENSOR_INFO_PIXEL_ARRAY_SIZE key. + * + * @return android.util.Size Dimensions of the full pixel array, possibly including black + * calibration pixels. + */ + Size getSensorInfoPixelArraySize(); + + /** + * Returns the area of the image sensor which corresponds to active pixels prior to the + * application of any geometric distortion correction. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE + * key. + * + * @return android.graphics.Rect Area of the image sensor which corresponds to active pixels prior + * to the application of any geometric distortion correction. + */ + @RequiresApi(api = VERSION_CODES.M) + Rect getSensorInfoPreCorrectionActiveArraySize(); + + /** + * Returns the clockwise angle through which the output image needs to be rotated to be upright on + * the device screen in its native orientation. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#SENSOR_ORIENTATION key. + * + * @return int Clockwise angle through which the output image needs to be rotated to be upright on + * the device screen in its native orientation. + */ + int getSensorOrientation(); + + /** + * Returns a level which generally classifies the overall set of the camera device functionality. + * + *

Possible values: + * + *

    + *
  • @see android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY + *
  • @see android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED + *
  • @see android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_FULL + *
  • @see android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_LEVEL_3 + *
  • @see android.hardware.camera2.CameraMetadata.INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL + *
+ * + * By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL key. + * + * @return int Level which generally classifies the overall set of the camera device + * functionality. + */ + int getHardwareLevel(); + + /** + * Returns a list of noise reduction modes for @see android.noiseReduction.mode that are supported + * by this camera device. + * + *

By default maps to the @see + * android.hardware.camera2.CameraCharacteristics#NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES + * key. + * + * @return int[] List of noise reduction modes that are supported by this camera device. + */ + int[] getAvailableNoiseReductionModes(); +} + +/** + * Implementation of the @see CameraProperties interface using the @see + * android.hardware.camera2.CameraCharacteristics class to access the different characteristics. + */ +class CameraPropertiesImpl implements CameraProperties { + private final CameraCharacteristics cameraCharacteristics; + private final String cameraName; + + public CameraPropertiesImpl(String cameraName, CameraManager cameraManager) + throws CameraAccessException { + this.cameraName = cameraName; + this.cameraCharacteristics = cameraManager.getCameraCharacteristics(cameraName); + } + + @Override + public String getCameraName() { + return cameraName; + } + + @Override + public Range[] getControlAutoExposureAvailableTargetFpsRanges() { + return cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + } + + @Override + public Range getControlAutoExposureCompensationRange() { + return cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + } + + @Override + public double getControlAutoExposureCompensationStep() { + Rational rational = + cameraCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + + return rational == null ? 0.0 : rational.doubleValue(); + } + + @Override + public int[] getControlAutoFocusAvailableModes() { + return cameraCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); + } + + @Override + public Integer getControlMaxRegionsAutoExposure() { + return cameraCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); + } + + @Override + public Integer getControlMaxRegionsAutoFocus() { + return cameraCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); + } + + @RequiresApi(api = VERSION_CODES.P) + @Override + public int[] getDistortionCorrectionAvailableModes() { + return cameraCharacteristics.get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); + } + + @Override + public Boolean getFlashInfoAvailable() { + return cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + } + + @Override + public int getLensFacing() { + return cameraCharacteristics.get(CameraCharacteristics.LENS_FACING); + } + + @Override + public Float getLensInfoMinimumFocusDistance() { + return cameraCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE); + } + + @Override + public Float getScalerAvailableMaxDigitalZoom() { + return cameraCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM); + } + + @Override + public Rect getSensorInfoActiveArraySize() { + return cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + } + + @Override + public Size getSensorInfoPixelArraySize() { + return cameraCharacteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); + } + + @RequiresApi(api = VERSION_CODES.M) + @Override + public Rect getSensorInfoPreCorrectionActiveArraySize() { + return cameraCharacteristics.get( + CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); + } + + @Override + public int getSensorOrientation() { + return cameraCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); + } + + @Override + public int getHardwareLevel() { + return cameraCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); + } + + @Override + public int[] getAvailableNoiseReductionModes() { + return cameraCharacteristics.get( + CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.java new file mode 100644 index 000000000000..60c866cd82d5 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegions.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.camera; + +import android.hardware.camera2.params.MeteringRectangle; +import android.util.Size; + +public final class CameraRegions { + private MeteringRectangle aeMeteringRectangle; + private MeteringRectangle afMeteringRectangle; + private Size maxBoundaries; + + public CameraRegions(Size maxBoundaries) { + assert (maxBoundaries == null || maxBoundaries.getWidth() > 0); + assert (maxBoundaries == null || maxBoundaries.getHeight() > 0); + this.maxBoundaries = maxBoundaries; + } + + public MeteringRectangle getAEMeteringRectangle() { + return aeMeteringRectangle; + } + + public MeteringRectangle getAFMeteringRectangle() { + return afMeteringRectangle; + } + + public Size getMaxBoundaries() { + return this.maxBoundaries; + } + + public void resetAutoExposureMeteringRectangle() { + this.aeMeteringRectangle = null; + } + + public void setAutoExposureMeteringRectangleFromPoint(double x, double y) { + this.aeMeteringRectangle = getMeteringRectangleForPoint(maxBoundaries, x, y); + } + + public void resetAutoFocusMeteringRectangle() { + this.afMeteringRectangle = null; + } + + public void setAutoFocusMeteringRectangleFromPoint(double x, double y) { + this.afMeteringRectangle = getMeteringRectangleForPoint(maxBoundaries, x, y); + } + + public MeteringRectangle getMeteringRectangleForPoint(Size maxBoundaries, double x, double y) { + assert (x >= 0 && x <= 1); + assert (y >= 0 && y <= 1); + if (maxBoundaries == null) + throw new IllegalStateException( + "Functionality for managing metering rectangles is unavailable as this CameraRegions instance was initialized with null boundaries."); + + // Interpolate the target coordinate + int targetX = (int) Math.round(x * ((double) (maxBoundaries.getWidth() - 1))); + int targetY = (int) Math.round(y * ((double) (maxBoundaries.getHeight() - 1))); + // Determine the dimensions of the metering triangle (10th of the viewport) + int targetWidth = (int) Math.round(((double) maxBoundaries.getWidth()) / 10d); + int targetHeight = (int) Math.round(((double) maxBoundaries.getHeight()) / 10d); + // Adjust target coordinate to represent top-left corner of metering rectangle + targetX -= targetWidth / 2; + targetY -= targetHeight / 2; + // Adjust target coordinate as to not fall out of bounds + if (targetX < 0) targetX = 0; + if (targetY < 0) targetY = 0; + int maxTargetX = maxBoundaries.getWidth() - 1 - targetWidth; + int maxTargetY = maxBoundaries.getHeight() - 1 - targetHeight; + if (targetX > maxTargetX) targetX = maxTargetX; + if (targetY > maxTargetY) targetY = maxTargetY; + + // Build the metering rectangle + return new MeteringRectangle(targetX, targetY, targetWidth, targetHeight, 1); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java new file mode 100644 index 000000000000..b4d4689f2b4e --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.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.camera; + +import android.app.Activity; +import android.content.Context; +import android.graphics.ImageFormat; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.params.StreamConfigurationMap; +import android.media.CamcorderProfile; +import android.util.Size; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugins.camera.types.ResolutionPreset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** Provides various utilities for camera. */ +public final class CameraUtils { + + private CameraUtils() {} + + static PlatformChannel.DeviceOrientation getDeviceOrientationFromDegrees(int degrees) { + // Round to the nearest 90 degrees. + degrees = (int) (Math.round(degrees / 90.0) * 90) % 360; + // Determine the corresponding device orientation. + switch (degrees) { + case 90: + return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; + case 180: + return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; + case 270: + return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; + case 0: + default: + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + } + } + + static String serializeDeviceOrientation(PlatformChannel.DeviceOrientation orientation) { + if (orientation == null) + throw new UnsupportedOperationException("Could not serialize null device orientation."); + switch (orientation) { + case PORTRAIT_UP: + return "portraitUp"; + case PORTRAIT_DOWN: + return "portraitDown"; + case LANDSCAPE_LEFT: + return "landscapeLeft"; + case LANDSCAPE_RIGHT: + return "landscapeRight"; + default: + throw new UnsupportedOperationException( + "Could not serialize device orientation: " + orientation.toString()); + } + } + + static PlatformChannel.DeviceOrientation deserializeDeviceOrientation(String orientation) { + if (orientation == null) + throw new UnsupportedOperationException("Could not deserialize null device orientation."); + switch (orientation) { + case "portraitUp": + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + case "portraitDown": + return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; + case "landscapeLeft": + return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; + case "landscapeRight": + return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; + default: + throw new UnsupportedOperationException( + "Could not deserialize device orientation: " + orientation); + } + } + + static Size computeBestPreviewSize(String cameraName, ResolutionPreset preset) { + if (preset.ordinal() > ResolutionPreset.high.ordinal()) { + preset = ResolutionPreset.high; + } + + CamcorderProfile profile = + getBestAvailableCamcorderProfileForResolutionPreset(cameraName, preset); + return new Size(profile.videoFrameWidth, profile.videoFrameHeight); + } + + static Size computeBestCaptureSize(StreamConfigurationMap streamConfigurationMap) { + // For still image captures, we use the largest available size. + return Collections.max( + Arrays.asList(streamConfigurationMap.getOutputSizes(ImageFormat.JPEG)), + new CompareSizesByArea()); + } + + public static List> getAvailableCameras(Activity activity) + throws CameraAccessException { + CameraManager cameraManager = (CameraManager) activity.getSystemService(Context.CAMERA_SERVICE); + String[] cameraNames = cameraManager.getCameraIdList(); + List> cameras = new ArrayList<>(); + for (String cameraName : cameraNames) { + HashMap details = new HashMap<>(); + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName); + details.put("name", cameraName); + int sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); + details.put("sensorOrientation", sensorOrientation); + + int lensFacing = characteristics.get(CameraCharacteristics.LENS_FACING); + switch (lensFacing) { + case CameraMetadata.LENS_FACING_FRONT: + details.put("lensFacing", "front"); + break; + case CameraMetadata.LENS_FACING_BACK: + details.put("lensFacing", "back"); + break; + case CameraMetadata.LENS_FACING_EXTERNAL: + details.put("lensFacing", "external"); + break; + } + cameras.add(details); + } + return cameras; + } + + static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( + String cameraName, ResolutionPreset preset) { + int cameraId = Integer.parseInt(cameraName); + 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 static class CompareSizesByArea implements Comparator { + @Override + public int compare(Size lhs, Size rhs) { + // We cast here to ensure the multiplications won't overflow. + return Long.signum( + (long) lhs.getWidth() * lhs.getHeight() - (long) rhs.getWidth() * rhs.getHeight()); + } + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java new file mode 100644 index 000000000000..42ad6d76dcfc --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraZoom.java @@ -0,0 +1,52 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import android.graphics.Rect; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.math.MathUtils; + +public final class CameraZoom { + public static final float DEFAULT_ZOOM_FACTOR = 1.0f; + + @NonNull private final Rect cropRegion = new Rect(); + @Nullable private final Rect sensorSize; + + public final float maxZoom; + public final boolean hasSupport; + + public CameraZoom(@Nullable final Rect sensorArraySize, final Float maxZoom) { + this.sensorSize = sensorArraySize; + + if (this.sensorSize == null) { + this.maxZoom = DEFAULT_ZOOM_FACTOR; + this.hasSupport = false; + return; + } + + this.maxZoom = + ((maxZoom == null) || (maxZoom < DEFAULT_ZOOM_FACTOR)) ? DEFAULT_ZOOM_FACTOR : maxZoom; + + this.hasSupport = (Float.compare(this.maxZoom, DEFAULT_ZOOM_FACTOR) > 0); + } + + public Rect computeZoom(final float zoom) { + if (sensorSize == null || !this.hasSupport) { + return null; + } + + final float newZoom = MathUtils.clamp(zoom, DEFAULT_ZOOM_FACTOR, this.maxZoom); + + final int centerX = this.sensorSize.width() / 2; + final int centerY = this.sensorSize.height() / 2; + final int deltaX = (int) ((0.5f * this.sensorSize.width()) / newZoom); + final int deltaY = (int) ((0.5f * this.sensorSize.height()) / newZoom); + + this.cropRegion.set(centerX - deltaX, centerY - deltaY, centerX + deltaX, centerY + deltaY); + + return cropRegion; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java new file mode 100644 index 000000000000..aaac1361eb3d --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java @@ -0,0 +1,138 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import android.os.Handler; +import android.text.TextUtils; +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.MethodChannel; +import io.flutter.plugins.camera.types.ExposureMode; +import io.flutter.plugins.camera.types.FocusMode; +import java.util.HashMap; +import java.util.Map; + +class DartMessenger { + @NonNull private final Handler handler; + @Nullable private MethodChannel cameraChannel; + @Nullable private MethodChannel deviceChannel; + + enum DeviceEventType { + ORIENTATION_CHANGED("orientation_changed"); + private final String method; + + DeviceEventType(String method) { + this.method = method; + } + } + + enum CameraEventType { + ERROR("error"), + CLOSING("camera_closing"), + INITIALIZED("initialized"); + + private final String method; + + CameraEventType(String method) { + this.method = method; + } + } + + 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"); + this.handler = handler; + } + + void sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation orientation) { + assert (orientation != null); + this.send( + DeviceEventType.ORIENTATION_CHANGED, + new HashMap() { + { + put("orientation", CameraUtils.serializeDeviceOrientation(orientation)); + } + }); + } + + void sendCameraInitializedEvent( + Integer previewWidth, + Integer previewHeight, + ExposureMode exposureMode, + FocusMode focusMode, + Boolean exposurePointSupported, + Boolean focusPointSupported) { + assert (previewWidth != null); + assert (previewHeight != null); + assert (exposureMode != null); + assert (focusMode != null); + assert (exposurePointSupported != null); + assert (focusPointSupported != null); + this.send( + CameraEventType.INITIALIZED, + new HashMap() { + { + put("previewWidth", previewWidth.doubleValue()); + put("previewHeight", previewHeight.doubleValue()); + put("exposureMode", exposureMode.toString()); + put("focusMode", focusMode.toString()); + put("exposurePointSupported", exposurePointSupported); + put("focusPointSupported", focusPointSupported); + } + }); + } + + void sendCameraClosingEvent() { + send(CameraEventType.CLOSING); + } + + void sendCameraErrorEvent(@Nullable String description) { + this.send( + CameraEventType.ERROR, + new HashMap() { + { + if (!TextUtils.isEmpty(description)) put("description", description); + } + }); + } + + void send(CameraEventType eventType) { + send(eventType, new HashMap<>()); + } + + void send(CameraEventType eventType, Map args) { + if (cameraChannel == null) { + return; + } + + handler.post( + new Runnable() { + @Override + public void run() { + cameraChannel.invokeMethod(eventType.method, args); + } + }); + } + + void send(DeviceEventType eventType) { + send(eventType, new HashMap<>()); + } + + void send(DeviceEventType eventType, Map args) { + if (deviceChannel == null) { + return; + } + + handler.post( + new Runnable() { + @Override + public void run() { + deviceChannel.invokeMethod(eventType.method, args); + } + }); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java new file mode 100644 index 000000000000..634596dde8bb --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DeviceOrientationManager.java @@ -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. + +package io.flutter.plugins.camera; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.hardware.SensorManager; +import android.provider.Settings; +import android.view.Display; +import android.view.OrientationEventListener; +import android.view.Surface; +import android.view.WindowManager; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; + +class DeviceOrientationManager { + + private static final IntentFilter orientationIntentFilter = + new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED); + + private final Activity activity; + private final DartMessenger messenger; + private final boolean isFrontFacing; + private final int sensorOrientation; + private PlatformChannel.DeviceOrientation lastOrientation; + private OrientationEventListener orientationEventListener; + private BroadcastReceiver broadcastReceiver; + + public DeviceOrientationManager( + Activity activity, DartMessenger messenger, boolean isFrontFacing, int sensorOrientation) { + this.activity = activity; + this.messenger = messenger; + this.isFrontFacing = isFrontFacing; + this.sensorOrientation = sensorOrientation; + } + + public void start() { + startSensorListener(); + startUIListener(); + } + + public void stop() { + stopSensorListener(); + stopUIListener(); + } + + public int getMediaOrientation() { + return this.getMediaOrientation(this.lastOrientation); + } + + public int getMediaOrientation(PlatformChannel.DeviceOrientation orientation) { + int angle = 0; + + // Fallback to device orientation when the orientation value is null + if (orientation == null) { + orientation = getUIOrientation(); + } + + switch (orientation) { + case PORTRAIT_UP: + angle = 0; + break; + case PORTRAIT_DOWN: + angle = 180; + break; + case LANDSCAPE_LEFT: + angle = 90; + break; + case LANDSCAPE_RIGHT: + angle = 270; + break; + } + if (isFrontFacing) angle *= -1; + return (angle + sensorOrientation + 360) % 360; + } + + private void startSensorListener() { + if (orientationEventListener != null) return; + orientationEventListener = + new OrientationEventListener(activity, SensorManager.SENSOR_DELAY_NORMAL) { + @Override + public void onOrientationChanged(int angle) { + if (!isSystemAutoRotationLocked()) { + PlatformChannel.DeviceOrientation newOrientation = calculateSensorOrientation(angle); + if (!newOrientation.equals(lastOrientation)) { + lastOrientation = newOrientation; + messenger.sendDeviceOrientationChangeEvent(newOrientation); + } + } + } + }; + if (orientationEventListener.canDetectOrientation()) { + orientationEventListener.enable(); + } + } + + private void startUIListener() { + if (broadcastReceiver != null) return; + broadcastReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (isSystemAutoRotationLocked()) { + PlatformChannel.DeviceOrientation orientation = getUIOrientation(); + if (!orientation.equals(lastOrientation)) { + lastOrientation = orientation; + messenger.sendDeviceOrientationChangeEvent(orientation); + } + } + } + }; + activity.registerReceiver(broadcastReceiver, orientationIntentFilter); + broadcastReceiver.onReceive(activity, null); + } + + private void stopSensorListener() { + if (orientationEventListener == null) return; + orientationEventListener.disable(); + orientationEventListener = null; + } + + private void stopUIListener() { + if (broadcastReceiver == null) return; + activity.unregisterReceiver(broadcastReceiver); + broadcastReceiver = null; + } + + private boolean isSystemAutoRotationLocked() { + return android.provider.Settings.System.getInt( + activity.getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 0) + != 1; + } + + private PlatformChannel.DeviceOrientation getUIOrientation() { + final int rotation = getDisplay().getRotation(); + final int orientation = activity.getResources().getConfiguration().orientation; + + switch (orientation) { + case Configuration.ORIENTATION_PORTRAIT: + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + } else { + return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; + } + case Configuration.ORIENTATION_LANDSCAPE: + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { + return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; + } else { + return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; + } + default: + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + } + } + + private PlatformChannel.DeviceOrientation calculateSensorOrientation(int angle) { + final int tolerance = 45; + angle += tolerance; + + // Orientation is 0 in the default orientation mode. This is portait-mode for phones + // and landscape for tablets. We have to compensate for this by calculating the default + // orientation, and apply an offset accordingly. + int defaultDeviceOrientation = getDeviceDefaultOrientation(); + if (defaultDeviceOrientation == Configuration.ORIENTATION_LANDSCAPE) { + angle += 90; + } + // Determine the orientation + angle = angle % 360; + return new PlatformChannel.DeviceOrientation[] { + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + } + [angle / 90]; + } + + private int getDeviceDefaultOrientation() { + Configuration config = activity.getResources().getConfiguration(); + int rotation = getDisplay().getRotation(); + if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) + && config.orientation == Configuration.ORIENTATION_LANDSCAPE) + || ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) + && config.orientation == Configuration.ORIENTATION_PORTRAIT)) { + return Configuration.ORIENTATION_LANDSCAPE; + } else { + return Configuration.ORIENTATION_PORTRAIT; + } + } + + @SuppressWarnings("deprecation") + private Display getDisplay() { + return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java new file mode 100644 index 000000000000..50bca6349217 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -0,0 +1,388 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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.types.ExposureMode; +import io.flutter.plugins.camera.types.FlashMode; +import io.flutter.plugins.camera.types.FocusMode; +import io.flutter.view.TextureRegistry; +import java.util.HashMap; +import java.util.Map; + +final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { + private final Activity activity; + 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"); + 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, x, y); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getMinExposureOffset": + { + try { + result.success(camera.getMinExposureOffset()); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getMaxExposureOffset": + { + try { + result.success(camera.getMaxExposureOffset()); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getExposureOffsetStepSize": + { + try { + result.success(camera.getExposureOffsetStepSize()); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setExposureOffset": + { + try { + camera.setExposureOffset(result, call.argument("offset")); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setFocusMode": + { + String modeStr = call.argument("mode"); + FocusMode mode = FocusMode.getValueForString(modeStr); + if (mode == null) { + result.error("setFocusModeFailed", "Unknown focus mode " + modeStr, null); + return; + } + try { + camera.setFocusMode(result, mode); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setFocusPoint": + { + Boolean reset = call.argument("reset"); + Double x = null; + Double y = null; + if (reset == null || !reset) { + x = call.argument("x"); + y = call.argument("y"); + } + try { + camera.setFocusPoint(result, x, y); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "startImageStream": + { + try { + 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 "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 resolutionPreset = call.argument("resolutionPreset"); + boolean enableAudio = call.argument("enableAudio"); + TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture = + textureRegistry.createSurfaceTexture(); + DartMessenger dartMessenger = + new DartMessenger( + messenger, flutterSurfaceTexture.id(), new Handler(Looper.getMainLooper())); + camera = + new Camera( + activity, + flutterSurfaceTexture, + dartMessenger, + cameraName, + 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/PictureCaptureRequest.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.java new file mode 100644 index 000000000000..4c11e2d40e62 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/PictureCaptureRequest.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.camera; + +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.Nullable; +import io.flutter.plugin.common.MethodChannel; + +class PictureCaptureRequest { + + enum State { + idle, + focusing, + preCapture, + waitingPreCaptureReady, + capturing, + finished, + error, + } + + private final Runnable timeoutCallback = + new Runnable() { + @Override + public void run() { + error("captureTimeout", "Picture capture request timed out", state.toString()); + } + }; + + private final MethodChannel.Result result; + private final TimeoutHandler timeoutHandler; + private State state; + + public PictureCaptureRequest(MethodChannel.Result result) { + this(result, new TimeoutHandler()); + } + + public PictureCaptureRequest(MethodChannel.Result result, TimeoutHandler timeoutHandler) { + this.result = result; + this.state = State.idle; + this.timeoutHandler = timeoutHandler; + } + + public void setState(State state) { + if (isFinished()) throw new IllegalStateException("Request has already been finished"); + this.state = state; + if (state != State.idle && state != State.finished && state != State.error) { + this.timeoutHandler.resetTimeout(timeoutCallback); + } else { + this.timeoutHandler.clearTimeout(timeoutCallback); + } + } + + public State getState() { + return state; + } + + public boolean isFinished() { + return state == State.finished || state == State.error; + } + + public void finish(String absolutePath) { + if (isFinished()) throw new IllegalStateException("Request has already been finished"); + this.timeoutHandler.clearTimeout(timeoutCallback); + result.success(absolutePath); + state = State.finished; + } + + public void error( + String errorCode, @Nullable String errorMessage, @Nullable Object errorDetails) { + if (isFinished()) throw new IllegalStateException("Request has already been finished"); + this.timeoutHandler.clearTimeout(timeoutCallback); + result.error(errorCode, errorMessage, errorDetails); + state = State.error; + } + + static class TimeoutHandler { + private static final int REQUEST_TIMEOUT = 5000; + private final Handler handler; + + TimeoutHandler() { + this.handler = new Handler(Looper.getMainLooper()); + } + + public void resetTimeout(Runnable runnable) { + clearTimeout(runnable); + handler.postDelayed(runnable, REQUEST_TIMEOUT); + } + + public void clearTimeout(Runnable runnable) { + handler.removeCallbacks(runnable); + } + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java new file mode 100644 index 000000000000..92cfd548cd06 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java @@ -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. + +package io.flutter.plugins.camera.features; + +import android.hardware.camera2.CaptureRequest; +import androidx.annotation.NonNull; +import io.flutter.plugins.camera.CameraProperties; + +/** + * An interface describing a feature in the camera. This holds a setting value of type T and must + * implement a means to check if this setting is supported by the current camera properties. It also + * must implement a builder update method which will update a given capture request builder for this + * feature's current setting value. + * + * @param + */ +public abstract class CameraFeature { + + protected final CameraProperties cameraProperties; + + protected CameraFeature(@NonNull CameraProperties cameraProperties) { + this.cameraProperties = cameraProperties; + } + + /** Debug name for this feature. */ + public abstract String getDebugName(); + + /** + * Gets the current value of this feature's setting. + * + * @return Current value of this feature's setting. + */ + public abstract T getValue(); + + /** + * Sets a new value for this feature's setting. + * + * @param value New value for this feature's setting. + */ + public abstract void setValue(T value); + + /** + * Returns whether or not this feature is supported. + * + *

When the feature is not supported any {@see #value} is simply ignored by the camera plugin. + * + * @return boolean Whether or not this feature is supported. + */ + public abstract boolean checkIsSupported(); + + /** + * Updates the setting in a provided {@see android.hardware.camera2.CaptureRequest.Builder}. + * + * @param requestBuilder A {@see android.hardware.camera2.CaptureRequest.Builder} instance used to + * configure the settings and outputs needed to capture a single image from the camera device. + */ + public abstract void updateBuilder(CaptureRequest.Builder requestBuilder); +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Point.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Point.java new file mode 100644 index 000000000000..b6b64f92d987 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/Point.java @@ -0,0 +1,16 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.features; + +/** Represents a point on an x/y axis. */ +public class Point { + public final Double x; + public final Double y; + + public Point(Double x, Double y) { + this.x = x; + this.y = y; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeature.java new file mode 100644 index 000000000000..1789a964253b --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeature.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.camera.features.autofocus; + +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CaptureRequest; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; + +/** Controls the auto focus configuration on the {@see anddroid.hardware.camera2} API. */ +public class AutoFocusFeature extends CameraFeature { + private FocusMode currentSetting = FocusMode.auto; + + // When switching recording modes this feature is re-created with the appropriate setting here. + private final boolean recordingVideo; + + /** + * Creates a new instance of the {@see AutoFocusFeature}. + * + * @param cameraProperties Collection of the characteristics for the current camera device. + * @param recordingVideo Indicates whether the camera is currently recording video. + */ + public AutoFocusFeature(CameraProperties cameraProperties, boolean recordingVideo) { + super(cameraProperties); + this.recordingVideo = recordingVideo; + } + + @Override + public String getDebugName() { + return "AutoFocusFeature"; + } + + @Override + public FocusMode getValue() { + return currentSetting; + } + + @Override + public void setValue(FocusMode value) { + this.currentSetting = value; + } + + @Override + public boolean checkIsSupported() { + int[] modes = cameraProperties.getControlAutoFocusAvailableModes(); + + final Float minFocus = cameraProperties.getLensInfoMinimumFocusDistance(); + + // Check if the focal length of the lens is fixed. If the minimum focus distance == 0, then the + // focal length is fixed. The minimum focus distance can be null on some devices: https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#LENS_INFO_MINIMUM_FOCUS_DISTANCE + boolean isFixedLength = minFocus == null || minFocus == 0; + + return !isFixedLength + && !(modes.length == 0 + || (modes.length == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF)); + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + if (!checkIsSupported()) { + return; + } + + switch (currentSetting) { + case locked: + // When locking the auto-focus the camera device should do a one-time focus and afterwards + // set the auto-focus to idle. This is accomplished by setting the CONTROL_AF_MODE to + // CONTROL_AF_MODE_AUTO. + requestBuilder.set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); + break; + case auto: + requestBuilder.set( + CaptureRequest.CONTROL_AF_MODE, + recordingVideo + ? CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO + : CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + default: + break; + } + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autofocus/FocusMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autofocus/FocusMode.java new file mode 100644 index 000000000000..56331b4fab8c --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/autofocus/FocusMode.java @@ -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. + +package io.flutter.plugins.camera.features.autofocus; + +// Mirrors focus_mode.dart +public enum FocusMode { + auto("auto"), + locked("locked"); + + private final String strValue; + + FocusMode(String strValue) { + this.strValue = strValue; + } + + public static FocusMode getValueForString(String modeStr) { + for (FocusMode value : values()) { + if (value.strValue.equals(modeStr)) { + return value; + } + } + return null; + } + + @Override + public String toString() { + return strValue; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeature.java new file mode 100644 index 000000000000..df08cd9a3c77 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeature.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.camera.features.exposurelock; + +import android.hardware.camera2.CaptureRequest; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; + +/** Controls whether or not the exposure mode is currently locked or automatically metering. */ +public class ExposureLockFeature extends CameraFeature { + + private ExposureMode currentSetting = ExposureMode.auto; + + /** + * Creates a new instance of the {@see ExposureLockFeature}. + * + * @param cameraProperties Collection of the characteristics for the current camera device. + */ + public ExposureLockFeature(CameraProperties cameraProperties) { + super(cameraProperties); + } + + @Override + public String getDebugName() { + return "ExposureLockFeature"; + } + + @Override + public ExposureMode getValue() { + return currentSetting; + } + + @Override + public void setValue(ExposureMode value) { + this.currentSetting = value; + } + + // Available on all devices. + @Override + public boolean checkIsSupported() { + return true; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + if (!checkIsSupported()) { + return; + } + + requestBuilder.set(CaptureRequest.CONTROL_AE_LOCK, currentSetting == ExposureMode.locked); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureMode.java new file mode 100644 index 000000000000..2971fb23727a --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurelock/ExposureMode.java @@ -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. + +package io.flutter.plugins.camera.features.exposurelock; + +// Mirrors exposure_mode.dart +public enum ExposureMode { + auto("auto"), + locked("locked"); + + private final String strValue; + + ExposureMode(String strValue) { + this.strValue = strValue; + } + + /** + * Tries to convert the supplied string into an {@see ExposureMode} enum value. + * + *

When the supplied string doesn't match a valid {@see ExposureMode} enum value, null is + * returned. + * + * @param modeStr String value to convert into an {@see ExposureMode} enum value. + * @return Matching {@see ExposureMode} enum value, or null if no match is found. + */ + public static ExposureMode getValueForString(String modeStr) { + for (ExposureMode value : values()) { + if (value.strValue.equals(modeStr)) { + return value; + } + } + return null; + } + + @Override + public String toString() { + return strValue; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.java new file mode 100644 index 000000000000..d5a9fcd4a38a --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeature.java @@ -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. + +package io.flutter.plugins.camera.features.exposureoffset; + +import android.hardware.camera2.CaptureRequest; +import android.util.Range; +import androidx.annotation.NonNull; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; + +/** Controls the exposure offset making the resulting image brighter or darker. */ +public class ExposureOffsetFeature extends CameraFeature { + + private double currentSetting = 0; + + /** + * Creates a new instance of the {@link ExposureOffsetFeature}. + * + * @param cameraProperties Collection of the characteristics for the current camera device. + */ + public ExposureOffsetFeature(CameraProperties cameraProperties) { + super(cameraProperties); + } + + @Override + public String getDebugName() { + return "ExposureOffsetFeature"; + } + + @Override + public Double getValue() { + return currentSetting; + } + + @Override + public void setValue(@NonNull Double value) { + double stepSize = getExposureOffsetStepSize(); + this.currentSetting = value / stepSize; + } + + // Available on all devices. + @Override + public boolean checkIsSupported() { + return true; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + if (!checkIsSupported()) { + return; + } + + requestBuilder.set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, (int) currentSetting); + } + + /** + * Returns the minimum exposure offset. + * + * @return double Minimum exposure offset. + */ + public double getMinExposureOffset() { + Range range = cameraProperties.getControlAutoExposureCompensationRange(); + double minStepped = range == null ? 0 : range.getLower(); + double stepSize = getExposureOffsetStepSize(); + return minStepped * stepSize; + } + + /** + * Returns the maximum exposure offset. + * + * @return double Maximum exposure offset. + */ + public double getMaxExposureOffset() { + Range range = cameraProperties.getControlAutoExposureCompensationRange(); + double maxStepped = range == null ? 0 : range.getUpper(); + double stepSize = getExposureOffsetStepSize(); + return maxStepped * stepSize; + } + + /** + * Returns the smallest step by which the exposure compensation can be changed. + * + *

Example: if this has a value of 0.5, then an aeExposureCompensation setting of -2 means that + * the actual AE offset is -1. More details can be found in the official Android documentation: + * https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics.html#CONTROL_AE_COMPENSATION_STEP + * + * @return double Smallest step by which the exposure compensation can be changed. + */ + public double getExposureOffsetStepSize() { + return cameraProperties.getControlAutoExposureCompensationStep(); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java new file mode 100644 index 000000000000..f729d33c8528 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java @@ -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. + +package io.flutter.plugins.camera.features.exposurepoint; + +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.MeteringRectangle; +import android.util.Log; +import androidx.annotation.NonNull; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; +import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.types.CameraRegions; + +/** Exposure point controls where in the frame exposure metering will come from. */ +public class ExposurePointFeature extends CameraFeature { + + private final CameraRegions cameraRegions; + private Point currentSetting = new Point(0.0, 0.0); + + /** + * Creates a new instance of the {@link ExposurePointFeature}. + * + * @param cameraProperties Collection of the characteristics for the current camera device. + * @param cameraRegions Utility class to assist in calculating exposure boundaries. + */ + public ExposurePointFeature(CameraProperties cameraProperties, CameraRegions cameraRegions) { + super(cameraProperties); + this.cameraRegions = cameraRegions; + } + + @Override + public String getDebugName() { + return "ExposurePointFeature"; + } + + @Override + public Point getValue() { + return currentSetting; + } + + @Override + public void setValue(@NonNull Point value) { + this.currentSetting = value; + + if (value.x == null || value.y == null) { + cameraRegions.resetAutoExposureMeteringRectangle(); + } else { + cameraRegions.setAutoExposureMeteringRectangleFromPoint(value.x, value.y); + } + } + + // Whether or not this camera can set the exposure point. + @Override + public boolean checkIsSupported() { + Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoExposure(); + return supportedRegions != null && supportedRegions > 0; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + if (!checkIsSupported()) { + return; + } + + MeteringRectangle aeRect = null; + try { + aeRect = cameraRegions.getAEMeteringRectangle(); + } catch (Exception e) { + Log.w("Camera", "Unable to retrieve the Auto Exposure metering rectangle.", e); + } + + requestBuilder.set( + CaptureRequest.CONTROL_AE_REGIONS, + aeRect == null ? null : new MeteringRectangle[] {aeRect}); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashFeature.java new file mode 100644 index 000000000000..054c81f5183b --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashFeature.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.features.flash; + +import android.hardware.camera2.CaptureRequest; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; + +/** Controls the flash configuration on the {@link android.hardware.camera2} API. */ +public class FlashFeature extends CameraFeature { + private FlashMode currentSetting = FlashMode.auto; + + /** + * Creates a new instance of the {@link FlashFeature}. + * + * @param cameraProperties Collection of characteristics for the current camera device. + */ + public FlashFeature(CameraProperties cameraProperties) { + super(cameraProperties); + } + + @Override + public String getDebugName() { + return "FlashFeature"; + } + + @Override + public FlashMode getValue() { + return currentSetting; + } + + @Override + public void setValue(FlashMode value) { + this.currentSetting = value; + } + + @Override + public boolean checkIsSupported() { + Boolean available = cameraProperties.getFlashInfoAvailable(); + return available != null && available; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + if (!checkIsSupported()) { + return; + } + + switch (currentSetting) { + case off: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + + case always: + requestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + + case torch: + requestBuilder.set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); + break; + + case auto: + requestBuilder.set( + CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + requestBuilder.set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + break; + } + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashMode.java new file mode 100644 index 000000000000..d4a5ee0ab12f --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashMode.java @@ -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. + +package io.flutter.plugins.camera.features.flash; + +// Mirrors flash_mode.dart +public enum FlashMode { + off("off"), + auto("auto"), + always("always"), + torch("torch"); + + private final String strValue; + + FlashMode(String strValue) { + this.strValue = strValue; + } + + public static FlashMode getValueForString(String modeStr) { + for (FlashMode value : values()) { + if (value.strValue.equals(modeStr)) return value; + } + return null; + } + + @Override + public String toString() { + return strValue; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeature.java new file mode 100644 index 000000000000..736fad4d92dc --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeature.java @@ -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. + +package io.flutter.plugins.camera.features.zoomlevel; + +import android.graphics.Rect; +import android.hardware.camera2.CaptureRequest; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; + +/** Controls the zoom configuration on the {@link android.hardware.camera2} API. */ +public class ZoomLevelFeature extends CameraFeature { + private static final float MINIMUM_ZOOM_LEVEL = 1.0f; + private final boolean hasSupport; + private final Rect sensorArraySize; + private Float currentSetting = MINIMUM_ZOOM_LEVEL; + private Float maximumZoomLevel = MINIMUM_ZOOM_LEVEL; + + /** + * Creates a new instance of the {@link ZoomLevelFeature}. + * + * @param cameraProperties Collection of characteristics for the current camera device. + */ + public ZoomLevelFeature(CameraProperties cameraProperties) { + super(cameraProperties); + + sensorArraySize = cameraProperties.getSensorInfoActiveArraySize(); + + if (sensorArraySize == null) { + maximumZoomLevel = MINIMUM_ZOOM_LEVEL; + hasSupport = false; + return; + } + + Float maxDigitalZoom = cameraProperties.getScalerAvailableMaxDigitalZoom(); + maximumZoomLevel = + ((maxDigitalZoom == null) || (maxDigitalZoom < MINIMUM_ZOOM_LEVEL)) + ? MINIMUM_ZOOM_LEVEL + : maxDigitalZoom; + + hasSupport = (Float.compare(maximumZoomLevel, MINIMUM_ZOOM_LEVEL) > 0); + } + + @Override + public String getDebugName() { + return "ZoomLevelFeature"; + } + + @Override + public Float getValue() { + return currentSetting; + } + + @Override + public void setValue(Float value) { + currentSetting = value; + } + + @Override + public boolean checkIsSupported() { + return hasSupport; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + if (!checkIsSupported()) { + return; + } + + final Rect computedZoom = + ZoomUtils.computeZoom( + currentSetting, sensorArraySize, MINIMUM_ZOOM_LEVEL, maximumZoomLevel); + requestBuilder.set(CaptureRequest.SCALER_CROP_REGION, computedZoom); + } + + /** + * Gets the minimum supported zoom level. + * + * @return The minimum zoom level. + */ + public float getMinimumZoomLevel() { + return MINIMUM_ZOOM_LEVEL; + } + + /** + * Gets the maximum supported zoom level. + * + * @return The maximum zoom level. + */ + public float getMaximumZoomLevel() { + return maximumZoomLevel; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtils.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtils.java new file mode 100644 index 000000000000..a4890b952cff --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtils.java @@ -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. + +package io.flutter.plugins.camera.features.zoomlevel; + +import android.graphics.Rect; +import androidx.annotation.NonNull; +import androidx.core.math.MathUtils; + +/** + * Utility class containing methods that assist with zoom features in the {@link + * android.hardware.camera2} API. + */ +final class ZoomUtils { + + /** + * Computes an image sensor area based on the supplied zoom settings. + * + *

The returned image sensor area can be applied to the {@link android.hardware.camera2} API in + * order to control zoom levels. + * + * @param zoom The desired zoom level. + * @param sensorArraySize The current area of the image sensor. + * @param minimumZoomLevel The minimum supported zoom level. + * @param maximumZoomLevel The maximim supported zoom level. + * @return An image sensor area based on the supplied zoom settings + */ + static Rect computeZoom( + float zoom, @NonNull Rect sensorArraySize, float minimumZoomLevel, float maximumZoomLevel) { + final float newZoom = MathUtils.clamp(zoom, minimumZoomLevel, maximumZoomLevel); + + final int centerX = sensorArraySize.width() / 2; + final int centerY = sensorArraySize.height() / 2; + final int deltaX = (int) ((0.5f * sensorArraySize.width()) / newZoom); + final int deltaY = (int) ((0.5f * sensorArraySize.height()) / newZoom); + + return new Rect(centerX - deltaX, centerY - deltaY, centerX + deltaX, centerY + deltaY); + } +} 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 new file mode 100644 index 000000000000..a78c2b47b7ad --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/media/MediaRecorderBuilder.java @@ -0,0 +1,74 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.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/main/java/io/flutter/plugins/camera/types/CameraRegions.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CameraRegions.java new file mode 100644 index 000000000000..b86241e78d29 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CameraRegions.java @@ -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. + +package io.flutter.plugins.camera.types; + +import android.annotation.TargetApi; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.MeteringRectangle; +import android.os.Build; +import android.util.Size; +import androidx.annotation.NonNull; +import io.flutter.plugins.camera.CameraProperties; +import java.util.Arrays; + +/** + * Utility class that contains information regarding the camera's regions. + * + *

The regions information is used to calculate focus and exposure settings. + */ +public final class CameraRegions { + + /** Factory class that assists in creating a {@link CameraRegions} instance. */ + public static class Factory { + /** + * Creates a new instance of the {@link CameraRegions} class. + * + *

The {@link CameraProperties} and {@link CaptureRequest.Builder} classed are used to + * determine if the device's camera supports distortion correction mode and calculate the + * correct boundaries based on the outcome. + * + * @param cameraProperties Collection of the characteristics for the current camera device. + * @param requestBuilder CaptureRequest builder containing current target and surface settings. + * @return new instance of the {@link CameraRegions} class. + */ + public static CameraRegions create( + @NonNull CameraProperties cameraProperties, + @NonNull CaptureRequest.Builder requestBuilder) { + Size boundaries; + + // No distortion correction support + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.P + && supportsDistortionCorrection(cameraProperties)) { + // Get the current distortion correction mode + Integer distortionCorrectionMode = + requestBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE); + + // Return the correct boundaries depending on the mode + android.graphics.Rect rect; + if (distortionCorrectionMode == null + || distortionCorrectionMode == CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) { + rect = cameraProperties.getSensorInfoPreCorrectionActiveArraySize(); + } else { + rect = cameraProperties.getSensorInfoActiveArraySize(); + } + + // Set new region size + boundaries = rect == null ? null : new Size(rect.width(), rect.height()); + } else { + boundaries = cameraProperties.getSensorInfoPixelArraySize(); + } + + // Create new camera regions using new size + return new CameraRegions(boundaries); + } + + @TargetApi(Build.VERSION_CODES.P) + private static boolean supportsDistortionCorrection(CameraProperties cameraProperties) { + int[] availableDistortionCorrectionModes = + cameraProperties.getDistortionCorrectionAvailableModes(); + if (availableDistortionCorrectionModes == null) { + availableDistortionCorrectionModes = new int[0]; + } + long nonOffModesSupported = + Arrays.stream(availableDistortionCorrectionModes) + .filter((value) -> value != CaptureRequest.DISTORTION_CORRECTION_MODE_OFF) + .count(); + return nonOffModesSupported > 0; + } + } + + private final Size boundaries; + + private MeteringRectangle aeMeteringRectangle; + private MeteringRectangle afMeteringRectangle; + + /** + * Creates a new instance of the {@link CameraRegions} class. + * + * @param boundaries The area of the image sensor. + */ + CameraRegions(Size boundaries) { + assert (boundaries == null || boundaries.getWidth() > 0); + assert (boundaries == null || boundaries.getHeight() > 0); + + this.boundaries = boundaries; + } + + /** + * Gets the {@link MeteringRectangle} on which the auto exposure will be applied. + * + * @return The {@link MeteringRectangle} on which the auto exposure will be applied. + */ + public MeteringRectangle getAEMeteringRectangle() { + return aeMeteringRectangle; + } + + /** + * Gets the {@link MeteringRectangle} on which the auto focus will be applied. + * + * @return The {@link MeteringRectangle} on which the auto focus will be applied. + */ + public MeteringRectangle getAFMeteringRectangle() { + return afMeteringRectangle; + } + + /** + * Gets the area of the image sensor. + * + *

If distortion correction is supported the size corresponds to the active pixels after any + * geometric distortion correction has been applied. If distortion correction is not supported the + * dimensions include the full pixel array, possibly including black calibration pixels. + * + * @return The area of the image sensor. + */ + public Size getBoundaries() { + return this.boundaries; + } + + /** Resets the {@link MeteringRectangle} on which the auto exposure will be applied. */ + public void resetAutoExposureMeteringRectangle() { + this.aeMeteringRectangle = null; + } + + /** + * Sets the coordinates which will form the centre of the exposure rectangle. + * + * @param x x – coordinate >= 0 + * @param y y – coordinate >= 0 + */ + public void setAutoExposureMeteringRectangleFromPoint(double x, double y) { + this.aeMeteringRectangle = convertPointToMeteringRectangle(x, y); + } + + /** Resets the {@link MeteringRectangle} on which the auto focus will be applied. */ + public void resetAutoFocusMeteringRectangle() { + this.afMeteringRectangle = null; + } + + /** + * Sets the coordinates which will form the centre of the focus rectangle. + * + * @param x x – coordinate >= 0 + * @param y y – coordinate >= 0 + */ + public void setAutoFocusMeteringRectangleFromPoint(double x, double y) { + this.afMeteringRectangle = convertPointToMeteringRectangle(x, y); + } + + /** + * Converts a point into a {@link MeteringRectangle} with the supplied coordinates as the centre + * point. + * + *

Since the Camera API (due to cross-platform constraints) only accepts a point when + * configuring a specific focus or exposure area and Android requires a rectangle to configure + * these settings there is a need to convert the point into a rectangle. This method will create + * the required rectangle with an arbitrarily size that is a 10th of the current viewport and the + * coordinates as the centre point. + * + * @param x x - coordinate >= 0 + * @param y y - coordinate >= 0 + * @return The dimensions of the metering rectangle based on the supplied coordinates. + */ + MeteringRectangle convertPointToMeteringRectangle(double x, double y) { + assert (x >= 0 && x <= 1); + assert (y >= 0 && y <= 1); + + // Interpolate the target coordinate + int targetX = (int) Math.round(x * ((double) (boundaries.getWidth() - 1))); + int targetY = (int) Math.round(y * ((double) (boundaries.getHeight() - 1))); + // Since the Camera API only allows Determine the dimensions of the metering rectangle (10th of + // the viewport) + int targetWidth = (int) Math.round(((double) boundaries.getWidth()) / 10d); + int targetHeight = (int) Math.round(((double) boundaries.getHeight()) / 10d); + // Adjust target coordinate to represent top-left corner of metering rectangle + targetX -= targetWidth / 2; + targetY -= targetHeight / 2; + // Adjust target coordinate as to not fall out of bounds + if (targetX < 0) targetX = 0; + if (targetY < 0) targetY = 0; + int maxTargetX = boundaries.getWidth() - 1 - targetWidth; + int maxTargetY = boundaries.getHeight() - 1 - targetHeight; + if (targetX > maxTargetX) targetX = maxTargetX; + if (targetY > maxTargetY) targetY = maxTargetY; + + // Build the metering rectangle + return new MeteringRectangle(targetX, targetY, targetWidth, targetHeight, 1); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ExposureMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ExposureMode.java new file mode 100644 index 000000000000..0bd23945e3f7 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ExposureMode.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.camera.types; + +// Mirrors exposure_mode.dart +public enum ExposureMode { + auto("auto"), + locked("locked"); + + private final String strValue; + + ExposureMode(String strValue) { + this.strValue = strValue; + } + + public static ExposureMode getValueForString(String modeStr) { + for (ExposureMode value : values()) { + if (value.strValue.equals(modeStr)) return value; + } + return null; + } + + @Override + public String toString() { + return strValue; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java new file mode 100644 index 000000000000..d7b661380098 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FlashMode.java @@ -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. + +package io.flutter.plugins.camera.types; + +// Mirrors flash_mode.dart +public enum FlashMode { + off("off"), + auto("auto"), + always("always"), + torch("torch"); + + private final String strValue; + + FlashMode(String strValue) { + this.strValue = strValue; + } + + public static FlashMode getValueForString(String modeStr) { + for (FlashMode value : values()) { + if (value.strValue.equals(modeStr)) return value; + } + return null; + } + + @Override + public String toString() { + return strValue; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FocusMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FocusMode.java new file mode 100644 index 000000000000..c879593d4f21 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/FocusMode.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.camera.types; + +// Mirrors focus_mode.dart +public enum FocusMode { + auto("auto"), + locked("locked"); + + private final String strValue; + + FocusMode(String strValue) { + this.strValue = strValue; + } + + public static FocusMode getValueForString(String modeStr) { + for (FocusMode value : values()) { + if (value.strValue.equals(modeStr)) return value; + } + return null; + } + + @Override + public String toString() { + return strValue; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ResolutionPreset.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ResolutionPreset.java new file mode 100644 index 000000000000..a70d85688037 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/ResolutionPreset.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.camera.types; + +// Mirrors camera.dart +public enum ResolutionPreset { + low, + medium, + high, + veryHigh, + ultraHigh, + max, +} diff --git a/packages/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 similarity index 82% rename from packages/camera/android/src/test/java/io/flutter/plugins/camera/CameraPermissionsTest.java rename to packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPermissionsTest.java index b622c313258a..ecb96a88f31a 100644 --- a/packages/camera/android/src/test/java/io/flutter/plugins/camera/CameraPermissionsTest.java +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPermissionsTest.java @@ -1,3 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.camera; import static junit.framework.TestCase.assertEquals; diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java new file mode 100644 index 000000000000..2c0381744191 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java @@ -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. + +package io.flutter.plugins.camera; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.fail; +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.graphics.Rect; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; +import android.util.Range; +import android.util.Rational; +import android.util.Size; +import org.junit.Before; +import org.junit.Test; + +public class CameraPropertiesImplTest { + private static final String CAMERA_NAME = "test_camera"; + private final CameraCharacteristics mockCharacteristics = mock(CameraCharacteristics.class); + private final CameraManager mockCameraManager = mock(CameraManager.class); + + private CameraPropertiesImpl cameraProperties; + + @Before + public void before() { + try { + when(mockCameraManager.getCameraCharacteristics(CAMERA_NAME)).thenReturn(mockCharacteristics); + cameraProperties = new CameraPropertiesImpl(CAMERA_NAME, mockCameraManager); + } catch (CameraAccessException e) { + fail(); + } + } + + @Test + public void ctor_Should_return_valid_instance() throws CameraAccessException { + verify(mockCameraManager, times(1)).getCameraCharacteristics(CAMERA_NAME); + assertNotNull(cameraProperties); + } + + @Test + @SuppressWarnings("unchecked") + public void getControlAutoExposureAvailableTargetFpsRangesTest() { + Range mockRange = mock(Range.class); + Range[] mockRanges = new Range[] {mockRange}; + when(mockCharacteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES)) + .thenReturn(mockRanges); + + Range[] actualRanges = + cameraProperties.getControlAutoExposureAvailableTargetFpsRanges(); + + verify(mockCharacteristics, times(1)) + .get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); + assertArrayEquals(actualRanges, mockRanges); + } + + @Test + @SuppressWarnings("unchecked") + public void getControlAutoExposureCompensationRangeTest() { + Range mockRange = mock(Range.class); + when(mockCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE)) + .thenReturn(mockRange); + + Range actualRange = cameraProperties.getControlAutoExposureCompensationRange(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_AE_COMPENSATION_RANGE); + assertEquals(actualRange, mockRange); + } + + @Test + public void + getControlAutoExposureCompensationStep_Should_return_double_When_rational_is_not_null() { + double expectedStep = 3.1415926535; + Rational mockRational = mock(Rational.class); + + when(mockCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP)) + .thenReturn(mockRational); + when(mockRational.doubleValue()).thenReturn(expectedStep); + + double actualSteps = cameraProperties.getControlAutoExposureCompensationStep(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + assertEquals(actualSteps, expectedStep, 0); + } + + @Test + public void getControlAutoExposureCompensationStep_Should_return_zero_When_rational_is_null() { + double expectedStep = 0.0; + + when(mockCharacteristics.get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP)) + .thenReturn(null); + + double actualSteps = cameraProperties.getControlAutoExposureCompensationStep(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_AE_COMPENSATION_STEP); + assertEquals(actualSteps, expectedStep, 0); + } + + @Test + public void getControlAutoFocusAvailableModesTest() { + int[] expectedAutoFocusModes = new int[] {0, 1, 2}; + when(mockCharacteristics.get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES)) + .thenReturn(expectedAutoFocusModes); + + int[] actualAutoFocusModes = cameraProperties.getControlAutoFocusAvailableModes(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES); + assertEquals(actualAutoFocusModes, expectedAutoFocusModes); + } + + @Test + public void getControlMaxRegionsAutoExposureTest() { + int expectedRegions = 42; + when(mockCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE)) + .thenReturn(expectedRegions); + + int actualRegions = cameraProperties.getControlMaxRegionsAutoExposure(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_MAX_REGIONS_AE); + assertEquals(actualRegions, expectedRegions); + } + + @Test + public void getControlMaxRegionsAutoFocusTest() { + int expectedRegions = 42; + when(mockCharacteristics.get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF)) + .thenReturn(expectedRegions); + + int actualRegions = cameraProperties.getControlMaxRegionsAutoFocus(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.CONTROL_MAX_REGIONS_AF); + assertEquals(actualRegions, expectedRegions); + } + + @Test + public void getDistortionCorrectionAvailableModesTest() { + int[] expectedCorrectionModes = new int[] {0, 1, 2}; + when(mockCharacteristics.get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES)) + .thenReturn(expectedCorrectionModes); + + int[] actualCorrectionModes = cameraProperties.getDistortionCorrectionAvailableModes(); + + verify(mockCharacteristics, times(1)) + .get(CameraCharacteristics.DISTORTION_CORRECTION_AVAILABLE_MODES); + assertEquals(actualCorrectionModes, expectedCorrectionModes); + } + + @Test + public void getFlashInfoAvailableTest() { + boolean expectedAvailability = true; + when(mockCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE)) + .thenReturn(expectedAvailability); + + boolean actualAvailability = cameraProperties.getFlashInfoAvailable(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.FLASH_INFO_AVAILABLE); + assertEquals(actualAvailability, expectedAvailability); + } + + @Test + public void getLensFacingTest() { + int expectedFacing = 42; + when(mockCharacteristics.get(CameraCharacteristics.LENS_FACING)).thenReturn(expectedFacing); + + int actualFacing = cameraProperties.getLensFacing(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.LENS_FACING); + assertEquals(actualFacing, expectedFacing); + } + + @Test + public void getLensInfoMinimumFocusDistanceTest() { + Float expectedFocusDistance = new Float(3.14); + when(mockCharacteristics.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE)) + .thenReturn(expectedFocusDistance); + + Float actualFocusDistance = cameraProperties.getLensInfoMinimumFocusDistance(); + + verify(mockCharacteristics, times(1)) + .get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE); + assertEquals(actualFocusDistance, expectedFocusDistance); + } + + @Test + public void getScalerAvailableMaxDigitalZoomTest() { + Float expectedDigitalZoom = new Float(3.14); + when(mockCharacteristics.get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM)) + .thenReturn(expectedDigitalZoom); + + Float actualDigitalZoom = cameraProperties.getScalerAvailableMaxDigitalZoom(); + + verify(mockCharacteristics, times(1)) + .get(CameraCharacteristics.SCALER_AVAILABLE_MAX_DIGITAL_ZOOM); + assertEquals(actualDigitalZoom, expectedDigitalZoom); + } + + @Test + public void getSensorInfoActiveArraySizeTest() { + Rect expectedArraySize = mock(Rect.class); + when(mockCharacteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)) + .thenReturn(expectedArraySize); + + Rect actualArraySize = cameraProperties.getSensorInfoActiveArraySize(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); + assertEquals(actualArraySize, expectedArraySize); + } + + @Test + public void getSensorInfoPixelArraySizeTest() { + Size expectedArraySize = mock(Size.class); + when(mockCharacteristics.get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE)) + .thenReturn(expectedArraySize); + + Size actualArraySize = cameraProperties.getSensorInfoPixelArraySize(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.SENSOR_INFO_PIXEL_ARRAY_SIZE); + assertEquals(actualArraySize, expectedArraySize); + } + + @Test + public void getSensorInfoPreCorrectionActiveArraySize() { + Rect expectedArraySize = mock(Rect.class); + when(mockCharacteristics.get( + CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE)) + .thenReturn(expectedArraySize); + + Rect actualArraySize = cameraProperties.getSensorInfoPreCorrectionActiveArraySize(); + + verify(mockCharacteristics, times(1)) + .get(CameraCharacteristics.SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE); + assertEquals(actualArraySize, expectedArraySize); + } + + @Test + public void getSensorOrientationTest() { + int expectedOrientation = 42; + when(mockCharacteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)) + .thenReturn(expectedOrientation); + + int actualOrientation = cameraProperties.getSensorOrientation(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.SENSOR_ORIENTATION); + assertEquals(actualOrientation, expectedOrientation); + } + + @Test + public void getHardwareLevelTest() { + int expectedLevel = 42; + when(mockCharacteristics.get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL)) + .thenReturn(expectedLevel); + + int actualLevel = cameraProperties.getHardwareLevel(); + + verify(mockCharacteristics, times(1)).get(CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL); + assertEquals(actualLevel, expectedLevel); + } + + @Test + public void getAvailableNoiseReductionModesTest() { + int[] expectedReductionModes = new int[] {0, 1, 2}; + when(mockCharacteristics.get( + CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES)) + .thenReturn(expectedReductionModes); + + int[] actualReductionModes = cameraProperties.getAvailableNoiseReductionModes(); + + verify(mockCharacteristics, times(1)) + .get(CameraCharacteristics.NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES); + assertEquals(actualReductionModes, expectedReductionModes); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java new file mode 100644 index 000000000000..b97192b889cf --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java @@ -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. + +package io.flutter.plugins.camera; + +import static org.junit.Assert.assertEquals; + +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import org.junit.Test; + +public class CameraUtilsTest { + + @Test + public void serializeDeviceOrientation_serializes_correctly() { + assertEquals( + "portraitUp", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_UP)); + assertEquals( + "portraitDown", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_DOWN)); + assertEquals( + "landscapeLeft", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT)); + assertEquals( + "landscapeRight", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT)); + } + + @Test(expected = UnsupportedOperationException.class) + public void serializeDeviceOrientation_throws_for_null() { + CameraUtils.serializeDeviceOrientation(null); + } + + @Test + public void deserializeDeviceOrientation_deserializes_correctly() { + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.deserializeDeviceOrientation("portraitUp")); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.deserializeDeviceOrientation("portraitDown")); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.deserializeDeviceOrientation("landscapeLeft")); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.deserializeDeviceOrientation("landscapeRight")); + } + + @Test(expected = UnsupportedOperationException.class) + public void deserializeDeviceOrientation_throws_for_null() { + CameraUtils.deserializeDeviceOrientation(null); + } + + @Test + public void getDeviceOrientationFromDegrees_converts_correctly() { + // Portrait UP + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.getDeviceOrientationFromDegrees(0)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.getDeviceOrientationFromDegrees(315)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.getDeviceOrientationFromDegrees(44)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.getDeviceOrientationFromDegrees(-45)); + // Portrait DOWN + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.getDeviceOrientationFromDegrees(180)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.getDeviceOrientationFromDegrees(135)); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.getDeviceOrientationFromDegrees(224)); + // Landscape LEFT + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.getDeviceOrientationFromDegrees(90)); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.getDeviceOrientationFromDegrees(45)); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.getDeviceOrientationFromDegrees(134)); + // Landscape RIGHT + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.getDeviceOrientationFromDegrees(270)); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.getDeviceOrientationFromDegrees(225)); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.getDeviceOrientationFromDegrees(314)); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java new file mode 100644 index 000000000000..1385c2e36949 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java @@ -0,0 +1,125 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import android.graphics.Rect; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class CameraZoomTest { + + @Test + public void ctor_when_parameters_are_valid() { + final Rect sensorSize = new Rect(0, 0, 0, 0); + final Float maxZoom = 4.0f; + final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom); + + assertNotNull(cameraZoom); + assertTrue(cameraZoom.hasSupport); + assertEquals(4.0f, cameraZoom.maxZoom, 0); + assertEquals(1.0f, CameraZoom.DEFAULT_ZOOM_FACTOR, 0); + } + + @Test + public void ctor_when_sensor_size_is_null() { + final Rect sensorSize = null; + final Float maxZoom = 4.0f; + final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom); + + assertNotNull(cameraZoom); + assertFalse(cameraZoom.hasSupport); + assertEquals(cameraZoom.maxZoom, 1.0f, 0); + } + + @Test + public void ctor_when_max_zoom_is_null() { + final Rect sensorSize = new Rect(0, 0, 0, 0); + final Float maxZoom = null; + final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom); + + assertNotNull(cameraZoom); + assertFalse(cameraZoom.hasSupport); + assertEquals(cameraZoom.maxZoom, 1.0f, 0); + } + + @Test + public void ctor_when_max_zoom_is_smaller_then_default_zoom_factor() { + final Rect sensorSize = new Rect(0, 0, 0, 0); + final Float maxZoom = 0.5f; + final CameraZoom cameraZoom = new CameraZoom(sensorSize, maxZoom); + + assertNotNull(cameraZoom); + assertFalse(cameraZoom.hasSupport); + assertEquals(cameraZoom.maxZoom, 1.0f, 0); + } + + @Test + public void setZoom_when_no_support_should_not_set_scaler_crop_region() { + final CameraZoom cameraZoom = new CameraZoom(null, null); + final Rect computedZoom = cameraZoom.computeZoom(2f); + + assertNull(computedZoom); + } + + @Test + public void setZoom_when_sensor_size_equals_zero_should_return_crop_region_of_zero() { + final Rect sensorSize = new Rect(0, 0, 0, 0); + final CameraZoom cameraZoom = new CameraZoom(sensorSize, 20f); + final Rect computedZoom = cameraZoom.computeZoom(18f); + + assertNotNull(computedZoom); + assertEquals(computedZoom.left, 0); + assertEquals(computedZoom.top, 0); + assertEquals(computedZoom.right, 0); + assertEquals(computedZoom.bottom, 0); + } + + @Test + public void setZoom_when_sensor_size_is_valid_should_return_crop_region() { + final Rect sensorSize = new Rect(0, 0, 100, 100); + final CameraZoom cameraZoom = new CameraZoom(sensorSize, 20f); + final Rect computedZoom = cameraZoom.computeZoom(18f); + + assertNotNull(computedZoom); + assertEquals(computedZoom.left, 48); + assertEquals(computedZoom.top, 48); + assertEquals(computedZoom.right, 52); + assertEquals(computedZoom.bottom, 52); + } + + @Test + public void setZoom_when_zoom_is_greater_then_max_zoom_clamp_to_max_zoom() { + final Rect sensorSize = new Rect(0, 0, 100, 100); + final CameraZoom cameraZoom = new CameraZoom(sensorSize, 10f); + final Rect computedZoom = cameraZoom.computeZoom(25f); + + assertNotNull(computedZoom); + assertEquals(computedZoom.left, 45); + assertEquals(computedZoom.top, 45); + assertEquals(computedZoom.right, 55); + assertEquals(computedZoom.bottom, 55); + } + + @Test + public void setZoom_when_zoom_is_smaller_then_min_zoom_clamp_to_min_zoom() { + final Rect sensorSize = new Rect(0, 0, 100, 100); + final CameraZoom cameraZoom = new CameraZoom(sensorSize, 10f); + final Rect computedZoom = cameraZoom.computeZoom(0.5f); + + assertNotNull(computedZoom); + assertEquals(computedZoom.left, 0); + assertEquals(computedZoom.top, 0); + assertEquals(computedZoom.right, 100); + assertEquals(computedZoom.bottom, 100); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java new file mode 100644 index 000000000000..25f5df9e9db9 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/DartMessengerTest.java @@ -0,0 +1,135 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import static junit.framework.TestCase.assertNull; +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; + +import android.os.Handler; +import androidx.annotation.NonNull; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.StandardMethodCodec; +import io.flutter.plugins.camera.types.ExposureMode; +import io.flutter.plugins.camera.types.FocusMode; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +public class DartMessengerTest { + /** A {@link BinaryMessenger} implementation that does nothing but save its messages. */ + private static class FakeBinaryMessenger implements BinaryMessenger { + private final List sentMessages = new ArrayList<>(); + + @Override + public void send(@NonNull String channel, ByteBuffer message) { + sentMessages.add(message); + } + + @Override + public void send(@NonNull String channel, ByteBuffer message, BinaryReply callback) { + send(channel, message); + } + + @Override + public void setMessageHandler(@NonNull String channel, BinaryMessageHandler handler) {} + + List getMessages() { + return new ArrayList<>(sentMessages); + } + } + + private Handler mockHandler; + private DartMessenger dartMessenger; + private FakeBinaryMessenger fakeBinaryMessenger; + + @Before + public void setUp() { + mockHandler = mock(Handler.class); + fakeBinaryMessenger = new FakeBinaryMessenger(); + dartMessenger = new DartMessenger(fakeBinaryMessenger, 0, mockHandler); + } + + @Test + public void sendCameraErrorEvent_includesErrorDescriptions() { + doAnswer(createPostHandlerAnswer()).when(mockHandler).post(any(Runnable.class)); + + dartMessenger.sendCameraErrorEvent("error description"); + List sentMessages = fakeBinaryMessenger.getMessages(); + + assertEquals(1, sentMessages.size()); + MethodCall call = decodeSentMessage(sentMessages.get(0)); + assertEquals("error", call.method); + assertEquals("error description", call.argument("description")); + } + + @Test + public void sendCameraInitializedEvent_includesPreviewSize() { + doAnswer(createPostHandlerAnswer()).when(mockHandler).post(any(Runnable.class)); + dartMessenger.sendCameraInitializedEvent(0, 0, ExposureMode.auto, FocusMode.auto, true, true); + + List sentMessages = fakeBinaryMessenger.getMessages(); + assertEquals(1, sentMessages.size()); + MethodCall call = decodeSentMessage(sentMessages.get(0)); + assertEquals("initialized", call.method); + assertEquals(0, (double) call.argument("previewWidth"), 0); + assertEquals(0, (double) call.argument("previewHeight"), 0); + assertEquals("ExposureMode auto", call.argument("exposureMode"), "auto"); + assertEquals("FocusMode continuous", call.argument("focusMode"), "auto"); + assertEquals("exposurePointSupported", call.argument("exposurePointSupported"), true); + assertEquals("focusPointSupported", call.argument("focusPointSupported"), true); + } + + @Test + public void sendCameraClosingEvent() { + doAnswer(createPostHandlerAnswer()).when(mockHandler).post(any(Runnable.class)); + dartMessenger.sendCameraClosingEvent(); + + List sentMessages = fakeBinaryMessenger.getMessages(); + assertEquals(1, sentMessages.size()); + MethodCall call = decodeSentMessage(sentMessages.get(0)); + assertEquals("camera_closing", call.method); + assertNull(call.argument("description")); + } + + @Test + public void sendDeviceOrientationChangedEvent() { + doAnswer(createPostHandlerAnswer()).when(mockHandler).post(any(Runnable.class)); + dartMessenger.sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation.PORTRAIT_UP); + + List sentMessages = fakeBinaryMessenger.getMessages(); + assertEquals(1, sentMessages.size()); + MethodCall call = decodeSentMessage(sentMessages.get(0)); + assertEquals("orientation_changed", call.method); + assertEquals(call.argument("orientation"), "portraitUp"); + } + + private static Answer createPostHandlerAnswer() { + return new Answer() { + @Override + public Boolean answer(InvocationOnMock invocation) throws Throwable { + Runnable runnable = invocation.getArgument(0, Runnable.class); + if (runnable != null) { + runnable.run(); + } + return true; + } + }; + } + + private MethodCall decodeSentMessage(ByteBuffer sentMessage) { + sentMessage.position(0); + + return StandardMethodCodec.INSTANCE.decodeMethodCall(sentMessage); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java new file mode 100644 index 000000000000..f257a7f7fd4b --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/PictureCaptureRequestTest.java @@ -0,0 +1,152 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import io.flutter.plugin.common.MethodChannel; +import org.junit.Test; + +public class PictureCaptureRequestTest { + + @Test + public void state_is_idle_by_default() { + PictureCaptureRequest req = new PictureCaptureRequest(null); + assertEquals("Default state is idle", req.getState(), PictureCaptureRequest.State.idle); + } + + @Test + public void setState_sets_state() { + PictureCaptureRequest req = new PictureCaptureRequest(null); + req.setState(PictureCaptureRequest.State.focusing); + assertEquals("State is focusing", req.getState(), PictureCaptureRequest.State.focusing); + req.setState(PictureCaptureRequest.State.preCapture); + assertEquals("State is preCapture", req.getState(), PictureCaptureRequest.State.preCapture); + req.setState(PictureCaptureRequest.State.waitingPreCaptureReady); + assertEquals( + "State is waitingPreCaptureReady", + req.getState(), + PictureCaptureRequest.State.waitingPreCaptureReady); + req.setState(PictureCaptureRequest.State.capturing); + assertEquals( + "State is awaitingPreCapture", req.getState(), PictureCaptureRequest.State.capturing); + } + + @Test + public void setState_resets_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + PictureCaptureRequest req = new PictureCaptureRequest(null, mockTimeoutHandler); + req.setState(PictureCaptureRequest.State.focusing); + req.setState(PictureCaptureRequest.State.preCapture); + req.setState(PictureCaptureRequest.State.waitingPreCaptureReady); + req.setState(PictureCaptureRequest.State.capturing); + verify(mockTimeoutHandler, times(4)).resetTimeout(any()); + verify(mockTimeoutHandler, never()).clearTimeout(any()); + } + + @Test + public void setState_clears_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + PictureCaptureRequest req = new PictureCaptureRequest(null, mockTimeoutHandler); + req.setState(PictureCaptureRequest.State.idle); + req.setState(PictureCaptureRequest.State.finished); + req = new PictureCaptureRequest(null, mockTimeoutHandler); + req.setState(PictureCaptureRequest.State.error); + verify(mockTimeoutHandler, never()).resetTimeout(any()); + verify(mockTimeoutHandler, times(3)).clearTimeout(any()); + } + + @Test + public void finish_sets_result_and_state() { + // Setup + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult); + // Act + req.finish("/test/path"); + // Test + verify(mockResult).success("/test/path"); + assertEquals("State is finished", req.getState(), PictureCaptureRequest.State.finished); + } + + @Test + public void finish_clears_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult, mockTimeoutHandler); + req.finish("/test/path"); + verify(mockTimeoutHandler, never()).resetTimeout(any()); + verify(mockTimeoutHandler).clearTimeout(any()); + } + + @Test + public void isFinished_is_true_When_state_is_finished_or_error() { + // Setup + PictureCaptureRequest req = new PictureCaptureRequest(null); + // Test false states + req.setState(PictureCaptureRequest.State.idle); + assertFalse(req.isFinished()); + req.setState(PictureCaptureRequest.State.preCapture); + assertFalse(req.isFinished()); + req.setState(PictureCaptureRequest.State.capturing); + assertFalse(req.isFinished()); + // Test true states + req.setState(PictureCaptureRequest.State.finished); + assertTrue(req.isFinished()); + req = new PictureCaptureRequest(null); // Refresh + req.setState(PictureCaptureRequest.State.error); + assertTrue(req.isFinished()); + } + + @Test(expected = IllegalStateException.class) + public void finish_throws_When_already_finished() { + // Setup + PictureCaptureRequest req = new PictureCaptureRequest(null); + req.setState(PictureCaptureRequest.State.finished); + // Act + req.finish("/test/path"); + } + + @Test + public void error_sets_result_and_state() { + // Setup + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult); + // Act + req.error("ERROR_CODE", "Error Message", null); + // Test + verify(mockResult).error("ERROR_CODE", "Error Message", null); + assertEquals("State is error", req.getState(), PictureCaptureRequest.State.error); + } + + @Test + public void error_clears_timeout() { + PictureCaptureRequest.TimeoutHandler mockTimeoutHandler = + mock(PictureCaptureRequest.TimeoutHandler.class); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + PictureCaptureRequest req = new PictureCaptureRequest(mockResult, mockTimeoutHandler); + req.error("ERROR_CODE", "Error Message", null); + verify(mockTimeoutHandler, never()).resetTimeout(any()); + verify(mockTimeoutHandler).clearTimeout(any()); + } + + @Test(expected = IllegalStateException.class) + public void error_throws_When_already_finished() { + // Setup + PictureCaptureRequest req = new PictureCaptureRequest(null); + req.setState(PictureCaptureRequest.State.finished); + // Act + req.error(null, null, null); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java new file mode 100644 index 000000000000..84e4ad0d0e91 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java @@ -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. + +package io.flutter.plugins.camera.features.autofocus; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CaptureRequest; +import io.flutter.plugins.camera.CameraProperties; +import org.junit.Test; + +public class AutoFocusFeatureTest { + private static final int[] FOCUS_MODES_ONLY_OFF = + new int[] {CameraCharacteristics.CONTROL_AF_MODE_OFF}; + private static final int[] FOCUS_MODES = + new int[] { + CameraCharacteristics.CONTROL_AF_MODE_OFF, CameraCharacteristics.CONTROL_AF_MODE_AUTO + }; + + @Test + public void getDebugName_should_return_the_name_of_the_feature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); + + assertEquals("AutoFocusFeature", autoFocusFeature.getDebugName()); + } + + @Test + public void getValue_should_return_auto_if_not_set() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); + + assertEquals(FocusMode.auto, autoFocusFeature.getValue()); + } + + @Test + public void getValue_should_echo_the_set_value() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); + FocusMode expectedValue = FocusMode.locked; + + autoFocusFeature.setValue(expectedValue); + FocusMode actualValue = autoFocusFeature.getValue(); + + assertEquals(expectedValue, actualValue); + } + + @Test + public void checkIsSupported_should_return_false_when_minimum_focus_distance_is_zero() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); + + when(mockCameraProperties.getControlAutoFocusAvailableModes()).thenReturn(FOCUS_MODES); + when(mockCameraProperties.getLensInfoMinimumFocusDistance()).thenReturn(0.0F); + + assertFalse(autoFocusFeature.checkIsSupported()); + } + + @Test + public void checkIsSupported_should_return_false_when_minimum_focus_distance_is_null() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); + + when(mockCameraProperties.getControlAutoFocusAvailableModes()).thenReturn(FOCUS_MODES); + when(mockCameraProperties.getLensInfoMinimumFocusDistance()).thenReturn(null); + + assertFalse(autoFocusFeature.checkIsSupported()); + } + + @Test + public void checkIsSupport_should_return_false_when_no_focus_modes_are_available() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); + + when(mockCameraProperties.getControlAutoFocusAvailableModes()).thenReturn(new int[] {}); + when(mockCameraProperties.getLensInfoMinimumFocusDistance()).thenReturn(1.0F); + + assertFalse(autoFocusFeature.checkIsSupported()); + } + + @Test + public void checkIsSupport_should_return_false_when_only_focus_off_is_available() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); + + when(mockCameraProperties.getControlAutoFocusAvailableModes()).thenReturn(FOCUS_MODES_ONLY_OFF); + when(mockCameraProperties.getLensInfoMinimumFocusDistance()).thenReturn(1.0F); + + assertFalse(autoFocusFeature.checkIsSupported()); + } + + @Test + public void checkIsSupport_should_return_true_when_only_multiple_focus_modes_are_available() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); + + when(mockCameraProperties.getControlAutoFocusAvailableModes()).thenReturn(FOCUS_MODES); + when(mockCameraProperties.getLensInfoMinimumFocusDistance()).thenReturn(1.0F); + + assertTrue(autoFocusFeature.checkIsSupported()); + } + + @Test + public void updateBuilder_should_return_when_checkIsSupported_is_false() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); + + when(mockCameraProperties.getControlAutoFocusAvailableModes()).thenReturn(FOCUS_MODES); + when(mockCameraProperties.getLensInfoMinimumFocusDistance()).thenReturn(0.0F); + + autoFocusFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, never()).set(any(), any()); + } + + @Test + public void updateBuilder_should_set_control_mode_to_auto_when_focus_is_locked() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); + + when(mockCameraProperties.getControlAutoFocusAvailableModes()).thenReturn(FOCUS_MODES); + when(mockCameraProperties.getLensInfoMinimumFocusDistance()).thenReturn(1.0F); + + autoFocusFeature.setValue(FocusMode.locked); + autoFocusFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)) + .set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_AUTO); + } + + @Test + public void + updateBuilder_should_set_control_mode_to_continuous_video_when_focus_is_auto_and_recording_video() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, true); + + when(mockCameraProperties.getControlAutoFocusAvailableModes()).thenReturn(FOCUS_MODES); + when(mockCameraProperties.getLensInfoMinimumFocusDistance()).thenReturn(1.0F); + + autoFocusFeature.setValue(FocusMode.auto); + autoFocusFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)) + .set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_VIDEO); + } + + @Test + public void + updateBuilder_should_set_control_mode_to_continuous_video_when_focus_is_auto_and_not_recording_video() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); + + when(mockCameraProperties.getControlAutoFocusAvailableModes()).thenReturn(FOCUS_MODES); + when(mockCameraProperties.getLensInfoMinimumFocusDistance()).thenReturn(1.0F); + + autoFocusFeature.setValue(FocusMode.auto); + autoFocusFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)) + .set(CaptureRequest.CONTROL_AF_MODE, CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/FocusModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/FocusModeTest.java new file mode 100644 index 000000000000..70d52d458d4d --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/FocusModeTest.java @@ -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. + +package io.flutter.plugins.camera.features.autofocus; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class FocusModeTest { + + @Test + public void getValueForString_returns_correct_values() { + assertEquals( + "Returns FocusMode.auto for 'auto'", FocusMode.getValueForString("auto"), FocusMode.auto); + assertEquals( + "Returns FocusMode.locked for 'locked'", + FocusMode.getValueForString("locked"), + FocusMode.locked); + } + + @Test + public void getValueForString_returns_null_for_nonexistant_value() { + assertEquals( + "Returns null for 'nonexistant'", FocusMode.getValueForString("nonexistant"), null); + } + + @Test + public void toString_returns_correct_value() { + assertEquals("Returns 'auto' for FocusMode.auto", FocusMode.auto.toString(), "auto"); + assertEquals("Returns 'locked' for FocusMode.locked", FocusMode.locked.toString(), "locked"); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java new file mode 100644 index 000000000000..d9e0a8d69c96 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java @@ -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. + +package io.flutter.plugins.camera.features.exposurelock; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.hardware.camera2.CaptureRequest; +import io.flutter.plugins.camera.CameraProperties; +import org.junit.Test; + +public class ExposureLockFeatureTest { + @Test + public void getDebugName_should_return_the_name_of_the_feature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); + + assertEquals("ExposureLockFeature", exposureLockFeature.getDebugName()); + } + + @Test + public void getValue_should_return_auto_if_not_set() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); + + assertEquals(ExposureMode.auto, exposureLockFeature.getValue()); + } + + @Test + public void getValue_should_echo_the_set_value() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); + ExposureMode expectedValue = ExposureMode.locked; + + exposureLockFeature.setValue(expectedValue); + ExposureMode actualValue = exposureLockFeature.getValue(); + + assertEquals(expectedValue, actualValue); + } + + @Test + public void checkIsSupported_should_return_true() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); + + assertTrue(exposureLockFeature.checkIsSupported()); + } + + @Test + public void + updateBuilder_should_set_control_ae_lock_to_false_when_auto_exposure_is_set_to_auto() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); + + exposureLockFeature.setValue(ExposureMode.auto); + exposureLockFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)).set(CaptureRequest.CONTROL_AE_LOCK, false); + } + + @Test + public void + updateBuilder_should_set_control_ae_lock_to_false_when_auto_exposure_is_set_to_locked() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); + + exposureLockFeature.setValue(ExposureMode.locked); + exposureLockFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)).set(CaptureRequest.CONTROL_AE_LOCK, true); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureModeTest.java new file mode 100644 index 000000000000..ad1d3d98f295 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureModeTest.java @@ -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. + +package io.flutter.plugins.camera.features.exposurelock; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class ExposureModeTest { + + @Test + public void getValueForString_returns_correct_values() { + assertEquals( + "Returns ExposureMode.auto for 'auto'", + ExposureMode.getValueForString("auto"), + ExposureMode.auto); + assertEquals( + "Returns ExposureMode.locked for 'locked'", + ExposureMode.getValueForString("locked"), + ExposureMode.locked); + } + + @Test + public void getValueForString_returns_null_for_nonexistant_value() { + assertEquals( + "Returns null for 'nonexistant'", ExposureMode.getValueForString("nonexistant"), null); + } + + @Test + public void toString_returns_correct_value() { + assertEquals("Returns 'auto' for ExposureMode.auto", ExposureMode.auto.toString(), "auto"); + assertEquals( + "Returns 'locked' for ExposureMode.locked", ExposureMode.locked.toString(), "locked"); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java new file mode 100644 index 000000000000..40d17fdc496e --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.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.camera.features.exposureoffset; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +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.hardware.camera2.CaptureRequest; +import io.flutter.plugins.camera.CameraProperties; +import org.junit.Test; + +public class ExposureOffsetFeatureTest { + @Test + public void getDebugName_should_return_the_name_of_the_feature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); + + assertEquals("ExposureOffsetFeature", exposureOffsetFeature.getDebugName()); + } + + @Test + public void getValue_should_return_zero_if_not_set() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); + + final double actualValue = exposureOffsetFeature.getValue(); + + assertEquals(0.0, actualValue, 0); + } + + @Test + public void getValue_should_echo_the_set_value() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); + double expectedValue = 4.0; + + when(mockCameraProperties.getControlAutoExposureCompensationStep()).thenReturn(0.5); + + exposureOffsetFeature.setValue(2.0); + double actualValue = exposureOffsetFeature.getValue(); + + assertEquals(expectedValue, actualValue, 0); + } + + @Test + public void + getExposureOffsetStepSize_should_return_the_control_exposure_compensation_step_value() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); + + when(mockCameraProperties.getControlAutoExposureCompensationStep()).thenReturn(0.5); + + assertEquals(0.5, exposureOffsetFeature.getExposureOffsetStepSize(), 0); + } + + @Test + public void checkIsSupported_should_return_true() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); + + assertTrue(exposureOffsetFeature.checkIsSupported()); + } + + @Test + public void updateBuilder_should_set_control_ae_exposure_compensation_to_offset() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); + + when(mockCameraProperties.getControlAutoExposureCompensationStep()).thenReturn(0.5); + + exposureOffsetFeature.setValue(2.0); + exposureOffsetFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)).set(CaptureRequest.CONTROL_AE_EXPOSURE_COMPENSATION, 4); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java new file mode 100644 index 000000000000..0aedc59ef635 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java @@ -0,0 +1,196 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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.exposurepoint; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.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.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.MeteringRectangle; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.types.CameraRegions; +import org.junit.Test; + +public class ExposurePointFeatureTest { + @Test + public void getDebugName_should_return_the_name_of_the_feature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CameraRegions mockCameraRegions = mock(CameraRegions.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockCameraRegions); + + assertEquals("ExposurePointFeature", exposurePointFeature.getDebugName()); + } + + @Test + public void getValue_should_return_default_point_if_not_set() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CameraRegions mockCameraRegions = mock(CameraRegions.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockCameraRegions); + Point expectedPoint = new Point(0.0, 0.0); + Point actualPoint = exposurePointFeature.getValue(); + + assertEquals(expectedPoint.x, actualPoint.x); + assertEquals(expectedPoint.y, actualPoint.y); + } + + @Test + public void getValue_should_echo_the_set_value() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CameraRegions mockCameraRegions = mock(CameraRegions.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockCameraRegions); + Point expectedPoint = new Point(0.0, 0.0); + + exposurePointFeature.setValue(expectedPoint); + Point actualPoint = exposurePointFeature.getValue(); + + assertEquals(expectedPoint, actualPoint); + } + + @Test + public void setValue_should_reset_point_when_x_coord_is_null() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CameraRegions mockCameraRegions = mock(CameraRegions.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockCameraRegions); + + exposurePointFeature.setValue(new Point(null, 0.0)); + + verify(mockCameraRegions, times(1)).resetAutoExposureMeteringRectangle(); + } + + @Test + public void setValue_should_reset_point_when_y_coord_is_null() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CameraRegions mockCameraRegions = mock(CameraRegions.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockCameraRegions); + + exposurePointFeature.setValue(new Point(0.0, null)); + + verify(mockCameraRegions, times(1)).resetAutoExposureMeteringRectangle(); + } + + @Test + public void setValue_should_reset_point_when_valid_coords_are_supplied() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CameraRegions mockCameraRegions = mock(CameraRegions.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockCameraRegions); + Point point = new Point(0.0, 0.0); + + exposurePointFeature.setValue(point); + + verify(mockCameraRegions, times(1)).setAutoExposureMeteringRectangleFromPoint(point.x, point.y); + } + + @Test + public void checkIsSupported_should_return_false_when_max_regions_is_null() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, null); + + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(null); + + assertFalse(exposurePointFeature.checkIsSupported()); + } + + @Test + public void checkIsSupported_should_return_false_when_max_regions_is_zero() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, null); + + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(0); + + assertFalse(exposurePointFeature.checkIsSupported()); + } + + @Test + public void checkIsSupported_should_return_true_when_max_regions_is_bigger_then_zero() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, null); + + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); + + assertTrue(exposurePointFeature.checkIsSupported()); + } + + @Test + public void updateBuilder_should_return_when_checkIsSupported_is_false() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CameraRegions mockCameraRegions = mock(CameraRegions.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockCameraRegions); + + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(0); + + exposurePointFeature.updateBuilder(null); + + verify(mockCameraRegions, never()).getAEMeteringRectangle(); + } + + @Test + public void updateBuilder_should_set_ae_regions_to_null_when_ae_metering_rectangle_is_null() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CameraRegions mockCameraRegions = mock(CameraRegions.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockCameraRegions); + + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); + when(mockCameraRegions.getAEMeteringRectangle()).thenReturn(null); + + exposurePointFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)).set(CaptureRequest.CONTROL_AE_REGIONS, null); + } + + @Test + public void updateBuilder_should_set_ae_regions_with_metering_rectangle() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CameraRegions mockCameraRegions = mock(CameraRegions.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockCameraRegions); + MeteringRectangle meteringRectangle = new MeteringRectangle(0, 0, 0, 0, 0); + + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); + when(mockCameraRegions.getAEMeteringRectangle()).thenReturn(meteringRectangle); + + exposurePointFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)) + .set(eq(CaptureRequest.CONTROL_AE_REGIONS), any(MeteringRectangle[].class)); + } + + @Test + public void updateBuilder_should_silently_fail_when_exception_occurs() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CameraRegions mockCameraRegions = mock(CameraRegions.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockCameraRegions); + + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); + when(mockCameraRegions.getAEMeteringRectangle()).thenThrow(new IllegalArgumentException()); + + exposurePointFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)).set(CaptureRequest.CONTROL_AE_REGIONS, null); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java new file mode 100644 index 000000000000..eccfb07993c1 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/flash/FlashFeatureTest.java @@ -0,0 +1,156 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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.flash; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.camera2.CaptureRequest; +import io.flutter.plugins.camera.CameraProperties; +import org.junit.Test; + +public class FlashFeatureTest { + @Test + public void getDebugName_should_return_the_name_of_the_feature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + assertEquals("FlashFeature", flashFeature.getDebugName()); + } + + @Test + public void getValue_should_return_auto_if_not_set() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + assertEquals(FlashMode.auto, flashFeature.getValue()); + } + + @Test + public void getValue_should_echo_the_set_value() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + FlashMode expectedValue = FlashMode.torch; + + flashFeature.setValue(expectedValue); + FlashMode actualValue = flashFeature.getValue(); + + assertEquals(expectedValue, actualValue); + } + + @Test + public void checkIsSupported_should_return_false_when_flash_info_available_is_null() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + when(mockCameraProperties.getFlashInfoAvailable()).thenReturn(null); + + assertFalse(flashFeature.checkIsSupported()); + } + + @Test + public void checkIsSupported_should_return_false_when_flash_info_available_is_false() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + when(mockCameraProperties.getFlashInfoAvailable()).thenReturn(false); + + assertFalse(flashFeature.checkIsSupported()); + } + + @Test + public void checkIsSupported_should_return_true_when_flash_info_available_is_true() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + when(mockCameraProperties.getFlashInfoAvailable()).thenReturn(true); + + assertTrue(flashFeature.checkIsSupported()); + } + + @Test + public void updateBuilder_should_return_when_checkIsSupported_is_false() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + when(mockCameraProperties.getFlashInfoAvailable()).thenReturn(false); + + flashFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, never()).set(any(), any()); + } + + @Test + public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_off() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + when(mockCameraProperties.getFlashInfoAvailable()).thenReturn(true); + + flashFeature.setValue(FlashMode.off); + flashFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)) + .set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + verify(mockBuilder, times(1)).set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + } + + @Test + public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_always() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + when(mockCameraProperties.getFlashInfoAvailable()).thenReturn(true); + + flashFeature.setValue(FlashMode.always); + flashFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)) + .set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH); + verify(mockBuilder, times(1)).set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + } + + @Test + public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_torch() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + when(mockCameraProperties.getFlashInfoAvailable()).thenReturn(true); + + flashFeature.setValue(FlashMode.torch); + flashFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)) + .set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON); + verify(mockBuilder, times(1)).set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_TORCH); + } + + @Test + public void updateBuilder_should_set_ae_mode_and_flash_mode_when_flash_mode_is_auto() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + when(mockCameraProperties.getFlashInfoAvailable()).thenReturn(true); + + flashFeature.setValue(FlashMode.auto); + flashFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)) + .set(CaptureRequest.CONTROL_AE_MODE, CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH); + verify(mockBuilder, times(1)).set(CaptureRequest.FLASH_MODE, CaptureRequest.FLASH_MODE_OFF); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeatureTest.java new file mode 100644 index 000000000000..c76708a3769e --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomLevelFeatureTest.java @@ -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. + +package io.flutter.plugins.camera.features.zoomlevel; + +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.anyFloat; +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.graphics.Rect; +import android.hardware.camera2.CaptureRequest; +import io.flutter.plugins.camera.CameraProperties; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; + +public class ZoomLevelFeatureTest { + private MockedStatic mockedStaticCameraZoom; + private CameraProperties mockCameraProperties; + private ZoomUtils mockCameraZoom; + private Rect mockZoomArea; + private Rect mockSensorArray; + + @Before + public void before() { + mockedStaticCameraZoom = mockStatic(ZoomUtils.class); + mockCameraProperties = mock(CameraProperties.class); + mockCameraZoom = mock(ZoomUtils.class); + mockZoomArea = mock(Rect.class); + mockSensorArray = mock(Rect.class); + + mockedStaticCameraZoom + .when(() -> ZoomUtils.computeZoom(anyFloat(), any(), anyFloat(), anyFloat())) + .thenReturn(mockZoomArea); + } + + @After + public void after() { + mockedStaticCameraZoom.close(); + } + + @Test + public void ctor_when_parameters_are_valid() { + when(mockCameraProperties.getSensorInfoActiveArraySize()).thenReturn(mockSensorArray); + when(mockCameraProperties.getScalerAvailableMaxDigitalZoom()).thenReturn(42f); + + final ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); + + verify(mockCameraProperties, times(1)).getSensorInfoActiveArraySize(); + verify(mockCameraProperties, times(1)).getScalerAvailableMaxDigitalZoom(); + assertNotNull(zoomLevelFeature); + assertEquals(42f, zoomLevelFeature.getMaximumZoomLevel(), 0); + } + + @Test + public void ctor_when_sensor_size_is_null() { + when(mockCameraProperties.getSensorInfoActiveArraySize()).thenReturn(null); + when(mockCameraProperties.getScalerAvailableMaxDigitalZoom()).thenReturn(42f); + + final ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); + + verify(mockCameraProperties, times(1)).getSensorInfoActiveArraySize(); + verify(mockCameraProperties, never()).getScalerAvailableMaxDigitalZoom(); + assertNotNull(zoomLevelFeature); + assertFalse(zoomLevelFeature.checkIsSupported()); + assertEquals(zoomLevelFeature.getMaximumZoomLevel(), 1.0f, 0); + } + + @Test + public void ctor_when_max_zoom_is_null() { + when(mockCameraProperties.getSensorInfoActiveArraySize()).thenReturn(mockSensorArray); + when(mockCameraProperties.getScalerAvailableMaxDigitalZoom()).thenReturn(null); + + final ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); + + verify(mockCameraProperties, times(1)).getSensorInfoActiveArraySize(); + verify(mockCameraProperties, times(1)).getScalerAvailableMaxDigitalZoom(); + assertNotNull(zoomLevelFeature); + assertFalse(zoomLevelFeature.checkIsSupported()); + assertEquals(zoomLevelFeature.getMaximumZoomLevel(), 1.0f, 0); + } + + @Test + public void ctor_when_max_zoom_is_smaller_then_default_zoom_factor() { + when(mockCameraProperties.getSensorInfoActiveArraySize()).thenReturn(mockSensorArray); + when(mockCameraProperties.getScalerAvailableMaxDigitalZoom()).thenReturn(0.5f); + + final ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); + + verify(mockCameraProperties, times(1)).getSensorInfoActiveArraySize(); + verify(mockCameraProperties, times(1)).getScalerAvailableMaxDigitalZoom(); + assertNotNull(zoomLevelFeature); + assertFalse(zoomLevelFeature.checkIsSupported()); + assertEquals(zoomLevelFeature.getMaximumZoomLevel(), 1.0f, 0); + } + + @Test + public void getDebugName_should_return_the_name_of_the_feature() { + ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); + + assertEquals("ZoomLevelFeature", zoomLevelFeature.getDebugName()); + } + + @Test + public void getValue_should_return_null_if_not_set() { + ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); + + assertEquals(1.0, (float) zoomLevelFeature.getValue(), 0); + } + + @Test + public void getValue_should_echo_setValue() { + ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); + + zoomLevelFeature.setValue(2.3f); + + assertEquals(2.3f, (float) zoomLevelFeature.getValue(), 0); + } + + @Test + public void checkIsSupport_returns_false_by_default() { + ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); + + assertFalse(zoomLevelFeature.checkIsSupported()); + } + + @Test + public void updateBuilder_should_set_scalar_crop_region_when_checkIsSupport_is_true() { + when(mockCameraProperties.getSensorInfoActiveArraySize()).thenReturn(mockSensorArray); + when(mockCameraProperties.getScalerAvailableMaxDigitalZoom()).thenReturn(42f); + + ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + + zoomLevelFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, times(1)).set(CaptureRequest.SCALER_CROP_REGION, mockZoomArea); + } + + @Test + public void getMinimumZoomLevel() { + ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); + + assertEquals(1.0f, zoomLevelFeature.getMinimumZoomLevel(), 0); + } + + @Test + public void getMaximumZoomLevel() { + when(mockCameraProperties.getSensorInfoActiveArraySize()).thenReturn(mockSensorArray); + when(mockCameraProperties.getScalerAvailableMaxDigitalZoom()).thenReturn(42f); + + ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); + + assertEquals(42f, zoomLevelFeature.getMaximumZoomLevel(), 0); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtilsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtilsTest.java new file mode 100644 index 000000000000..f83e5fb11e08 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/zoomlevel/ZoomUtilsTest.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.camera.features.zoomlevel; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import android.graphics.Rect; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class ZoomUtilsTest { + @Test + public void setZoom_when_sensor_size_equals_zero_should_return_crop_region_of_zero() { + final Rect sensorSize = new Rect(0, 0, 0, 0); + final Rect computedZoom = ZoomUtils.computeZoom(18f, sensorSize, 1f, 20f); + + assertNotNull(computedZoom); + assertEquals(computedZoom.left, 0); + assertEquals(computedZoom.top, 0); + assertEquals(computedZoom.right, 0); + assertEquals(computedZoom.bottom, 0); + } + + @Test + public void setZoom_when_sensor_size_is_valid_should_return_crop_region() { + final Rect sensorSize = new Rect(0, 0, 100, 100); + final Rect computedZoom = ZoomUtils.computeZoom(18f, sensorSize, 1f, 20f); + + assertNotNull(computedZoom); + assertEquals(computedZoom.left, 48); + assertEquals(computedZoom.top, 48); + assertEquals(computedZoom.right, 52); + assertEquals(computedZoom.bottom, 52); + } + + @Test + public void setZoom_when_zoom_is_greater_then_max_zoom_clamp_to_max_zoom() { + final Rect sensorSize = new Rect(0, 0, 100, 100); + final Rect computedZoom = ZoomUtils.computeZoom(25f, sensorSize, 1f, 10f); + + assertNotNull(computedZoom); + assertEquals(computedZoom.left, 45); + assertEquals(computedZoom.top, 45); + assertEquals(computedZoom.right, 55); + assertEquals(computedZoom.bottom, 55); + } + + @Test + public void setZoom_when_zoom_is_smaller_then_min_zoom_clamp_to_min_zoom() { + final Rect sensorSize = new Rect(0, 0, 100, 100); + final Rect computedZoom = ZoomUtils.computeZoom(0.5f, sensorSize, 1f, 10f); + + assertNotNull(computedZoom); + assertEquals(computedZoom.left, 0); + assertEquals(computedZoom.top, 0); + assertEquals(computedZoom.right, 100); + assertEquals(computedZoom.bottom, 100); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java new file mode 100644 index 000000000000..9b8b54cc959c --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/media/MediaRecorderBuilderTest.java @@ -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. + +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_Should_set_values_in_correct_order_When_audio_is_disabled() 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_Should_set_values_in_correct_order_When_audio_is_enabled() 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/android/src/test/java/io/flutter/plugins/camera/types/CameraRegionsFactoryTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/CameraRegionsFactoryTest.java new file mode 100644 index 000000000000..5fa0c2c4a2a4 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/CameraRegionsFactoryTest.java @@ -0,0 +1,201 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.types; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.hardware.camera2.CaptureRequest; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.util.Size; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.utils.TestUtils; +import org.junit.Before; +import org.junit.Test; + +public class CameraRegionsFactoryTest { + private Size mockSize; + + @Before + public void before() { + mockSize = mock(Size.class); + + when(mockSize.getHeight()).thenReturn(640); + when(mockSize.getWidth()).thenReturn(480); + } + + @Test + public void + create_should_initialize_with_sensor_info_pixel_array_size_when_running_pre_android_p() { + updateSdkVersion(VERSION_CODES.O_MR1); + + try { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + + when(mockCameraProperties.getSensorInfoPixelArraySize()).thenReturn(mockSize); + + CameraRegions cameraRegions = CameraRegions.Factory.create(mockCameraProperties, mockBuilder); + + assertEquals(mockSize, cameraRegions.getBoundaries()); + verify(mockCameraProperties, never()).getSensorInfoPreCorrectionActiveArraySize(); + verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); + } finally { + updateSdkVersion(0); + } + } + + @Test + public void + create_should_initialize_with_sensor_info_pixel_array_size_when_distortion_correction_is_null() { + updateSdkVersion(VERSION_CODES.P); + + try { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + + when(mockCameraProperties.getDistortionCorrectionAvailableModes()).thenReturn(null); + when(mockCameraProperties.getSensorInfoPixelArraySize()).thenReturn(mockSize); + + CameraRegions cameraRegions = CameraRegions.Factory.create(mockCameraProperties, mockBuilder); + + assertEquals(mockSize, cameraRegions.getBoundaries()); + verify(mockCameraProperties, never()).getSensorInfoPreCorrectionActiveArraySize(); + verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); + } finally { + updateSdkVersion(0); + } + } + + @Test + public void + create_should_initialize_with_sensor_info_pixel_array_size_when_distortion_correction_is_off() { + updateSdkVersion(VERSION_CODES.P); + + try { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + + when(mockCameraProperties.getDistortionCorrectionAvailableModes()) + .thenReturn(new int[] {CaptureRequest.DISTORTION_CORRECTION_MODE_OFF}); + when(mockCameraProperties.getSensorInfoPixelArraySize()).thenReturn(mockSize); + + CameraRegions cameraRegions = CameraRegions.Factory.create(mockCameraProperties, mockBuilder); + + assertEquals(mockSize, cameraRegions.getBoundaries()); + verify(mockCameraProperties, never()).getSensorInfoPreCorrectionActiveArraySize(); + verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); + } finally { + updateSdkVersion(0); + } + } + + @Test + public void + create_should_initialize_with_sensor_info_pre_correction_active_array_size_when_distortion_correction_mode_is_set_to_null() { + updateSdkVersion(VERSION_CODES.P); + + try { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + + when(mockCameraProperties.getDistortionCorrectionAvailableModes()) + .thenReturn( + new int[] { + CaptureRequest.DISTORTION_CORRECTION_MODE_OFF, + CaptureRequest.DISTORTION_CORRECTION_MODE_FAST + }); + + when(mockBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE)).thenReturn(null); + when(mockCameraProperties.getSensorInfoPreCorrectionActiveArraySize()).thenReturn(null); + + CameraRegions cameraRegions = CameraRegions.Factory.create(mockCameraProperties, mockBuilder); + + assertNull(cameraRegions.getBoundaries()); + verify(mockCameraProperties, never()).getSensorInfoPixelArraySize(); + verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); + } finally { + updateSdkVersion(0); + } + } + + @Test + public void + create_should_initialize_with_sensor_info_pre_correction_active_array_size_when_distortion_correction_mode_is_set_off() { + updateSdkVersion(VERSION_CODES.P); + + try { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + + when(mockCameraProperties.getDistortionCorrectionAvailableModes()) + .thenReturn( + new int[] { + CaptureRequest.DISTORTION_CORRECTION_MODE_OFF, + CaptureRequest.DISTORTION_CORRECTION_MODE_FAST + }); + + when(mockBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE)) + .thenReturn(CaptureRequest.DISTORTION_CORRECTION_MODE_OFF); + when(mockCameraProperties.getSensorInfoPreCorrectionActiveArraySize()).thenReturn(null); + + CameraRegions cameraRegions = CameraRegions.Factory.create(mockCameraProperties, mockBuilder); + + assertNull(cameraRegions.getBoundaries()); + verify(mockCameraProperties, never()).getSensorInfoPixelArraySize(); + verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); + } finally { + updateSdkVersion(0); + } + } + + @Test + public void + ctor_should_initialize_with_sensor_info_active_array_size_when_distortion_correction_mode_is_set() { + updateSdkVersion(VERSION_CODES.P); + + try { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + + when(mockCameraProperties.getDistortionCorrectionAvailableModes()) + .thenReturn( + new int[] { + CaptureRequest.DISTORTION_CORRECTION_MODE_OFF, + CaptureRequest.DISTORTION_CORRECTION_MODE_FAST + }); + + when(mockBuilder.get(CaptureRequest.DISTORTION_CORRECTION_MODE)) + .thenReturn(CaptureRequest.DISTORTION_CORRECTION_MODE_FAST); + when(mockCameraProperties.getSensorInfoActiveArraySize()).thenReturn(null); + + CameraRegions cameraRegions = CameraRegions.Factory.create(mockCameraProperties, mockBuilder); + + assertNull(cameraRegions.getBoundaries()); + verify(mockCameraProperties, never()).getSensorInfoPixelArraySize(); + verify(mockCameraProperties, never()).getSensorInfoPreCorrectionActiveArraySize(); + } finally { + updateSdkVersion(0); + } + } + + @Test + public void getBoundaries_should_return_null_if_not_set() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + CameraRegions cameraRegions = CameraRegions.Factory.create(mockCameraProperties, mockBuilder); + + assertNull(cameraRegions.getBoundaries()); + } + + private static void updateSdkVersion(int version) { + TestUtils.setFinalStatic(VERSION.class, "SDK_INT", version); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/CameraRegionsTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/CameraRegionsTest.java new file mode 100644 index 000000000000..b760e1e9ca29 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/CameraRegionsTest.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.types; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import android.hardware.camera2.params.MeteringRectangle; +import android.util.Size; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class CameraRegionsTest { + io.flutter.plugins.camera.types.CameraRegions cameraRegions; + + @Before + public void setUp() { + this.cameraRegions = new io.flutter.plugins.camera.types.CameraRegions(new Size(100, 100)); + } + + @Test(expected = AssertionError.class) + public void getMeteringRectangleForPoint_should_throw_for_x_upper_bound() { + cameraRegions.convertPointToMeteringRectangle(1.5, 0); + } + + @Test(expected = AssertionError.class) + public void getMeteringRectangleForPoint_should_throw_for_x_lower_bound() { + cameraRegions.convertPointToMeteringRectangle(-0.5, 0); + } + + @Test(expected = AssertionError.class) + public void getMeteringRectangleForPoint_should_throw_for_y_upper_bound() { + cameraRegions.convertPointToMeteringRectangle(0, 1.5); + } + + @Test(expected = AssertionError.class) + public void getMeteringRectangleForPoint_should_throw_for_y_lower_bound() { + cameraRegions.convertPointToMeteringRectangle(0, -0.5); + } + + @Test + public void getMeteringRectangleForPoint_should_return_valid_MeteringRectangle() { + MeteringRectangle r; + // Center + r = cameraRegions.convertPointToMeteringRectangle(0.5, 0.5); + assertEquals(new MeteringRectangle(45, 45, 10, 10, 1), r); + + // Top left + r = cameraRegions.convertPointToMeteringRectangle(0.0, 0.0); + assertEquals(new MeteringRectangle(0, 0, 10, 10, 1), r); + + // Bottom right + r = cameraRegions.convertPointToMeteringRectangle(1.0, 1.0); + assertEquals(new MeteringRectangle(89, 89, 10, 10, 1), r); + + // Top left + r = cameraRegions.convertPointToMeteringRectangle(0.0, 1.0); + assertEquals(new MeteringRectangle(0, 89, 10, 10, 1), r); + + // Top right + r = cameraRegions.convertPointToMeteringRectangle(1.0, 0.0); + assertEquals(new MeteringRectangle(89, 0, 10, 10, 1), r); + } + + @Test(expected = AssertionError.class) + public void constructor_should_throw_for_0_width_boundary() { + new io.flutter.plugins.camera.CameraRegions(new Size(0, 50)); + } + + @Test(expected = AssertionError.class) + public void constructor_should_throw_for_0_height_boundary() { + new io.flutter.plugins.camera.CameraRegions(new Size(100, 0)); + } + + @Test + public void setAutoExposureMeteringRectangleFromPoint_should_set_aeMeteringRectangle_for_point() { + cameraRegions.setAutoExposureMeteringRectangleFromPoint(0, 0); + assertEquals(new MeteringRectangle(0, 0, 10, 10, 1), cameraRegions.getAEMeteringRectangle()); + } + + @Test + public void resetAutoExposureMeteringRectangle_should_reset_aeMeteringRectangle() { + io.flutter.plugins.camera.types.CameraRegions cr = + new io.flutter.plugins.camera.types.CameraRegions(new Size(100, 50)); + cr.setAutoExposureMeteringRectangleFromPoint(0, 0); + assertNotNull(cr.getAEMeteringRectangle()); + cr.resetAutoExposureMeteringRectangle(); + assertNull(cr.getAEMeteringRectangle()); + } + + @Test + public void setAutoFocusMeteringRectangleFromPoint_should_set_afMeteringRectangle_for_point() { + io.flutter.plugins.camera.types.CameraRegions cr = + new io.flutter.plugins.camera.types.CameraRegions(new Size(100, 50)); + cr.setAutoFocusMeteringRectangleFromPoint(0, 0); + assertEquals(new MeteringRectangle(0, 0, 10, 5, 1), cr.getAFMeteringRectangle()); + } + + @Test + public void resetAutoFocusMeteringRectangle_should_reset_afMeteringRectangle() { + io.flutter.plugins.camera.types.CameraRegions cr = + new io.flutter.plugins.camera.types.CameraRegions(new Size(100, 50)); + cr.setAutoFocusMeteringRectangleFromPoint(0, 0); + assertNotNull(cr.getAFMeteringRectangle()); + cr.resetAutoFocusMeteringRectangle(); + assertNull(cr.getAFMeteringRectangle()); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java new file mode 100644 index 000000000000..5f4bd9f89ec7 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/ExposureModeTest.java @@ -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. + +package io.flutter.plugins.camera.types; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class ExposureModeTest { + + @Test + public void getValueForString_returns_correct_values() { + assertEquals( + "Returns ExposureMode.auto for 'auto'", + ExposureMode.getValueForString("auto"), + ExposureMode.auto); + assertEquals( + "Returns ExposureMode.locked for 'locked'", + ExposureMode.getValueForString("locked"), + ExposureMode.locked); + } + + @Test + public void getValueForString_returns_null_for_nonexistant_value() { + assertEquals( + "Returns null for 'nonexistant'", ExposureMode.getValueForString("nonexistant"), null); + } + + @Test + public void toString_returns_correct_value() { + assertEquals("Returns 'auto' for ExposureMode.auto", ExposureMode.auto.toString(), "auto"); + assertEquals( + "Returns 'locked' for ExposureMode.locked", ExposureMode.locked.toString(), "locked"); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java new file mode 100644 index 000000000000..5a53648bc51e --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FlashModeTest.java @@ -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. + +package io.flutter.plugins.camera.types; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class FlashModeTest { + + @Test + public void getValueForString_returns_correct_values() { + assertEquals( + "Returns FlashMode.off for 'off'", FlashMode.getValueForString("off"), FlashMode.off); + assertEquals( + "Returns FlashMode.auto for 'auto'", FlashMode.getValueForString("auto"), FlashMode.auto); + assertEquals( + "Returns FlashMode.always for 'always'", + FlashMode.getValueForString("always"), + FlashMode.always); + assertEquals( + "Returns FlashMode.torch for 'torch'", + FlashMode.getValueForString("torch"), + FlashMode.torch); + } + + @Test + public void getValueForString_returns_null_for_nonexistant_value() { + assertEquals( + "Returns null for 'nonexistant'", FlashMode.getValueForString("nonexistant"), null); + } + + @Test + public void toString_returns_correct_value() { + assertEquals("Returns 'off' for FlashMode.off", FlashMode.off.toString(), "off"); + assertEquals("Returns 'auto' for FlashMode.auto", FlashMode.auto.toString(), "auto"); + assertEquals("Returns 'always' for FlashMode.always", FlashMode.always.toString(), "always"); + assertEquals("Returns 'torch' for FlashMode.torch", FlashMode.torch.toString(), "torch"); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java new file mode 100644 index 000000000000..58e6d7ce3306 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/types/FocusModeTest.java @@ -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. + +package io.flutter.plugins.camera.types; + +import static org.junit.Assert.assertEquals; + +import org.junit.Test; + +public class FocusModeTest { + + @Test + public void getValueForString_returns_correct_values() { + assertEquals( + "Returns FocusMode.auto for 'auto'", FocusMode.getValueForString("auto"), FocusMode.auto); + assertEquals( + "Returns FocusMode.locked for 'locked'", + FocusMode.getValueForString("locked"), + FocusMode.locked); + } + + @Test + public void getValueForString_returns_null_for_nonexistant_value() { + assertEquals( + "Returns null for 'nonexistant'", FocusMode.getValueForString("nonexistant"), null); + } + + @Test + public void toString_returns_correct_value() { + assertEquals("Returns 'auto' for FocusMode.auto", FocusMode.auto.toString(), "auto"); + assertEquals("Returns 'locked' for FocusMode.locked", FocusMode.locked.toString(), "locked"); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java new file mode 100644 index 000000000000..9fc669527bfa --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java @@ -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. + +package io.flutter.plugins.camera.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); + } + } +} diff --git a/packages/camera/camera_android.iml b/packages/camera/camera/camera_android.iml similarity index 100% rename from packages/camera/camera_android.iml rename to packages/camera/camera/camera_android.iml diff --git a/packages/camera/example/android.iml b/packages/camera/camera/example/android.iml similarity index 100% rename from packages/camera/example/android.iml rename to packages/camera/camera/example/android.iml diff --git a/packages/camera/camera/example/android/app/build.gradle b/packages/camera/camera/example/android/app/build.gradle new file mode 100644 index 000000000000..7d0e281b74e8 --- /dev/null +++ b/packages/camera/camera/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 29 + + 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/camera/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/camera/camera/example/android/app/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/camera/example/android/app/gradle/wrapper/gradle-wrapper.properties rename to packages/camera/camera/example/android/app/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/EmbeddingV1ActivityTest.java b/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/EmbeddingV1ActivityTest.java new file mode 100644 index 000000000000..9cd4ef60991b --- /dev/null +++ b/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/EmbeddingV1ActivityTest.java @@ -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. + +package io.flutter.plugins.cameraexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@RunWith(FlutterTestRunner.class) +@SuppressWarnings("deprecation") +public class EmbeddingV1ActivityTest { + @Rule + public ActivityTestRule rule = + new ActivityTestRule<>(EmbeddingV1Activity.class); +} diff --git a/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/FlutterActivityTest.java b/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/FlutterActivityTest.java new file mode 100644 index 000000000000..32acc1ba9c15 --- /dev/null +++ b/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/FlutterActivityTest.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.cameraexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@RunWith(FlutterTestRunner.class) +public class FlutterActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/camera/camera/example/android/app/src/main/AndroidManifest.xml b/packages/camera/camera/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..f216a7251bcf --- /dev/null +++ b/packages/camera/camera/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/packages/camera/camera/example/android/app/src/main/java/io/flutter/plugins/cameraexample/EmbeddingV1Activity.java b/packages/camera/camera/example/android/app/src/main/java/io/flutter/plugins/cameraexample/EmbeddingV1Activity.java new file mode 100644 index 000000000000..406e1417e3af --- /dev/null +++ b/packages/camera/camera/example/android/app/src/main/java/io/flutter/plugins/cameraexample/EmbeddingV1Activity.java @@ -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. + +package io.flutter.plugins.cameraexample; + +import android.os.Bundle; +import dev.flutter.plugins.integration_test.IntegrationTestPlugin; +import io.flutter.plugins.camera.CameraPlugin; +import io.flutter.plugins.pathprovider.PathProviderPlugin; +import io.flutter.plugins.videoplayer.VideoPlayerPlugin; + +@SuppressWarnings("deprecation") +public class EmbeddingV1Activity extends io.flutter.app.FlutterActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + CameraPlugin.registerWith(registrarFor("io.flutter.plugins.camera.CameraPlugin")); + IntegrationTestPlugin.registerWith( + registrarFor("dev.flutter.plugins.integration_test.IntegrationTestPlugin")); + PathProviderPlugin.registerWith( + registrarFor("io.flutter.plugins.pathprovider.PathProviderPlugin")); + VideoPlayerPlugin.registerWith( + registrarFor("io.flutter.plugins.videoplayer.VideoPlayerPlugin")); + } +} diff --git a/packages/camera/example/android/app/src/main/res/drawable/launch_background.xml b/packages/camera/camera/example/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from packages/camera/example/android/app/src/main/res/drawable/launch_background.xml rename to packages/camera/camera/example/android/app/src/main/res/drawable/launch_background.xml diff --git a/packages/camera/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/camera/camera/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from packages/camera/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/camera/camera/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/camera/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/camera/camera/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from packages/camera/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/camera/camera/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/camera/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/camera/camera/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from packages/camera/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/camera/camera/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/camera/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/camera/camera/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from packages/camera/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/camera/camera/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/camera/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/camera/camera/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from packages/camera/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/camera/camera/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/packages/camera/example/android/app/src/main/res/values/styles.xml b/packages/camera/camera/example/android/app/src/main/res/values/styles.xml similarity index 100% rename from packages/camera/example/android/app/src/main/res/values/styles.xml rename to packages/camera/camera/example/android/app/src/main/res/values/styles.xml diff --git a/packages/camera/camera/example/android/build.gradle b/packages/camera/camera/example/android/build.gradle new file mode 100644 index 000000000000..456d020f6e2c --- /dev/null +++ b/packages/camera/camera/example/android/build.gradle @@ -0,0 +1,29 @@ +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/camera/example/android/gradle.properties b/packages/camera/camera/example/android/gradle.properties similarity index 100% rename from packages/camera/example/android/gradle.properties rename to packages/camera/camera/example/android/gradle.properties diff --git a/packages/camera/camera/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/camera/camera/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..01a286e96a21 --- /dev/null +++ b/packages/camera/camera/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.6.2-all.zip diff --git a/packages/camera/example/android/settings.gradle b/packages/camera/camera/example/android/settings.gradle similarity index 100% rename from packages/camera/example/android/settings.gradle rename to packages/camera/camera/example/android/settings.gradle diff --git a/packages/camera/example/camera_example.iml b/packages/camera/camera/example/camera_example.iml similarity index 100% rename from packages/camera/example/camera_example.iml rename to packages/camera/camera/example/camera_example.iml diff --git a/packages/camera/example/camera_example_android.iml b/packages/camera/camera/example/camera_example_android.iml similarity index 100% rename from packages/camera/example/camera_example_android.iml rename to packages/camera/camera/example/camera_example_android.iml diff --git a/packages/camera/camera/example/integration_test/camera_test.dart b/packages/camera/camera/example/integration_test/camera_test.dart new file mode 100644 index 000000000000..00a1714f9a5a --- /dev/null +++ b/packages/camera/camera/example/integration_test/camera_test.dart @@ -0,0 +1,235 @@ +// Copyright 2013 The Flutter Authors. All 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/camera.dart'; +import 'package:flutter/painting.dart'; +import 'package:flutter_test/flutter_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; + + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + setUpAll(() async { + 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: + Platform.isAndroid ? const Size(240, 320) : const Size(288, 352), + ResolutionPreset.medium: + Platform.isAndroid ? const Size(480, 720) : 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 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 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(); + } + } + }, skip: !Platform.isAndroid); + + // 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 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 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(); + } + } + }, skip: !Platform.isAndroid); + + testWidgets('Pause and resume video recording', (WidgetTester tester) async { + final List cameras = await 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 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)); + }, skip: !Platform.isAndroid); + + testWidgets( + 'Android image streaming', + (WidgetTester tester) async { + final List cameras = await availableCameras(); + if (cameras.isEmpty) { + return; + } + + final CameraController controller = CameraController( + cameras[0], + ResolutionPreset.low, + enableAudio: false, + ); + + await controller.initialize(); + bool _isDetecting = false; + + await controller.startImageStream((CameraImage 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(); + }, + skip: !Platform.isAndroid, + ); +} diff --git a/packages/android_alarm_manager/example/ios/Flutter/AppFrameworkInfo.plist b/packages/camera/camera/example/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from packages/android_alarm_manager/example/ios/Flutter/AppFrameworkInfo.plist rename to packages/camera/camera/example/ios/Flutter/AppFrameworkInfo.plist diff --git a/packages/camera/camera/example/ios/Flutter/Debug.xcconfig b/packages/camera/camera/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 000000000000..b2f5fae9c254 --- /dev/null +++ b/packages/camera/camera/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/example/ios/Flutter/Release.xcconfig b/packages/camera/camera/example/ios/Flutter/Release.xcconfig new file mode 100644 index 000000000000..88c29144c836 --- /dev/null +++ b/packages/camera/camera/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/example/ios/Podfile b/packages/camera/camera/example/ios/Podfile new file mode 100644 index 000000000000..5bc7b7e85717 --- /dev/null +++ b/packages/camera/camera/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/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..aead167a5e99 --- /dev/null +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,636 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + 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 */; }; + 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 */; }; +/* 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 */ + 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; }; + 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; }; + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; +/* 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 */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 03BB76692665316900CE5A93 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 03BB766A2665316900CE5A93 /* CameraFocusTests.m */, + 03BB767226653ABE00CE5A93 /* CameraOrientationTests.m */, + 03BB766C2665316900CE5A93 /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 78D1009194BD06C03BED950D /* Frameworks */ = { + isa = PBXGroup; + children = ( + 2A4F2DE74AE0C572296A00BF /* libPods-Runner.a */, + 5555DD51E06E67921CFA83DD /* 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 */, + 78D1009194BD06C03BED950D /* 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 = ( + 8F7D83D0CFC9B51065F87CE1 /* Pods-Runner.debug.xcconfig */, + A4725B4F24805CD3CA67828F /* Pods-Runner.release.xcconfig */, + 40D9DDFB3787960D28DF3FB3 /* Pods-RunnerTests.debug.xcconfig */, + D1FF8C34CA9E9BE702C5EC06 /* 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 = ( + 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 */, + 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 = { + 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 */ + 1D0D227A6719C1144CAE5AB5 /* [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; + }; + 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"; + }; + 604FC00FF5713F40F2A4441D /* [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"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 03BB76642665316900CE5A93 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 03BB766B2665316900CE5A93 /* CameraFocusTests.m in Sources */, + 334733EA2668111C00DCC49E /* CameraOrientationTests.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 = 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 = { + 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.cameraExample; + 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.cameraExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 03BB76712665316900CE5A93 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 03BB766F2665316900CE5A93 /* Debug */, + 03BB76702665316900CE5A93 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/camera/camera/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/camera/camera/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + 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 new file mode 100644 index 000000000000..1447e08231be --- /dev/null +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/android_alarm_manager/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/camera/camera/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/camera/camera/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/in_app_purchase/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from packages/in_app_purchase/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/packages/camera/camera/example/ios/Runner/AppDelegate.h b/packages/camera/camera/example/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..0681d288bb70 --- /dev/null +++ b/packages/camera/camera/example/ios/Runner/AppDelegate.h @@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +@interface AppDelegate : FlutterAppDelegate + +@end diff --git a/packages/camera/camera/example/ios/Runner/AppDelegate.m b/packages/camera/camera/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..30b87969f44a --- /dev/null +++ b/packages/camera/camera/example/ios/Runner/AppDelegate.m @@ -0,0 +1,17 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "AppDelegate.h" +#include "GeneratedPluginRegistrant.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GeneratedPluginRegistrant registerWithRegistry:self]; + // Override point for customization after application launch. + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/camera/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to packages/camera/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/packages/android_alarm_manager/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/camera/camera/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/camera/camera/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/android_alarm_manager/example/ios/Runner/Base.lproj/Main.storyboard b/packages/camera/camera/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/camera/camera/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/packages/camera/camera/example/ios/Runner/Info.plist b/packages/camera/camera/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..ff2e341a1803 --- /dev/null +++ b/packages/camera/camera/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/example/ios/Runner/main.m b/packages/camera/camera/example/ios/Runner/main.m new file mode 100644 index 000000000000..f97b9ef5c8a1 --- /dev/null +++ b/packages/camera/camera/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/camera/camera/example/ios/RunnerTests/CameraFocusTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m new file mode 100644 index 000000000000..5d93bdf70332 --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m @@ -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 camera; +@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; +@end + +@interface CameraFocusTests : XCTestCase +@property(readonly, nonatomic) FLTCam *camera; +@property(readonly, nonatomic) id mockDevice; + +@end + +@implementation CameraFocusTests + +- (void)setUp { + _camera = [[FLTCam alloc] init]; + _mockDevice = OCMClassMock([AVCaptureDevice class]); +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the + // class. +} + +- (void)testAutoFocusWithContinuousModeSupported_ShouldSetContinuousAutoFocus { + // AVCaptureFocusModeContinuousAutoFocus is supported + OCMStub([_mockDevice isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]).andReturn(true); + // AVCaptureFocusModeContinuousAutoFocus is supported + OCMStub([_mockDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]).andReturn(true); + + // Don't expect setFocusMode:AVCaptureFocusModeAutoFocus + [[_mockDevice reject] setFocusMode:AVCaptureFocusModeAutoFocus]; + + // Run test + [_camera applyFocusMode:FocusModeAuto onDevice:_mockDevice]; + + // Expect setFocusMode:AVCaptureFocusModeContinuousAutoFocus + OCMVerify([_mockDevice setFocusMode:AVCaptureFocusModeContinuousAutoFocus]); +} + +- (void)testAutoFocusWithContinuousModeNotSupported_ShouldSetAutoFocus { + // AVCaptureFocusModeContinuousAutoFocus is not supported + OCMStub([_mockDevice isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) + .andReturn(false); + // AVCaptureFocusModeContinuousAutoFocus is supported + OCMStub([_mockDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]).andReturn(true); + + // Don't expect setFocusMode:AVCaptureFocusModeContinuousAutoFocus + [[_mockDevice reject] setFocusMode:AVCaptureFocusModeContinuousAutoFocus]; + + // Run test + [_camera applyFocusMode:FocusModeAuto onDevice:_mockDevice]; + + // Expect setFocusMode:AVCaptureFocusModeAutoFocus + OCMVerify([_mockDevice setFocusMode:AVCaptureFocusModeAutoFocus]); +} + +- (void)testAutoFocusWithNoModeSupported_ShouldSetNothing { + // AVCaptureFocusModeContinuousAutoFocus is not supported + OCMStub([_mockDevice isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) + .andReturn(false); + // AVCaptureFocusModeContinuousAutoFocus is not supported + OCMStub([_mockDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]).andReturn(false); + + // Don't expect any setFocus + [[_mockDevice reject] setFocusMode:AVCaptureFocusModeContinuousAutoFocus]; + [[_mockDevice reject] setFocusMode:AVCaptureFocusModeAutoFocus]; + + // Run test + [_camera applyFocusMode:FocusModeAuto onDevice:_mockDevice]; +} + +- (void)testLockedFocusWithModeSupported_ShouldSetModeAutoFocus { + // AVCaptureFocusModeContinuousAutoFocus is supported + OCMStub([_mockDevice isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]).andReturn(true); + // AVCaptureFocusModeContinuousAutoFocus is supported + OCMStub([_mockDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]).andReturn(true); + + // Don't expect any setFocus + [[_mockDevice reject] setFocusMode:AVCaptureFocusModeContinuousAutoFocus]; + + // Run test + [_camera applyFocusMode:FocusModeLocked onDevice:_mockDevice]; + + // Expect setFocusMode:AVCaptureFocusModeAutoFocus + OCMVerify([_mockDevice setFocusMode:AVCaptureFocusModeAutoFocus]); +} + +- (void)testLockedFocusWithModeNotSupported_ShouldSetNothing { + // AVCaptureFocusModeContinuousAutoFocus is supported + OCMStub([_mockDevice isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]).andReturn(true); + // AVCaptureFocusModeContinuousAutoFocus is not supported + OCMStub([_mockDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]).andReturn(false); + + // Don't expect any setFocus + [[_mockDevice reject] setFocusMode:AVCaptureFocusModeContinuousAutoFocus]; + [[_mockDevice reject] setFocusMode:AVCaptureFocusModeAutoFocus]; + + // Run test + [_camera applyFocusMode:FocusModeLocked onDevice:_mockDevice]; +} + +@end diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m new file mode 100644 index 000000000000..6c29ef7b2866 --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/CameraOrientationTests.m @@ -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 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/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/GoogleSignInPluginTest/Info.plist b/packages/camera/camera/example/ios/RunnerTests/Info.plist similarity index 100% rename from packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/GoogleSignInPluginTest/Info.plist rename to packages/camera/camera/example/ios/RunnerTests/Info.plist diff --git a/packages/camera/camera/example/lib/main.dart b/packages/camera/camera/example/lib/main.dart new file mode 100644 index 000000000000..536e95de9c8b --- /dev/null +++ b/packages/camera/camera/example/lib/main.dart @@ -0,0 +1,953 @@ +// Copyright 2013 The Flutter 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:camera/camera.dart'; +import 'package:flutter/material.dart'; +import 'package:video_player/video_player.dart'; + +class CameraExampleHome extends StatefulWidget { + @override + _CameraExampleHomeState 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(); + WidgetsBinding.instance?.addObserver(this); + + _flashModeControlRowAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _flashModeControlRowAnimation = CurvedAnimation( + parent: _flashModeControlRowAnimationController, + curve: Curves.easeInCubic, + ); + _exposureModeControlRowAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _exposureModeControlRowAnimation = CurvedAnimation( + parent: _exposureModeControlRowAnimationController, + curve: Curves.easeInCubic, + ); + _focusModeControlRowAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _focusModeControlRowAnimation = CurvedAnimation( + parent: _focusModeControlRowAnimationController, + curve: Curves.easeInCubic, + ); + } + + @override + void dispose() { + WidgetsBinding.instance?.removeObserver(this); + _flashModeControlRowAnimationController.dispose(); + _exposureModeControlRowAnimationController.dispose(); + super.dispose(); + } + + @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); + } + } + + final GlobalKey _scaffoldKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return Scaffold( + key: _scaffoldKey, + appBar: AppBar( + title: const Text('Camera example'), + ), + body: Column( + children: [ + Expanded( + child: Container( + child: Padding( + padding: const EdgeInsets.all(1.0), + child: Center( + child: _cameraPreviewWidget(), + ), + ), + decoration: BoxDecoration( + color: Colors.black, + border: Border.all( + color: + controller != null && controller!.value.isRecordingVideo + ? Colors.redAccent + : Colors.grey, + width: 3.0, + ), + ), + ), + ), + _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: (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 controller!.setZoomLevel(_currentScale); + } + + /// Display the thumbnail of the captured image or video. + Widget _thumbnailWidget() { + final VideoPlayerController? localVideoController = videoController; + + return Expanded( + child: Align( + alignment: Alignment.centerRight, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + localVideoController == null && imageFile == null + ? Container() + : SizedBox( + child: (localVideoController == null) + ? 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, + ), + ], + ), + ), + ); + } + + /// Display a bar with buttons to change the flash and exposure modes + Widget _modeControlRowWidget() { + return Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + IconButton( + icon: Icon(Icons.flash_on), + color: Colors.blue, + onPressed: controller != null ? onFlashModeButtonPressed : null, + ), + IconButton( + icon: Icon(Icons.exposure), + color: Colors.blue, + onPressed: + controller != null ? onExposureModeButtonPressed : null, + ), + IconButton( + icon: Icon(Icons.filter_center_focus), + color: Colors.blue, + onPressed: controller != null ? onFocusModeButtonPressed : null, + ), + IconButton( + icon: Icon(enableAudio ? Icons.volume_up : Icons.volume_mute), + color: Colors.blue, + onPressed: controller != null ? onAudioModeButtonPressed : null, + ), + IconButton( + icon: Icon(controller?.value.isCaptureOrientationLocked ?? false + ? Icons.screen_lock_rotation + : Icons.screen_rotation), + color: Colors.blue, + onPressed: controller != null + ? onCaptureOrientationLockButtonPressed + : null, + ), + ], + ), + _flashModeControlRowWidget(), + _exposureModeControlRowWidget(), + _focusModeControlRowWidget(), + ], + ); + } + + Widget _flashModeControlRowWidget() { + return SizeTransition( + sizeFactor: _flashModeControlRowAnimation, + child: ClipRect( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + IconButton( + icon: Icon(Icons.flash_off), + color: controller?.value.flashMode == FlashMode.off + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.off) + : null, + ), + IconButton( + icon: Icon(Icons.flash_auto), + color: controller?.value.flashMode == FlashMode.auto + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.auto) + : null, + ), + IconButton( + icon: Icon(Icons.flash_on), + color: controller?.value.flashMode == FlashMode.always + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.always) + : null, + ), + IconButton( + icon: Icon(Icons.highlight), + color: controller?.value.flashMode == FlashMode.torch + ? Colors.orange + : Colors.blue, + onPressed: controller != null + ? () => onSetFlashModeButtonPressed(FlashMode.torch) + : null, + ), + ], + ), + ), + ); + } + + Widget _exposureModeControlRowWidget() { + final ButtonStyle styleAuto = TextButton.styleFrom( + primary: controller?.value.exposureMode == ExposureMode.auto + ? Colors.orange + : Colors.blue, + ); + final ButtonStyle styleLocked = TextButton.styleFrom( + primary: controller?.value.exposureMode == ExposureMode.locked + ? Colors.orange + : Colors.blue, + ); + + return SizeTransition( + sizeFactor: _exposureModeControlRowAnimation, + child: ClipRect( + child: Container( + color: Colors.grey.shade50, + child: Column( + children: [ + Center( + child: Text("Exposure Mode"), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + TextButton( + child: Text('AUTO'), + style: styleAuto, + onPressed: controller != null + ? () => + onSetExposureModeButtonPressed(ExposureMode.auto) + : null, + onLongPress: () { + if (controller != null) { + controller!.setExposurePoint(null); + showInSnackBar('Resetting exposure point'); + } + }, + ), + TextButton( + child: Text('LOCKED'), + style: styleLocked, + onPressed: controller != null + ? () => + onSetExposureModeButtonPressed(ExposureMode.locked) + : null, + ), + ], + ), + Center( + child: Text("Exposure Offset"), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + Text(_minAvailableExposureOffset.toString()), + Slider( + value: _currentExposureOffset, + min: _minAvailableExposureOffset, + max: _maxAvailableExposureOffset, + label: _currentExposureOffset.toString(), + onChanged: _minAvailableExposureOffset == + _maxAvailableExposureOffset + ? null + : setExposureOffset, + ), + Text(_maxAvailableExposureOffset.toString()), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _focusModeControlRowWidget() { + final ButtonStyle styleAuto = TextButton.styleFrom( + primary: controller?.value.focusMode == FocusMode.auto + ? Colors.orange + : Colors.blue, + ); + final ButtonStyle styleLocked = TextButton.styleFrom( + primary: controller?.value.focusMode == FocusMode.locked + ? Colors.orange + : Colors.blue, + ); + + return SizeTransition( + sizeFactor: _focusModeControlRowAnimation, + child: ClipRect( + child: Container( + color: Colors.grey.shade50, + child: Column( + children: [ + Center( + child: Text("Focus Mode"), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + TextButton( + child: Text('AUTO'), + style: styleAuto, + onPressed: controller != null + ? () => onSetFocusModeButtonPressed(FocusMode.auto) + : null, + onLongPress: () { + if (controller != null) controller!.setFocusPoint(null); + showInSnackBar('Resetting focus point'); + }, + ), + TextButton( + child: Text('LOCKED'), + style: styleLocked, + onPressed: controller != null + ? () => onSetFocusModeButtonPressed(FocusMode.locked) + : null, + ), + ], + ), + ], + ), + ), + ), + ); + } + + /// Display the control bar with buttons to take pictures and record videos. + Widget _captureControlRowWidget() { + 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.isRecordingPaused + ? Icon(Icons.play_arrow) + : 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, + ) + ], + ); + } + + /// Display a row of toggle to select the camera (or a message if no camera is available). + Widget _cameraTogglesRowWidget() { + final List toggles = []; + + final onChanged = (CameraDescription? description) { + if (description == null) { + return; + } + + onNewCameraSelected(description); + }; + + if (cameras.isEmpty) { + return const Text('No camera found'); + } else { + for (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) { + // ignore: deprecated_member_use + _scaffoldKey.currentState?.showSnackBar(SnackBar(content: Text(message))); + } + + void onViewFinderTap(TapDownDetails details, BoxConstraints constraints) { + if (controller == null) { + return; + } + + final CameraController cameraController = controller!; + + final offset = Offset( + details.localPosition.dx / constraints.maxWidth, + details.localPosition.dy / constraints.maxHeight, + ); + cameraController.setExposurePoint(offset); + cameraController.setFocusPoint(offset); + } + + void onNewCameraSelected(CameraDescription cameraDescription) async { + if (controller != null) { + await controller!.dispose(); + } + final CameraController cameraController = CameraController( + cameraDescription, + ResolutionPreset.medium, + enableAudio: enableAudio, + imageFormatGroup: ImageFormatGroup.jpeg, + ); + controller = cameraController; + + // If the controller is updated then update the UI. + cameraController.addListener(() { + if (mounted) setState(() {}); + if (cameraController.value.hasError) { + showInSnackBar( + 'Camera error ${cameraController.value.errorDescription}'); + } + }); + + try { + await cameraController.initialize(); + await Future.wait([ + cameraController + .getMinExposureOffset() + .then((value) => _minAvailableExposureOffset = value), + cameraController + .getMaxExposureOffset() + .then((value) => _maxAvailableExposureOffset = value), + cameraController + .getMaxZoomLevel() + .then((value) => _maxAvailableZoom = value), + cameraController + .getMinZoomLevel() + .then((value) => _minAvailableZoom = value), + ]); + } on CameraException catch (e) { + _showCameraException(e); + } + + 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); + } + } + + void onCaptureOrientationLockButtonPressed() async { + 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}'); + } + } + } + + 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((file) { + if (mounted) setState(() {}); + if (file != null) { + showInSnackBar('Video recorded to ${file.path}'); + videoFile = file; + _startVideoPlayer(); + } + }); + } + + 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 null; + } + + 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 null; + } + + 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 = + 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 { + 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}'); + } +} + +class CameraApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + home: CameraExampleHome(), + ); + } +} + +List cameras = []; + +Future main() async { + // Fetch the available cameras before initializing the app. + try { + WidgetsFlutterBinding.ensureInitialized(); + cameras = await availableCameras(); + } on CameraException catch (e) { + logError(e.code, e.description); + } + runApp(CameraApp()); +} diff --git a/packages/camera/camera/example/pubspec.yaml b/packages/camera/camera/example/pubspec.yaml new file mode 100644 index 000000000000..eb8995e2f354 --- /dev/null +++ b/packages/camera/camera/example/pubspec.yaml @@ -0,0 +1,32 @@ +name: camera_example +description: Demonstrates how to use the camera plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.22.0" + +dependencies: + camera: + # When depending on this package from a real application you should use: + # camera: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + path_provider: ^2.0.0 + flutter: + sdk: flutter + video_player: ^2.1.4 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + pedantic: ^1.10.0 + +flutter: + uses-material-design: true diff --git a/packages/camera/camera/example/test_driver/integration_test.dart b/packages/camera/camera/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..dedb2537fb88 --- /dev/null +++ b/packages/camera/camera/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('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); + exit(result['result'] == 'true' ? 0 : 1); +} diff --git a/packages/android_alarm_manager/ios/Assets/.gitkeep b/packages/camera/camera/ios/Assets/.gitkeep similarity index 100% rename from packages/android_alarm_manager/ios/Assets/.gitkeep rename to packages/camera/camera/ios/Assets/.gitkeep diff --git a/packages/camera/ios/Classes/CameraPlugin.h b/packages/camera/camera/ios/Classes/CameraPlugin.h similarity index 75% rename from packages/camera/ios/Classes/CameraPlugin.h rename to packages/camera/camera/ios/Classes/CameraPlugin.h index ae865e496a45..f13d810445bc 100644 --- a/packages/camera/ios/Classes/CameraPlugin.h +++ b/packages/camera/camera/ios/Classes/CameraPlugin.h @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/camera/camera/ios/Classes/CameraPlugin.m b/packages/camera/camera/ios/Classes/CameraPlugin.m new file mode 100644 index 000000000000..ebd5366ba78d --- /dev/null +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -0,0 +1,1490 @@ +// Copyright 2013 The Flutter Authors. 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) 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 (@available(iOS 9.0, *)) { + 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; + + _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)setExposurePointWithResult:(FlutterResult)result x:(double)x y:(double)y { + if (!_captureDevice.isExposurePointOfInterestSupported) { + result([FlutterError errorWithCode:@"setExposurePointFailed" + message:@"Device does not have exposure point capabilities" + details:nil]); + return; + } + [_captureDevice lockForConfiguration:nil]; + [_captureDevice setExposurePointOfInterest:CGPointMake(y, 1 - x)]; + [_captureDevice unlockForConfiguration]; + // Retrigger auto exposure + [self applyExposureMode]; + result(nil); +} + +- (void)setFocusPointWithResult:(FlutterResult)result x:(double)x y:(double)y { + if (!_captureDevice.isFocusPointOfInterestSupported) { + result([FlutterError errorWithCode:@"setFocusPointFailed" + message:@"Device does not have focus point capabilities" + details:nil]); + return; + } + [_captureDevice lockForConfiguration:nil]; + [_captureDevice setFocusPointOfInterest:CGPointMake(y, 1 - x)]; + [_captureDevice unlockForConfiguration]; + // Retrigger auto focus + [self applyFocusMode]; + result(nil); +} + +- (void)setExposureOffsetWithResult:(FlutterResult)result offset:(double)offset { + [_captureDevice lockForConfiguration:nil]; + [_captureDevice setExposureTargetBias:offset completionHandler:nil]; + [_captureDevice unlockForConfiguration]; + result(@(offset)); +} + +- (void)startImageStreamWithMessenger:(NSObject *)messenger { + 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 = ^{ + [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 { + result(FlutterMethodNotImplemented); + } + } +} + +@end diff --git a/packages/camera/ios/camera.podspec b/packages/camera/camera/ios/camera.podspec similarity index 91% rename from packages/camera/ios/camera.podspec rename to packages/camera/camera/ios/camera.podspec index 960f102e7706..4f9955311fb9 100644 --- a/packages/camera/ios/camera.podspec +++ b/packages/camera/camera/ios/camera.podspec @@ -19,8 +19,4 @@ A Flutter plugin to use the camera from your Flutter app. s.platform = :ios, '8.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } - - s.test_spec 'Tests' do |test_spec| - test_spec.source_files = 'Tests/**/*' - end end diff --git a/packages/camera/camera/lib/camera.dart b/packages/camera/camera/lib/camera.dart new file mode 100644 index 000000000000..1e24efbd3dc6 --- /dev/null +++ b/packages/camera/camera/lib/camera.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. + +export 'src/camera_controller.dart'; +export 'src/camera_image.dart'; +export 'src/camera_preview.dart'; + +export 'package:camera_platform_interface/camera_platform_interface.dart' + show + CameraDescription, + CameraException, + CameraLensDirection, + FlashMode, + ExposureMode, + FocusMode, + ResolutionPreset, + XFile, + ImageFormatGroup; diff --git a/packages/camera/camera/lib/src/camera_controller.dart b/packages/camera/camera/lib/src/camera_controller.dart new file mode 100644 index 000000000000..3284a9b01fa2 --- /dev/null +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -0,0 +1,781 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:math'; + +import 'package:camera/camera.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:pedantic/pedantic.dart'; +import 'package:quiver/core.dart'; + +final MethodChannel _channel = const MethodChannel('plugins.flutter.io/camera'); + +/// Signature for a callback receiving the a camera image. +/// +/// This is used by [CameraController.startImageStream]. +// ignore: inference_failure_on_function_return_type +typedef onLatestImageAvailable = Function(CameraImage image); + +/// Completes with a list of available cameras. +/// +/// May throw a [CameraException]. +Future> availableCameras() async { + return CameraPlatform.instance.availableCameras(); +} + +/// The state of a [CameraController]. +class CameraValue { + /// Creates a new camera controller state. + const CameraValue({ + required this.isInitialized, + this.errorDescription, + this.previewSize, + required this.isRecordingVideo, + required this.isTakingPicture, + required this.isStreamingImages, + required bool isRecordingPaused, + required this.flashMode, + required this.exposureMode, + required this.focusMode, + required this.exposurePointSupported, + required this.focusPointSupported, + required this.deviceOrientation, + this.lockedCaptureOrientation, + this.recordingOrientation, + }) : _isRecordingPaused = isRecordingPaused; + + /// Creates a new camera controller state for an uninitialized controller. + const CameraValue.uninitialized() + : this( + isInitialized: false, + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + isRecordingPaused: false, + flashMode: FlashMode.auto, + exposureMode: ExposureMode.auto, + exposurePointSupported: false, + focusMode: FocusMode.auto, + focusPointSupported: false, + deviceOrientation: DeviceOrientation.portraitUp, + ); + + /// True after [CameraController.initialize] has completed successfully. + final bool isInitialized; + + /// True when a picture capture request has been sent but as not yet returned. + final bool isTakingPicture; + + /// True when the camera is recording (not the same as previewing). + final bool isRecordingVideo; + + /// True when images from the camera are being streamed. + final bool isStreamingImages; + + final bool _isRecordingPaused; + + /// True when camera [isRecordingVideo] and recording is paused. + bool get isRecordingPaused => isRecordingVideo && _isRecordingPaused; + + /// Description of an error state. + /// + /// This is null while the controller is not in an error state. + /// When [hasError] is true this contains the error description. + final String? errorDescription; + + /// The size of the preview in pixels. + /// + /// Is `null` until [isInitialized] is `true`. + final Size? previewSize; + + /// Convenience getter for `previewSize.width / previewSize.height`. + /// + /// Can only be called when [initialize] is done. + double get aspectRatio => previewSize!.width / previewSize!.height; + + /// Whether the controller is in an error state. + /// + /// When true [errorDescription] describes the error. + bool get hasError => errorDescription != null; + + /// The flash mode the camera is currently set to. + final FlashMode flashMode; + + /// The exposure mode the camera is currently set to. + final ExposureMode exposureMode; + + /// The focus mode the camera is currently set to. + final FocusMode focusMode; + + /// Whether setting the exposure point is supported. + final bool exposurePointSupported; + + /// Whether setting the focus point is supported. + final bool focusPointSupported; + + /// The current device orientation. + final DeviceOrientation deviceOrientation; + + /// The currently locked capture orientation. + final DeviceOrientation? lockedCaptureOrientation; + + /// Whether the capture orientation is currently locked. + bool get isCaptureOrientationLocked => lockedCaptureOrientation != null; + + /// The orientation of the currently running video recording. + final DeviceOrientation? recordingOrientation; + + /// Creates a modified copy of the object. + /// + /// Explicitly specified fields get the specified value, all other fields get + /// the same value of the current object. + CameraValue copyWith({ + bool? isInitialized, + bool? isRecordingVideo, + bool? isTakingPicture, + bool? isStreamingImages, + String? errorDescription, + Size? previewSize, + bool? isRecordingPaused, + FlashMode? flashMode, + ExposureMode? exposureMode, + FocusMode? focusMode, + bool? exposurePointSupported, + bool? focusPointSupported, + DeviceOrientation? deviceOrientation, + Optional? lockedCaptureOrientation, + Optional? recordingOrientation, + }) { + return CameraValue( + isInitialized: isInitialized ?? this.isInitialized, + errorDescription: errorDescription, + previewSize: previewSize ?? this.previewSize, + isRecordingVideo: isRecordingVideo ?? this.isRecordingVideo, + isTakingPicture: isTakingPicture ?? this.isTakingPicture, + isStreamingImages: isStreamingImages ?? this.isStreamingImages, + isRecordingPaused: isRecordingPaused ?? _isRecordingPaused, + flashMode: flashMode ?? this.flashMode, + exposureMode: exposureMode ?? this.exposureMode, + focusMode: focusMode ?? this.focusMode, + exposurePointSupported: + exposurePointSupported ?? this.exposurePointSupported, + focusPointSupported: focusPointSupported ?? this.focusPointSupported, + deviceOrientation: deviceOrientation ?? this.deviceOrientation, + lockedCaptureOrientation: lockedCaptureOrientation == null + ? this.lockedCaptureOrientation + : lockedCaptureOrientation.orNull, + recordingOrientation: recordingOrientation == null + ? this.recordingOrientation + : recordingOrientation.orNull, + ); + } + + @override + String toString() { + return '$runtimeType(' + 'isRecordingVideo: $isRecordingVideo, ' + 'isInitialized: $isInitialized, ' + 'errorDescription: $errorDescription, ' + 'previewSize: $previewSize, ' + 'isStreamingImages: $isStreamingImages, ' + 'flashMode: $flashMode, ' + 'exposureMode: $exposureMode, ' + 'focusMode: $focusMode, ' + 'exposurePointSupported: $exposurePointSupported, ' + 'focusPointSupported: $focusPointSupported, ' + 'deviceOrientation: $deviceOrientation, ' + 'lockedCaptureOrientation: $lockedCaptureOrientation, ' + 'recordingOrientation: $recordingOrientation)'; + } +} + +/// Controls a device camera. +/// +/// Use [availableCameras] to get a list of available cameras. +/// +/// Before using a [CameraController] a call to [initialize] must complete. +/// +/// To show the camera preview on the screen use a [CameraPreview] widget. +class CameraController extends ValueNotifier { + /// Creates a new camera controller in an uninitialized state. + CameraController( + this.description, + this.resolutionPreset, { + this.enableAudio = true, + this.imageFormatGroup, + }) : super(const CameraValue.uninitialized()); + + /// The properties of the camera device controlled by this controller. + final CameraDescription description; + + /// The resolution this controller is targeting. + /// + /// This resolution preset is not guaranteed to be available on the device, + /// if unavailable a lower resolution will be used. + /// + /// See also: [ResolutionPreset]. + final ResolutionPreset resolutionPreset; + + /// Whether to include audio when recording a video. + final bool enableAudio; + + /// The [ImageFormatGroup] describes the output of the raw image format. + /// + /// When null the imageFormat will fallback to the platforms default. + final ImageFormatGroup? imageFormatGroup; + + /// The id of a camera that hasn't been initialized. + @visibleForTesting + static const int kUninitializedCameraId = -1; + int _cameraId = kUninitializedCameraId; + + bool _isDisposed = false; + StreamSubscription? _imageStreamSubscription; + FutureOr? _initCalled; + StreamSubscription? _deviceOrientationSubscription; + + /// Checks whether [CameraController.dispose] has completed successfully. + /// + /// This is a no-op when asserts are disabled. + void debugCheckIsDisposed() { + assert(_isDisposed); + } + + /// The camera identifier with which the controller is associated. + int get cameraId => _cameraId; + + /// Initializes the camera on the device. + /// + /// Throws a [CameraException] if the initialization fails. + Future initialize() async { + if (_isDisposed) { + throw CameraException( + 'Disposed CameraController', + 'initialize was called on a disposed CameraController', + ); + } + try { + Completer _initializeCompleter = Completer(); + + _deviceOrientationSubscription = + CameraPlatform.instance.onDeviceOrientationChanged().listen((event) { + value = value.copyWith( + deviceOrientation: event.orientation, + ); + }); + + _cameraId = await CameraPlatform.instance.createCamera( + description, + resolutionPreset, + enableAudio: enableAudio, + ); + + unawaited(CameraPlatform.instance + .onCameraInitialized(_cameraId) + .first + .then((event) { + _initializeCompleter.complete(event); + })); + + await CameraPlatform.instance.initializeCamera( + _cameraId, + imageFormatGroup: imageFormatGroup ?? ImageFormatGroup.unknown, + ); + + value = value.copyWith( + isInitialized: true, + previewSize: await _initializeCompleter.future + .then((CameraInitializedEvent event) => Size( + event.previewWidth, + event.previewHeight, + )), + exposureMode: await _initializeCompleter.future + .then((event) => event.exposureMode), + focusMode: + await _initializeCompleter.future.then((event) => event.focusMode), + exposurePointSupported: await _initializeCompleter.future + .then((event) => event.exposurePointSupported), + focusPointSupported: await _initializeCompleter.future + .then((event) => event.focusPointSupported), + ); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + + _initCalled = true; + } + + /// Prepare the capture session for video recording. + /// + /// Use of this method is optional, but it may be called for performance + /// reasons on iOS. + /// + /// Preparing audio can cause a minor delay in the CameraPreview view on iOS. + /// If video recording is intended, calling this early eliminates this delay + /// that would otherwise be experienced when video recording is started. + /// This operation is a no-op on Android. + /// + /// Throws a [CameraException] if the prepare fails. + Future prepareForVideoRecording() async { + await CameraPlatform.instance.prepareForVideoRecording(); + } + + /// Captures an image and returns the file where it was saved. + /// + /// Throws a [CameraException] if the capture fails. + Future takePicture() async { + _throwIfNotInitialized("takePicture"); + if (value.isTakingPicture) { + throw CameraException( + 'Previous capture has not returned yet.', + 'takePicture was called before the previous capture returned.', + ); + } + try { + value = value.copyWith(isTakingPicture: true); + XFile file = await CameraPlatform.instance.takePicture(_cameraId); + value = value.copyWith(isTakingPicture: false); + return file; + } on PlatformException catch (e) { + value = value.copyWith(isTakingPicture: false); + throw CameraException(e.code, e.message); + } + } + + /// Start streaming images from platform camera. + /// + /// Settings for capturing images on iOS and Android is set to always use the + /// latest image available from the camera and will drop all other images. + /// + /// When running continuously with [CameraPreview] widget, this function runs + /// best with [ResolutionPreset.low]. Running on [ResolutionPreset.high] can + /// have significant frame rate drops for [CameraPreview] on lower end + /// devices. + /// + /// Throws a [CameraException] if image streaming or video recording has + /// already started. + /// + /// The `startImageStream` method is only available on Android and iOS (other + /// platforms won't be supported in current setup). + /// + // TODO(bmparr): Add settings for resolution and fps. + Future startImageStream(onLatestImageAvailable onAvailable) async { + assert(defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS); + _throwIfNotInitialized("startImageStream"); + if (value.isRecordingVideo) { + throw CameraException( + 'A video recording is already started.', + 'startImageStream was called while a video is being recorded.', + ); + } + if (value.isStreamingImages) { + throw CameraException( + 'A camera has started streaming images.', + 'startImageStream was called while a camera was streaming images.', + ); + } + + try { + await _channel.invokeMethod('startImageStream'); + value = value.copyWith(isStreamingImages: true); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + const EventChannel cameraEventChannel = + EventChannel('plugins.flutter.io/camera/imageStream'); + _imageStreamSubscription = + cameraEventChannel.receiveBroadcastStream().listen( + (dynamic imageData) { + onAvailable(CameraImage.fromPlatformData(imageData)); + }, + ); + } + + /// Stop streaming images from platform camera. + /// + /// Throws a [CameraException] if image streaming was not started or video + /// recording was started. + /// + /// The `stopImageStream` method is only available on Android and iOS (other + /// platforms won't be supported in current setup). + Future stopImageStream() async { + assert(defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS); + _throwIfNotInitialized("stopImageStream"); + if (value.isRecordingVideo) { + throw CameraException( + 'A video recording is already started.', + 'stopImageStream was called while a video is being recorded.', + ); + } + if (!value.isStreamingImages) { + throw CameraException( + 'No camera is streaming images', + 'stopImageStream was called when no camera is streaming images.', + ); + } + + try { + value = value.copyWith(isStreamingImages: false); + await _channel.invokeMethod('stopImageStream'); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + + await _imageStreamSubscription?.cancel(); + _imageStreamSubscription = null; + } + + /// Start a video recording. + /// + /// The video is returned as a [XFile] after calling [stopVideoRecording]. + /// Throws a [CameraException] if the capture fails. + Future startVideoRecording() async { + _throwIfNotInitialized("startVideoRecording"); + if (value.isRecordingVideo) { + throw CameraException( + 'A video recording is already started.', + 'startVideoRecording was called when a recording is already started.', + ); + } + if (value.isStreamingImages) { + throw CameraException( + 'A camera has started streaming images.', + 'startVideoRecording was called while a camera was streaming images.', + ); + } + + try { + await CameraPlatform.instance.startVideoRecording(_cameraId); + value = value.copyWith( + isRecordingVideo: true, + isRecordingPaused: false, + recordingOrientation: Optional.fromNullable( + value.lockedCaptureOrientation ?? value.deviceOrientation)); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Stops the video recording and returns the file where it was saved. + /// + /// Throws a [CameraException] if the capture failed. + Future stopVideoRecording() async { + _throwIfNotInitialized("stopVideoRecording"); + if (!value.isRecordingVideo) { + throw CameraException( + 'No video is recording', + 'stopVideoRecording was called when no video is recording.', + ); + } + try { + XFile file = await CameraPlatform.instance.stopVideoRecording(_cameraId); + value = value.copyWith( + isRecordingVideo: false, + recordingOrientation: Optional.absent(), + ); + return file; + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Pause video recording. + /// + /// This feature is only available on iOS and Android sdk 24+. + Future pauseVideoRecording() async { + _throwIfNotInitialized("pauseVideoRecording"); + if (!value.isRecordingVideo) { + throw CameraException( + 'No video is recording', + 'pauseVideoRecording was called when no video is recording.', + ); + } + try { + await CameraPlatform.instance.pauseVideoRecording(_cameraId); + value = value.copyWith(isRecordingPaused: true); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Resume video recording after pausing. + /// + /// This feature is only available on iOS and Android sdk 24+. + Future resumeVideoRecording() async { + _throwIfNotInitialized("resumeVideoRecording"); + if (!value.isRecordingVideo) { + throw CameraException( + 'No video is recording', + 'resumeVideoRecording was called when no video is recording.', + ); + } + try { + await CameraPlatform.instance.resumeVideoRecording(_cameraId); + value = value.copyWith(isRecordingPaused: false); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Returns a widget showing a live camera preview. + Widget buildPreview() { + _throwIfNotInitialized("buildPreview"); + try { + return CameraPlatform.instance.buildPreview(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets the maximum supported zoom level for the selected camera. + Future getMaxZoomLevel() { + _throwIfNotInitialized("getMaxZoomLevel"); + try { + return CameraPlatform.instance.getMaxZoomLevel(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets the minimum supported zoom level for the selected camera. + Future getMinZoomLevel() { + _throwIfNotInitialized("getMinZoomLevel"); + try { + return CameraPlatform.instance.getMinZoomLevel(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Set the zoom level for the selected camera. + /// + /// The supplied [zoom] value should be between 1.0 and the maximum supported + /// zoom level returned by the `getMaxZoomLevel`. Throws an `CameraException` + /// when an illegal zoom level is suplied. + Future setZoomLevel(double zoom) { + _throwIfNotInitialized("setZoomLevel"); + try { + return CameraPlatform.instance.setZoomLevel(_cameraId, zoom); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the flash mode for taking pictures. + Future setFlashMode(FlashMode mode) async { + try { + await CameraPlatform.instance.setFlashMode(_cameraId, mode); + value = value.copyWith(flashMode: mode); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the exposure mode for taking pictures. + Future setExposureMode(ExposureMode mode) async { + try { + await CameraPlatform.instance.setExposureMode(_cameraId, mode); + value = value.copyWith(exposureMode: mode); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the exposure point for automatically determining the exposure value. + /// + /// Supplying a `null` value will reset the exposure point to it's default + /// value. + Future setExposurePoint(Offset? point) async { + if (point != null && + (point.dx < 0 || point.dx > 1 || point.dy < 0 || point.dy > 1)) { + throw ArgumentError( + 'The values of point should be anywhere between (0,0) and (1,1).'); + } + + try { + await CameraPlatform.instance.setExposurePoint( + _cameraId, + point == null + ? null + : Point( + point.dx, + point.dy, + ), + ); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets the minimum supported exposure offset for the selected camera in EV units. + Future getMinExposureOffset() async { + _throwIfNotInitialized("getMinExposureOffset"); + try { + return CameraPlatform.instance.getMinExposureOffset(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets the maximum supported exposure offset for the selected camera in EV units. + Future getMaxExposureOffset() async { + _throwIfNotInitialized("getMaxExposureOffset"); + try { + return CameraPlatform.instance.getMaxExposureOffset(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Gets the supported step size for exposure offset for the selected camera in EV units. + /// + /// Returns 0 when the camera supports using a free value without stepping. + Future getExposureOffsetStepSize() async { + _throwIfNotInitialized("getExposureOffsetStepSize"); + try { + return CameraPlatform.instance.getExposureOffsetStepSize(_cameraId); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the exposure offset for the selected camera. + /// + /// The supplied [offset] value should be in EV units. 1 EV unit represents a + /// doubling in brightness. It should be between the minimum and maximum offsets + /// obtained through `getMinExposureOffset` and `getMaxExposureOffset` respectively. + /// Throws a `CameraException` when an illegal offset is supplied. + /// + /// When the supplied [offset] value does not align with the step size obtained + /// through `getExposureStepSize`, it will automatically be rounded to the nearest step. + /// + /// Returns the (rounded) offset value that was set. + Future setExposureOffset(double offset) async { + _throwIfNotInitialized("setExposureOffset"); + // Check if offset is in range + List range = + await Future.wait([getMinExposureOffset(), getMaxExposureOffset()]); + if (offset < range[0] || offset > range[1]) { + throw CameraException( + "exposureOffsetOutOfBounds", + "The provided exposure offset was outside the supported range for this device.", + ); + } + + // Round to the closest step if needed + double stepSize = await getExposureOffsetStepSize(); + if (stepSize > 0) { + double inv = 1.0 / stepSize; + double roundedOffset = (offset * inv).roundToDouble() / inv; + if (roundedOffset > range[1]) { + roundedOffset = (offset * inv).floorToDouble() / inv; + } else if (roundedOffset < range[0]) { + roundedOffset = (offset * inv).ceilToDouble() / inv; + } + offset = roundedOffset; + } + + try { + return CameraPlatform.instance.setExposureOffset(_cameraId, offset); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Locks the capture orientation. + /// + /// If [orientation] is omitted, the current device orientation is used. + Future lockCaptureOrientation([DeviceOrientation? orientation]) async { + try { + await CameraPlatform.instance.lockCaptureOrientation( + _cameraId, orientation ?? value.deviceOrientation); + value = value.copyWith( + lockedCaptureOrientation: + Optional.fromNullable(orientation ?? value.deviceOrientation)); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the focus mode for taking pictures. + Future setFocusMode(FocusMode mode) async { + try { + await CameraPlatform.instance.setFocusMode(_cameraId, mode); + value = value.copyWith(focusMode: mode); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Unlocks the capture orientation. + Future unlockCaptureOrientation() async { + try { + await CameraPlatform.instance.unlockCaptureOrientation(_cameraId); + value = value.copyWith(lockedCaptureOrientation: Optional.absent()); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Sets the focus point for automatically determining the focus value. + /// + /// Supplying a `null` value will reset the focus point to it's default + /// value. + Future setFocusPoint(Offset? point) async { + if (point != null && + (point.dx < 0 || point.dx > 1 || point.dy < 0 || point.dy > 1)) { + throw ArgumentError( + 'The values of point should be anywhere between (0,0) and (1,1).'); + } + try { + await CameraPlatform.instance.setFocusPoint( + _cameraId, + point == null + ? null + : Point( + point.dx, + point.dy, + ), + ); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Releases the resources of this camera. + @override + Future dispose() async { + if (_isDisposed) { + return; + } + unawaited(_deviceOrientationSubscription?.cancel()); + _isDisposed = true; + super.dispose(); + if (_initCalled != null) { + await _initCalled; + await CameraPlatform.instance.dispose(_cameraId); + } + } + + void _throwIfNotInitialized(String functionName) { + if (!value.isInitialized) { + throw CameraException( + 'Uninitialized CameraController', + '$functionName() was called on an uninitialized CameraController.', + ); + } + if (_isDisposed) { + throw CameraException( + 'Disposed CameraController', + '$functionName() was called on a disposed CameraController.', + ); + } + } +} diff --git a/packages/camera/camera/lib/src/camera_image.dart b/packages/camera/camera/lib/src/camera_image.dart new file mode 100644 index 000000000000..411c7e86db41 --- /dev/null +++ b/packages/camera/camera/lib/src/camera_image.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 'dart:typed_data'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; + +/// A single color plane of image data. +/// +/// The number and meaning of the planes in an image are determined by the +/// format of the Image. +class Plane { + Plane._fromPlatformData(Map data) + : bytes = data['bytes'], + bytesPerPixel = data['bytesPerPixel'], + bytesPerRow = data['bytesPerRow'], + height = data['height'], + width = data['width']; + + /// Bytes representing this plane. + final Uint8List bytes; + + /// The distance between adjacent pixel samples on Android, in bytes. + /// + /// Will be `null` on iOS. + final int? bytesPerPixel; + + /// The row stride for this color plane, in bytes. + final int bytesPerRow; + + /// Height of the pixel buffer on iOS. + /// + /// Will be `null` on Android + final int? height; + + /// Width of the pixel buffer on iOS. + /// + /// Will be `null` on Android. + final int? width; +} + +/// Describes how pixels are represented in an image. +class ImageFormat { + ImageFormat._fromPlatformData(this.raw) : group = _asImageFormatGroup(raw); + + /// Describes the format group the raw image format falls into. + final ImageFormatGroup group; + + /// Raw version of the format from the Android or iOS platform. + /// + /// On Android, this is an `int` from class `android.graphics.ImageFormat`. See + /// https://developer.android.com/reference/android/graphics/ImageFormat + /// + /// On iOS, this is a `FourCharCode` constant from Pixel Format Identifiers. + /// See https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers?language=objc + final dynamic raw; +} + +ImageFormatGroup _asImageFormatGroup(dynamic rawFormat) { + if (defaultTargetPlatform == TargetPlatform.android) { + switch (rawFormat) { + // android.graphics.ImageFormat.YUV_420_888 + case 35: + return ImageFormatGroup.yuv420; + // android.graphics.ImageFormat.JPEG + case 256: + return ImageFormatGroup.jpeg; + } + } + + if (defaultTargetPlatform == TargetPlatform.iOS) { + switch (rawFormat) { + // kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange + case 875704438: + return ImageFormatGroup.yuv420; + // kCVPixelFormatType_32BGRA + case 1111970369: + return ImageFormatGroup.bgra8888; + } + } + + return ImageFormatGroup.unknown; +} + +/// A single complete image buffer from the platform camera. +/// +/// This class allows for direct application access to the pixel data of an +/// Image through one or more [Uint8List]. Each buffer is encapsulated in a +/// [Plane] that describes the layout of the pixel data in that plane. The +/// [CameraImage] is not directly usable as a UI resource. +/// +/// Although not all image formats are planar on iOS, we treat 1-dimensional +/// images as single planar images. +class CameraImage { + /// CameraImage Constructor + CameraImage.fromPlatformData(Map data) + : format = ImageFormat._fromPlatformData(data['format']), + height = data['height'], + width = data['width'], + planes = List.unmodifiable(data['planes'] + .map((dynamic planeData) => Plane._fromPlatformData(planeData))); + + /// Format of the image provided. + /// + /// Determines the number of planes needed to represent the image, and + /// the general layout of the pixel data in each [Uint8List]. + final ImageFormat format; + + /// Height of the image in pixels. + /// + /// For formats where some color channels are subsampled, this is the height + /// of the largest-resolution plane. + final int height; + + /// Width of the image in pixels. + /// + /// For formats where some color channels are subsampled, this is the width + /// of the largest-resolution plane. + final int width; + + /// The pixels planes for this image. + /// + /// The number of planes is determined by the format of the image. + final List planes; +} diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart new file mode 100644 index 000000000000..e2f1ff931e42 --- /dev/null +++ b/packages/camera/camera/lib/src/camera_preview.dart @@ -0,0 +1,71 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera/camera.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +/// A widget showing a live camera preview. +class CameraPreview extends StatelessWidget { + /// Creates a preview widget for the given camera controller. + const CameraPreview(this.controller, {this.child}); + + /// The controller for the camera that the preview is shown for. + final CameraController controller; + + /// A widget to overlay on top of the camera preview + final Widget? child; + + @override + Widget build(BuildContext context) { + return controller.value.isInitialized + ? AspectRatio( + aspectRatio: _isLandscape() + ? controller.value.aspectRatio + : (1 / controller.value.aspectRatio), + child: Stack( + fit: StackFit.expand, + children: [ + _wrapInRotatedBox(child: controller.buildPreview()), + child ?? Container(), + ], + ), + ) + : Container(); + } + + Widget _wrapInRotatedBox({required Widget child}) { + if (defaultTargetPlatform != TargetPlatform.android) { + return child; + } + + return RotatedBox( + quarterTurns: _getQuarterTurns(), + child: child, + ); + } + + bool _isLandscape() { + return [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight] + .contains(_getApplicableOrientation()); + } + + int _getQuarterTurns() { + Map turns = { + DeviceOrientation.portraitUp: 0, + DeviceOrientation.landscapeLeft: 1, + DeviceOrientation.portraitDown: 2, + DeviceOrientation.landscapeRight: 3, + }; + return turns[_getApplicableOrientation()]!; + } + + DeviceOrientation _getApplicableOrientation() { + return controller.value.isRecordingVideo + ? controller.value.recordingOrientation! + : (controller.value.lockedCaptureOrientation ?? + controller.value.deviceOrientation); + } +} diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml new file mode 100644 index 000000000000..a7df9e0d51be --- /dev/null +++ b/packages/camera/camera/pubspec.yaml @@ -0,0 +1,36 @@ +name: camera +description: A Flutter plugin for getting information about and controlling the + camera on Android and iOS. Supports previewing the camera feed, capturing images, capturing video, + and streaming image buffers to dart. +repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 +version: 0.8.1+3 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +flutter: + plugin: + platforms: + android: + package: io.flutter.plugins.camera + pluginClass: CameraPlugin + ios: + pluginClass: CameraPlugin + +dependencies: + camera_platform_interface: ^2.0.0 + flutter: + sdk: flutter + pedantic: ^1.10.0 + quiver: ^3.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_driver: + 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 new file mode 100644 index 000000000000..840770d1eed7 --- /dev/null +++ b/packages/camera/camera/test/camera_image_stream_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: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(); + + setUp(() { + CameraPlatform.instance = MockCameraPlatform(); + }); + + test('startImageStream() throws $CameraException when uninitialized', () { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + expect( + () => cameraController.startImageStream((image) => null), + throwsA( + isA() + .having( + (error) => error.code, + 'code', + 'Uninitialized CameraController', + ) + .having( + (error) => error.description, + 'description', + 'startImageStream() was called on an uninitialized CameraController.', + ), + ), + ); + }); + + test('startImageStream() throws $CameraException when recording videos', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + + cameraController.value = + cameraController.value.copyWith(isRecordingVideo: true); + + expect( + () => cameraController.startImageStream((image) => null), + throwsA(isA().having( + (error) => error.description, + 'A video recording is already started.', + 'startImageStream was called while a video is being recorded.', + ))); + }); + test( + 'startImageStream() throws $CameraException when already streaming images', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + cameraController.value = + cameraController.value.copyWith(isStreamingImages: true); + expect( + () => cameraController.startImageStream((image) => null), + throwsA(isA().having( + (error) => error.description, + 'A camera has started streaming images.', + 'startImageStream was called while a camera was streaming images.', + ))); + }); + + test('startImageStream() calls CameraPlatform', () async { + MethodChannelMock cameraChannelMock = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'startImageStream': {}}); + MethodChannelMock streamChannelMock = MethodChannelMock( + channelName: 'plugins.flutter.io/camera/imageStream', + methods: {'listen': {}}); + + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.startImageStream((image) => null); + + expect(cameraChannelMock.log, + [isMethodCall('startImageStream', arguments: null)]); + expect(streamChannelMock.log, + [isMethodCall('listen', arguments: null)]); + }); + + test('stopImageStream() throws $CameraException when uninitialized', () { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + expect( + cameraController.stopImageStream, + throwsA( + isA() + .having( + (error) => error.code, + 'code', + 'Uninitialized CameraController', + ) + .having( + (error) => error.description, + 'description', + 'stopImageStream() was called on an uninitialized CameraController.', + ), + ), + ); + }); + + test('stopImageStream() throws $CameraException when recording videos', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.startImageStream((image) => null); + cameraController.value = + cameraController.value.copyWith(isRecordingVideo: true); + expect( + cameraController.stopImageStream, + throwsA(isA().having( + (error) => error.description, + 'A video recording is already started.', + 'stopImageStream was called while a video is being recorded.', + ))); + }); + + test('stopImageStream() throws $CameraException when not streaming images', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + expect( + cameraController.stopImageStream, + throwsA(isA().having( + (error) => error.description, + 'No camera is streaming images', + 'stopImageStream was called when no camera is streaming images.', + ))); + }); + + test('stopImageStream() intended behaviour', () async { + MethodChannelMock cameraChannelMock = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'startImageStream': {}, 'stopImageStream': {}}); + MethodChannelMock streamChannelMock = MethodChannelMock( + channelName: 'plugins.flutter.io/camera/imageStream', + methods: {'listen': {}, 'cancel': {}}); + + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + await cameraController.startImageStream((image) => null); + await cameraController.stopImageStream(); + + expect(cameraChannelMock.log, [ + isMethodCall('startImageStream', arguments: null), + isMethodCall('stopImageStream', arguments: null) + ]); + + expect(streamChannelMock.log, [ + isMethodCall('listen', arguments: null), + isMethodCall('cancel', arguments: null) + ]); + }); +} diff --git a/packages/camera/camera/test/camera_image_test.dart b/packages/camera/camera/test/camera_image_test.dart new file mode 100644 index 000000000000..2d827d983f3a --- /dev/null +++ b/packages/camera/camera/test/camera_image_test.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:typed_data'; + +import 'package:camera/camera.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('$CameraImage tests', () { + test('$CameraImage can be created', () { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + CameraImage cameraImage = CameraImage.fromPlatformData({ + 'format': 35, + 'height': 1, + 'width': 4, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'bytesPerPixel': 1, + 'bytesPerRow': 4, + 'height': 1, + 'width': 4 + } + ] + }); + expect(cameraImage.height, 1); + expect(cameraImage.width, 4); + expect(cameraImage.format.group, ImageFormatGroup.yuv420); + expect(cameraImage.planes.length, 1); + }); + + test('$CameraImage has ImageFormatGroup.yuv420 for iOS', () { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + + CameraImage cameraImage = CameraImage.fromPlatformData({ + 'format': 875704438, + 'height': 1, + 'width': 4, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'bytesPerPixel': 1, + 'bytesPerRow': 4, + 'height': 1, + 'width': 4 + } + ] + }); + expect(cameraImage.format.group, ImageFormatGroup.yuv420); + }); + + test('$CameraImage has ImageFormatGroup.yuv420 for Android', () { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + CameraImage cameraImage = CameraImage.fromPlatformData({ + 'format': 35, + 'height': 1, + 'width': 4, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'bytesPerPixel': 1, + 'bytesPerRow': 4, + 'height': 1, + 'width': 4 + } + ] + }); + expect(cameraImage.format.group, ImageFormatGroup.yuv420); + }); + + test('$CameraImage has ImageFormatGroup.bgra8888 for iOS', () { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + + CameraImage cameraImage = CameraImage.fromPlatformData({ + 'format': 1111970369, + 'height': 1, + 'width': 4, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'bytesPerPixel': 1, + 'bytesPerRow': 4, + 'height': 1, + 'width': 4 + } + ] + }); + expect(cameraImage.format.group, ImageFormatGroup.bgra8888); + }); + test('$CameraImage has ImageFormatGroup.unknown', () { + CameraImage cameraImage = CameraImage.fromPlatformData({ + 'format': null, + 'height': 1, + 'width': 4, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'bytesPerPixel': 1, + 'bytesPerRow': 4, + 'height': 1, + 'width': 4 + } + ] + }); + expect(cameraImage.format.group, ImageFormatGroup.unknown); + }); + }); +} diff --git a/packages/camera/camera/test/camera_preview_test.dart b/packages/camera/camera/test/camera_preview_test.dart new file mode 100644 index 000000000000..d579341c0e58 --- /dev/null +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -0,0 +1,239 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:camera/camera.dart'; +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:quiver/core.dart'; + +class FakeController extends ValueNotifier + implements CameraController { + FakeController() : super(const CameraValue.uninitialized()); + + @override + Future dispose() async { + super.dispose(); + } + + @override + Widget buildPreview() { + return Texture(textureId: CameraController.kUninitializedCameraId); + } + + @override + int get cameraId => CameraController.kUninitializedCameraId; + + @override + void debugCheckIsDisposed() {} + + @override + CameraDescription get description => CameraDescription( + name: '', lensDirection: CameraLensDirection.back, sensorOrientation: 0); + + @override + bool get enableAudio => false; + + @override + Future getExposureOffsetStepSize() async => 1.0; + + @override + Future getMaxExposureOffset() async => 1.0; + + @override + Future getMaxZoomLevel() async => 1.0; + + @override + Future getMinExposureOffset() async => 1.0; + + @override + Future getMinZoomLevel() async => 1.0; + + @override + ImageFormatGroup? get imageFormatGroup => null; + + @override + Future initialize() async {} + + @override + Future lockCaptureOrientation([DeviceOrientation? orientation]) async {} + + @override + Future pauseVideoRecording() async {} + + @override + Future prepareForVideoRecording() async {} + + @override + ResolutionPreset get resolutionPreset => ResolutionPreset.low; + + @override + Future resumeVideoRecording() async {} + + @override + Future setExposureMode(ExposureMode mode) async {} + + @override + Future setExposureOffset(double offset) async => offset; + + @override + Future setExposurePoint(Offset? point) async {} + + @override + Future setFlashMode(FlashMode mode) async {} + + @override + Future setFocusMode(FocusMode mode) async {} + + @override + Future setFocusPoint(Offset? point) async {} + + @override + Future setZoomLevel(double zoom) async {} + + @override + Future startImageStream(onAvailable) async {} + + @override + Future startVideoRecording() async {} + + @override + Future stopImageStream() async {} + + @override + Future stopVideoRecording() async => XFile(''); + + @override + Future takePicture() async => XFile(''); + + @override + Future unlockCaptureOrientation() async {} +} + +void main() { + group('RotatedBox (Android only)', () { + testWidgets( + 'when recording rotatedBox should turn according to recording orientation', + ( + WidgetTester tester, + ) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + final FakeController controller = FakeController(); + controller.value = controller.value.copyWith( + isInitialized: true, + isRecordingVideo: true, + deviceOrientation: DeviceOrientation.portraitUp, + lockedCaptureOrientation: + Optional.fromNullable(DeviceOrientation.landscapeRight), + recordingOrientation: + Optional.fromNullable(DeviceOrientation.landscapeLeft), + previewSize: Size(480, 640), + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: CameraPreview(controller), + ), + ); + expect(find.byType(RotatedBox), findsOneWidget); + + RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + expect(rotatedBox.quarterTurns, 1); + + debugDefaultTargetPlatformOverride = null; + }); + + testWidgets( + 'when orientation locked rotatedBox should turn according to locked orientation', + ( + WidgetTester tester, + ) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + final FakeController controller = FakeController(); + controller.value = controller.value.copyWith( + isInitialized: true, + deviceOrientation: DeviceOrientation.portraitUp, + lockedCaptureOrientation: + Optional.fromNullable(DeviceOrientation.landscapeRight), + recordingOrientation: + Optional.fromNullable(DeviceOrientation.landscapeLeft), + previewSize: Size(480, 640), + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: CameraPreview(controller), + ), + ); + expect(find.byType(RotatedBox), findsOneWidget); + + RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + expect(rotatedBox.quarterTurns, 3); + + debugDefaultTargetPlatformOverride = null; + }); + + testWidgets( + 'when not locked and not recording rotatedBox should turn according to device orientation', + ( + WidgetTester tester, + ) async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + + final FakeController controller = FakeController(); + controller.value = controller.value.copyWith( + isInitialized: true, + deviceOrientation: DeviceOrientation.portraitUp, + lockedCaptureOrientation: null, + recordingOrientation: + Optional.fromNullable(DeviceOrientation.landscapeLeft), + previewSize: Size(480, 640), + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: CameraPreview(controller), + ), + ); + expect(find.byType(RotatedBox), findsOneWidget); + + RotatedBox rotatedBox = + tester.widget(find.byType(RotatedBox)); + expect(rotatedBox.quarterTurns, 0); + + debugDefaultTargetPlatformOverride = null; + }); + }); + + testWidgets('when not on Android there should not be a rotated box', + (WidgetTester tester) async { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + final FakeController controller = FakeController(); + controller.value = controller.value.copyWith( + isInitialized: true, + previewSize: Size(480, 640), + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: CameraPreview(controller), + ), + ); + expect(find.byType(RotatedBox), findsNothing); + expect(find.byType(Texture), findsOneWidget); + debugDefaultTargetPlatformOverride = null; + }); +} diff --git a/packages/camera/camera/test/camera_test.dart b/packages/camera/camera/test/camera_test.dart new file mode 100644 index 000000000000..26382a9b7d60 --- /dev/null +++ b/packages/camera/camera/test/camera_test.dart @@ -0,0 +1,1389 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:math'; +import 'dart:ui'; + +import 'package:camera/camera.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +get mockAvailableCameras => [ + CameraDescription( + name: 'camBack', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + CameraDescription( + name: 'camFront', + lensDirection: CameraLensDirection.front, + sensorOrientation: 180), + ]; + +get mockInitializeCamera => 13; + +get mockOnCameraInitializedEvent => CameraInitializedEvent( + 13, + 75, + 75, + ExposureMode.auto, + true, + FocusMode.auto, + true, + ); + +get mockOnDeviceOrientationChangedEvent => + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + +get mockOnCameraClosingEvent => null; + +get mockOnCameraErrorEvent => CameraErrorEvent(13, 'closing'); + +XFile mockTakePicture = XFile('foo/bar.png'); + +get mockVideoRecordingXFile => null; + +bool mockPlatformException = false; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + + group('camera', () { + test('debugCheckIsDisposed should not throw assertion error when disposed', + () { + final MockCameraDescription description = MockCameraDescription(); + final CameraController controller = CameraController( + description, + ResolutionPreset.low, + ); + + controller.dispose(); + + expect(controller.debugCheckIsDisposed, returnsNormally); + }); + + test('debugCheckIsDisposed should throw assertion error when not disposed', + () { + final MockCameraDescription description = MockCameraDescription(); + final CameraController controller = CameraController( + description, + ResolutionPreset.low, + ); + + expect( + () => controller.debugCheckIsDisposed(), + throwsAssertionError, + ); + }); + + test('availableCameras() has camera', () async { + CameraPlatform.instance = MockCameraPlatform(); + + var camList = await availableCameras(); + + expect(camList, equals(mockAvailableCameras)); + }); + }); + + group('$CameraController', () { + setUpAll(() { + CameraPlatform.instance = MockCameraPlatform(); + }); + + test('Can be initialized', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + expect(cameraController.value.aspectRatio, 1); + expect(cameraController.value.previewSize, Size(75, 75)); + expect(cameraController.value.isInitialized, isTrue); + }); + + test('can be disposed', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + expect(cameraController.value.aspectRatio, 1); + expect(cameraController.value.previewSize, Size(75, 75)); + expect(cameraController.value.isInitialized, isTrue); + + await cameraController.dispose(); + + verify(CameraPlatform.instance.dispose(13)).called(1); + }); + + test('initialize() throws CameraException when disposed', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + expect(cameraController.value.aspectRatio, 1); + expect(cameraController.value.previewSize, Size(75, 75)); + expect(cameraController.value.isInitialized, isTrue); + + await cameraController.dispose(); + + verify(CameraPlatform.instance.dispose(13)).called(1); + + expect( + cameraController.initialize, + throwsA(isA().having( + (error) => error.description, + 'Error description', + 'initialize was called on a disposed CameraController', + ))); + }); + + test('initialize() throws $CameraException on $PlatformException ', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + mockPlatformException = true; + + expect( + cameraController.initialize, + throwsA(isA().having( + (error) => error.description, + 'foo', + 'bar', + ))); + mockPlatformException = false; + }); + + test('initialize() sets imageFormat', () async { + debugDefaultTargetPlatformOverride = TargetPlatform.android; + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max, + imageFormatGroup: ImageFormatGroup.yuv420, + ); + await cameraController.initialize(); + verify(CameraPlatform.instance + .initializeCamera(13, imageFormatGroup: ImageFormatGroup.yuv420)) + .called(1); + }); + + test('prepareForVideoRecording() calls $CameraPlatform ', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.prepareForVideoRecording(); + + verify(CameraPlatform.instance.prepareForVideoRecording()).called(1); + }); + + test('takePicture() throws $CameraException when uninitialized ', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + expect( + cameraController.takePicture(), + throwsA( + isA() + .having( + (error) => error.code, + 'code', + 'Uninitialized CameraController', + ) + .having( + (error) => error.description, + 'description', + 'takePicture() was called on an uninitialized CameraController.', + ), + ), + ); + }); + + test('takePicture() throws $CameraException when takePicture is true', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + cameraController.value = + cameraController.value.copyWith(isTakingPicture: true); + expect( + cameraController.takePicture(), + throwsA(isA().having( + (error) => error.description, + 'Previous capture has not returned yet.', + 'takePicture was called before the previous capture returned.', + ))); + }); + + test('takePicture() returns $XFile', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + XFile xFile = await cameraController.takePicture(); + + expect(xFile.path, mockTakePicture.path); + }); + + test('takePicture() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + mockPlatformException = true; + expect( + cameraController.takePicture(), + throwsA(isA().having( + (error) => error.description, + 'foo', + 'bar', + ))); + mockPlatformException = false; + }); + + test('startVideoRecording() throws $CameraException when uninitialized', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + expect( + cameraController.startVideoRecording(), + throwsA( + isA() + .having( + (error) => error.code, + 'code', + 'Uninitialized CameraController', + ) + .having( + (error) => error.description, + 'description', + 'startVideoRecording() was called on an uninitialized CameraController.', + ), + ), + ); + }); + test('startVideoRecording() throws $CameraException when recording videos', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + + cameraController.value = + cameraController.value.copyWith(isRecordingVideo: true); + + expect( + cameraController.startVideoRecording(), + throwsA(isA().having( + (error) => error.description, + 'A video recording is already started.', + 'startVideoRecording was called when a recording is already started.', + ))); + }); + + test( + 'startVideoRecording() throws $CameraException when already streaming images', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + + cameraController.value = + cameraController.value.copyWith(isStreamingImages: true); + + expect( + cameraController.startVideoRecording(), + throwsA(isA().having( + (error) => error.description, + 'A camera has started streaming images.', + 'startVideoRecording was called while a camera was streaming images.', + ))); + }); + + test('getMaxZoomLevel() throws $CameraException when uninitialized', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + expect( + cameraController.getMaxZoomLevel, + throwsA( + isA() + .having( + (error) => error.code, + 'code', + 'Uninitialized CameraController', + ) + .having( + (error) => error.description, + 'description', + 'getMaxZoomLevel() was called on an uninitialized CameraController.', + ), + ), + ); + }); + + test('getMaxZoomLevel() throws $CameraException when disposed', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + await cameraController.dispose(); + + expect( + cameraController.getMaxZoomLevel, + throwsA( + isA() + .having( + (error) => error.code, + 'code', + 'Disposed CameraController', + ) + .having( + (error) => error.description, + 'description', + 'getMaxZoomLevel() was called on a disposed CameraController.', + ), + ), + ); + }); + + test( + 'getMaxZoomLevel() throws $CameraException when a platform exception occured.', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + when(CameraPlatform.instance.getMaxZoomLevel(mockInitializeCamera)) + .thenThrow(CameraException( + 'TEST_ERROR', + 'This is a test error messge', + )); + + expect( + cameraController.getMaxZoomLevel, + throwsA(isA() + .having((error) => error.code, 'code', 'TEST_ERROR') + .having( + (error) => error.description, + 'description', + 'This is a test error messge', + ))); + }); + + test('getMaxZoomLevel() returns max zoom level.', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + when(CameraPlatform.instance.getMaxZoomLevel(mockInitializeCamera)) + .thenAnswer((_) => Future.value(42.0)); + + final maxZoomLevel = await cameraController.getMaxZoomLevel(); + expect(maxZoomLevel, 42.0); + }); + + test('getMinZoomLevel() throws $CameraException when uninitialized', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + expect( + cameraController.getMinZoomLevel, + throwsA( + isA() + .having( + (error) => error.code, + 'code', + 'Uninitialized CameraController', + ) + .having( + (error) => error.description, + 'description', + 'getMinZoomLevel() was called on an uninitialized CameraController.', + ), + ), + ); + }); + + test('getMinZoomLevel() throws $CameraException when disposed', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + await cameraController.dispose(); + + expect( + cameraController.getMinZoomLevel, + throwsA( + isA() + .having( + (error) => error.code, + 'code', + 'Disposed CameraController', + ) + .having( + (error) => error.description, + 'description', + 'getMinZoomLevel() was called on a disposed CameraController.', + ), + ), + ); + }); + + test( + 'getMinZoomLevel() throws $CameraException when a platform exception occured.', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + when(CameraPlatform.instance.getMinZoomLevel(mockInitializeCamera)) + .thenThrow(CameraException( + 'TEST_ERROR', + 'This is a test error messge', + )); + + expect( + cameraController.getMinZoomLevel, + throwsA(isA() + .having((error) => error.code, 'code', 'TEST_ERROR') + .having( + (error) => error.description, + 'description', + 'This is a test error messge', + ))); + }); + + test('getMinZoomLevel() returns max zoom level.', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + when(CameraPlatform.instance.getMinZoomLevel(mockInitializeCamera)) + .thenAnswer((_) => Future.value(42.0)); + + final maxZoomLevel = await cameraController.getMinZoomLevel(); + expect(maxZoomLevel, 42.0); + }); + + test('setZoomLevel() throws $CameraException when uninitialized', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + expect( + () => cameraController.setZoomLevel(42.0), + throwsA( + isA() + .having( + (error) => error.code, + 'code', + 'Uninitialized CameraController', + ) + .having( + (error) => error.description, + 'description', + 'setZoomLevel() was called on an uninitialized CameraController.', + ), + ), + ); + }); + + test('setZoomLevel() throws $CameraException when disposed', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + await cameraController.dispose(); + + expect( + () => cameraController.setZoomLevel(42.0), + throwsA( + isA() + .having( + (error) => error.code, + 'code', + 'Disposed CameraController', + ) + .having( + (error) => error.description, + 'description', + 'setZoomLevel() was called on a disposed CameraController.', + ), + ), + ); + }); + + test( + 'setZoomLevel() throws $CameraException when a platform exception occured.', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + when(CameraPlatform.instance.setZoomLevel(mockInitializeCamera, 42.0)) + .thenThrow(CameraException( + 'TEST_ERROR', + 'This is a test error messge', + )); + + expect( + () => cameraController.setZoomLevel(42), + throwsA(isA() + .having((error) => error.code, 'code', 'TEST_ERROR') + .having( + (error) => error.description, + 'description', + 'This is a test error messge', + ))); + + reset(CameraPlatform.instance); + }); + + test( + 'setZoomLevel() completes and calls method channel with correct value.', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + + await cameraController.initialize(); + await cameraController.setZoomLevel(42.0); + + verify(CameraPlatform.instance.setZoomLevel(mockInitializeCamera, 42.0)) + .called(1); + }); + + test('setFlashMode() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.setFlashMode(FlashMode.always); + + verify(CameraPlatform.instance + .setFlashMode(cameraController.cameraId, FlashMode.always)) + .called(1); + }); + + test('setFlashMode() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + when(CameraPlatform.instance + .setFlashMode(cameraController.cameraId, FlashMode.always)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.setFlashMode(FlashMode.always), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test('setExposureMode() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.setExposureMode(ExposureMode.auto); + + verify(CameraPlatform.instance + .setExposureMode(cameraController.cameraId, ExposureMode.auto)) + .called(1); + }); + + test('setExposureMode() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + when(CameraPlatform.instance + .setExposureMode(cameraController.cameraId, ExposureMode.auto)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.setExposureMode(ExposureMode.auto), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test('setExposurePoint() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.setExposurePoint(Offset(0.5, 0.5)); + + verify(CameraPlatform.instance.setExposurePoint( + cameraController.cameraId, Point(0.5, 0.5))) + .called(1); + }); + + test('setExposurePoint() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + when(CameraPlatform.instance.setExposurePoint( + cameraController.cameraId, Point(0.5, 0.5))) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.setExposurePoint(Offset(0.5, 0.5)), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test('getMinExposureOffset() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + when(CameraPlatform.instance + .getMinExposureOffset(cameraController.cameraId)) + .thenAnswer((_) => Future.value(0.0)); + + await cameraController.getMinExposureOffset(); + + verify(CameraPlatform.instance + .getMinExposureOffset(cameraController.cameraId)) + .called(1); + }); + + test('getMinExposureOffset() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + when(CameraPlatform.instance + .getMinExposureOffset(cameraController.cameraId)) + .thenThrow( + CameraException( + 'TEST_ERROR', + 'This is a test error message', + ), + ); + + expect( + cameraController.getMinExposureOffset(), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test('getMaxExposureOffset() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + when(CameraPlatform.instance + .getMaxExposureOffset(cameraController.cameraId)) + .thenAnswer((_) => Future.value(1.0)); + + await cameraController.getMaxExposureOffset(); + + verify(CameraPlatform.instance + .getMaxExposureOffset(cameraController.cameraId)) + .called(1); + }); + + test('getMaxExposureOffset() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + when(CameraPlatform.instance + .getMaxExposureOffset(cameraController.cameraId)) + .thenThrow( + CameraException( + 'TEST_ERROR', + 'This is a test error message', + ), + ); + + expect( + cameraController.getMaxExposureOffset(), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test('getExposureOffsetStepSize() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + when(CameraPlatform.instance + .getExposureOffsetStepSize(cameraController.cameraId)) + .thenAnswer((_) => Future.value(0.0)); + + await cameraController.getExposureOffsetStepSize(); + + verify(CameraPlatform.instance + .getExposureOffsetStepSize(cameraController.cameraId)) + .called(1); + }); + + test( + 'getExposureOffsetStepSize() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + when(CameraPlatform.instance + .getExposureOffsetStepSize(cameraController.cameraId)) + .thenThrow( + CameraException( + 'TEST_ERROR', + 'This is a test error message', + ), + ); + + expect( + cameraController.getExposureOffsetStepSize(), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test('setExposureOffset() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + when(CameraPlatform.instance + .getMinExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => -1.0); + when(CameraPlatform.instance + .getMaxExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => 2.0); + when(CameraPlatform.instance + .getExposureOffsetStepSize(cameraController.cameraId)) + .thenAnswer((_) async => 1.0); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 1.0)) + .thenAnswer((_) async => 1.0); + + await cameraController.setExposureOffset(1.0); + + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 1.0)) + .called(1); + }); + + test('setExposureOffset() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + when(CameraPlatform.instance + .getMinExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => -1.0); + when(CameraPlatform.instance + .getMaxExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => 2.0); + when(CameraPlatform.instance + .getExposureOffsetStepSize(cameraController.cameraId)) + .thenAnswer((_) async => 1.0); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 1.0)) + .thenThrow( + CameraException( + 'TEST_ERROR', + 'This is a test error message', + ), + ); + + expect( + cameraController.setExposureOffset(1.0), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test( + 'setExposureOffset() throws $CameraException when offset is out of bounds', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + when(CameraPlatform.instance + .getMinExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => -1.0); + when(CameraPlatform.instance + .getMaxExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => 2.0); + when(CameraPlatform.instance + .getExposureOffsetStepSize(cameraController.cameraId)) + .thenAnswer((_) async => 1.0); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 0.0)) + .thenAnswer((_) async => 0.0); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, -1.0)) + .thenAnswer((_) async => 0.0); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 2.0)) + .thenAnswer((_) async => 0.0); + + expect( + cameraController.setExposureOffset(3.0), + throwsA(isA().having( + (error) => error.description, + 'exposureOffsetOutOfBounds', + 'The provided exposure offset was outside the supported range for this device.', + ))); + expect( + cameraController.setExposureOffset(-2.0), + throwsA(isA().having( + (error) => error.description, + 'exposureOffsetOutOfBounds', + 'The provided exposure offset was outside the supported range for this device.', + ))); + + await cameraController.setExposureOffset(0.0); + await cameraController.setExposureOffset(-1.0); + await cameraController.setExposureOffset(2.0); + + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 0.0)) + .called(1); + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, -1.0)) + .called(1); + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 2.0)) + .called(1); + }); + + test('setExposureOffset() rounds offset to nearest step', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + when(CameraPlatform.instance + .getMinExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => -1.2); + when(CameraPlatform.instance + .getMaxExposureOffset(cameraController.cameraId)) + .thenAnswer((_) async => 1.2); + when(CameraPlatform.instance + .getExposureOffsetStepSize(cameraController.cameraId)) + .thenAnswer((_) async => 0.4); + + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, -1.2)) + .thenAnswer((_) async => -1.2); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, -0.8)) + .thenAnswer((_) async => -0.8); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, -0.4)) + .thenAnswer((_) async => -0.4); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 0.0)) + .thenAnswer((_) async => 0.0); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 0.4)) + .thenAnswer((_) async => 0.4); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 0.8)) + .thenAnswer((_) async => 0.8); + when(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 1.2)) + .thenAnswer((_) async => 1.2); + + await cameraController.setExposureOffset(1.2); + await cameraController.setExposureOffset(-1.2); + await cameraController.setExposureOffset(0.1); + await cameraController.setExposureOffset(0.2); + await cameraController.setExposureOffset(0.3); + await cameraController.setExposureOffset(0.4); + await cameraController.setExposureOffset(0.5); + await cameraController.setExposureOffset(0.6); + await cameraController.setExposureOffset(0.7); + await cameraController.setExposureOffset(-0.1); + await cameraController.setExposureOffset(-0.2); + await cameraController.setExposureOffset(-0.3); + await cameraController.setExposureOffset(-0.4); + await cameraController.setExposureOffset(-0.5); + await cameraController.setExposureOffset(-0.6); + await cameraController.setExposureOffset(-0.7); + + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 0.8)) + .called(2); + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, -0.8)) + .called(2); + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 0.0)) + .called(2); + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, 0.4)) + .called(4); + verify(CameraPlatform.instance + .setExposureOffset(cameraController.cameraId, -0.4)) + .called(4); + }); + + test('lockCaptureOrientation() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.lockCaptureOrientation(); + expect(cameraController.value.lockedCaptureOrientation, + equals(DeviceOrientation.portraitUp)); + await cameraController + .lockCaptureOrientation(DeviceOrientation.landscapeRight); + expect(cameraController.value.lockedCaptureOrientation, + equals(DeviceOrientation.landscapeRight)); + + verify(CameraPlatform.instance.lockCaptureOrientation( + cameraController.cameraId, DeviceOrientation.portraitUp)) + .called(1); + verify(CameraPlatform.instance.lockCaptureOrientation( + cameraController.cameraId, DeviceOrientation.landscapeRight)) + .called(1); + }); + + test( + 'lockCaptureOrientation() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + when(CameraPlatform.instance.lockCaptureOrientation( + cameraController.cameraId, DeviceOrientation.portraitUp)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.lockCaptureOrientation(DeviceOrientation.portraitUp), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test('unlockCaptureOrientation() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + + await cameraController.unlockCaptureOrientation(); + expect(cameraController.value.lockedCaptureOrientation, equals(null)); + + verify(CameraPlatform.instance + .unlockCaptureOrientation(cameraController.cameraId)) + .called(1); + }); + + test( + 'unlockCaptureOrientation() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + when(CameraPlatform.instance + .unlockCaptureOrientation(cameraController.cameraId)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.unlockCaptureOrientation(), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + }); +} + +class MockCameraPlatform extends Mock + with MockPlatformInterfaceMixin + implements CameraPlatform { + @override + Future initializeCamera( + int? cameraId, { + ImageFormatGroup? imageFormatGroup = ImageFormatGroup.unknown, + }) async => + super.noSuchMethod(Invocation.method( + #initializeCamera, + [cameraId], + { + #imageFormatGroup: imageFormatGroup, + }, + )); + + @override + Future dispose(int? cameraId) async { + return super.noSuchMethod(Invocation.method(#dispose, [cameraId])); + } + + @override + Future> availableCameras() => + Future.value(mockAvailableCameras); + + @override + Future createCamera( + CameraDescription description, + ResolutionPreset? resolutionPreset, { + bool enableAudio = false, + }) => + mockPlatformException + ? throw PlatformException(code: 'foo', message: 'bar') + : Future.value(mockInitializeCamera); + + @override + Stream onCameraInitialized(int cameraId) => + Stream.value(mockOnCameraInitializedEvent); + + @override + Stream onCameraClosing(int cameraId) => + Stream.value(mockOnCameraClosingEvent); + + @override + Stream onCameraError(int cameraId) => + Stream.value(mockOnCameraErrorEvent); + + @override + Stream onDeviceOrientationChanged() => + Stream.value(mockOnDeviceOrientationChangedEvent); + + @override + Future takePicture(int cameraId) => mockPlatformException + ? throw PlatformException(code: 'foo', message: 'bar') + : Future.value(mockTakePicture); + + @override + Future prepareForVideoRecording() async => + super.noSuchMethod(Invocation.method(#prepareForVideoRecording, null)); + + @override + Future startVideoRecording(int cameraId, + {Duration? maxVideoDuration}) => + Future.value(mockVideoRecordingXFile); + + @override + Future lockCaptureOrientation( + int? cameraId, DeviceOrientation? orientation) async => + super.noSuchMethod( + Invocation.method(#lockCaptureOrientation, [cameraId, orientation])); + + @override + Future unlockCaptureOrientation(int? cameraId) async => super + .noSuchMethod(Invocation.method(#unlockCaptureOrientation, [cameraId])); + + @override + Future getMaxZoomLevel(int? cameraId) async => super.noSuchMethod( + Invocation.method(#getMaxZoomLevel, [cameraId]), + returnValue: 1.0, + ); + + @override + Future getMinZoomLevel(int? cameraId) async => super.noSuchMethod( + Invocation.method(#getMinZoomLevel, [cameraId]), + returnValue: 0.0, + ); + + @override + Future setZoomLevel(int? cameraId, double? zoom) async => + super.noSuchMethod(Invocation.method(#setZoomLevel, [cameraId, zoom])); + + @override + Future setFlashMode(int? cameraId, FlashMode? mode) async => + super.noSuchMethod(Invocation.method(#setFlashMode, [cameraId, mode])); + + @override + Future setExposureMode(int? cameraId, ExposureMode? mode) async => + super.noSuchMethod(Invocation.method(#setExposureMode, [cameraId, mode])); + + @override + Future setExposurePoint(int? cameraId, Point? point) async => + super.noSuchMethod( + Invocation.method(#setExposurePoint, [cameraId, point])); + + @override + Future getMinExposureOffset(int? cameraId) async => + super.noSuchMethod( + Invocation.method(#getMinExposureOffset, [cameraId]), + returnValue: 0.0, + ); + + @override + Future getMaxExposureOffset(int? cameraId) async => + super.noSuchMethod( + Invocation.method(#getMaxExposureOffset, [cameraId]), + returnValue: 1.0, + ); + + @override + Future getExposureOffsetStepSize(int? cameraId) async => + super.noSuchMethod( + Invocation.method(#getExposureOffsetStepSize, [cameraId]), + returnValue: 1.0, + ); + + @override + Future setExposureOffset(int? cameraId, double? offset) async => + super.noSuchMethod( + Invocation.method(#setExposureOffset, [cameraId, offset]), + returnValue: 1.0, + ); +} + +class MockCameraDescription extends CameraDescription { + /// Creates a new camera description with the given properties. + MockCameraDescription() + : super( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ); + + @override + CameraLensDirection get lensDirection => CameraLensDirection.back; + + @override + String get name => 'back'; +} diff --git a/packages/camera/camera/test/camera_value_test.dart b/packages/camera/camera/test/camera_value_test.dart new file mode 100644 index 000000000000..e0378cca2cb9 --- /dev/null +++ b/packages/camera/camera/test/camera_value_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 'dart:ui'; + +import 'package:camera/camera.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('camera_value', () { + test('Can be created', () { + var cameraValue = const CameraValue( + isInitialized: false, + errorDescription: null, + previewSize: Size(10, 10), + isRecordingPaused: false, + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + flashMode: FlashMode.auto, + exposureMode: ExposureMode.auto, + exposurePointSupported: true, + focusMode: FocusMode.auto, + deviceOrientation: DeviceOrientation.portraitUp, + lockedCaptureOrientation: DeviceOrientation.portraitUp, + recordingOrientation: DeviceOrientation.portraitUp, + focusPointSupported: true, + ); + + expect(cameraValue, isA()); + expect(cameraValue.isInitialized, isFalse); + expect(cameraValue.errorDescription, null); + expect(cameraValue.previewSize, Size(10, 10)); + expect(cameraValue.isRecordingPaused, isFalse); + expect(cameraValue.isRecordingVideo, isFalse); + expect(cameraValue.isTakingPicture, isFalse); + expect(cameraValue.isStreamingImages, isFalse); + expect(cameraValue.flashMode, FlashMode.auto); + expect(cameraValue.exposureMode, ExposureMode.auto); + expect(cameraValue.exposurePointSupported, true); + expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); + expect( + cameraValue.lockedCaptureOrientation, DeviceOrientation.portraitUp); + expect(cameraValue.recordingOrientation, DeviceOrientation.portraitUp); + }); + + test('Can be created as uninitialized', () { + var cameraValue = const CameraValue.uninitialized(); + + expect(cameraValue, isA()); + expect(cameraValue.isInitialized, isFalse); + expect(cameraValue.errorDescription, null); + expect(cameraValue.previewSize, null); + expect(cameraValue.isRecordingPaused, isFalse); + expect(cameraValue.isRecordingVideo, isFalse); + expect(cameraValue.isTakingPicture, isFalse); + expect(cameraValue.isStreamingImages, isFalse); + expect(cameraValue.flashMode, FlashMode.auto); + expect(cameraValue.exposureMode, ExposureMode.auto); + expect(cameraValue.exposurePointSupported, false); + expect(cameraValue.focusMode, FocusMode.auto); + expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); + expect(cameraValue.lockedCaptureOrientation, null); + expect(cameraValue.recordingOrientation, null); + }); + + test('Can be copied with isInitialized', () { + var cv = const CameraValue.uninitialized(); + var cameraValue = cv.copyWith(isInitialized: true); + + expect(cameraValue, isA()); + expect(cameraValue.isInitialized, isTrue); + expect(cameraValue.errorDescription, null); + expect(cameraValue.previewSize, null); + expect(cameraValue.isRecordingPaused, isFalse); + expect(cameraValue.isRecordingVideo, isFalse); + expect(cameraValue.isTakingPicture, isFalse); + expect(cameraValue.isStreamingImages, isFalse); + expect(cameraValue.flashMode, FlashMode.auto); + expect(cameraValue.focusMode, FocusMode.auto); + expect(cameraValue.exposureMode, ExposureMode.auto); + expect(cameraValue.exposurePointSupported, false); + expect(cameraValue.deviceOrientation, DeviceOrientation.portraitUp); + expect(cameraValue.lockedCaptureOrientation, null); + expect(cameraValue.recordingOrientation, null); + }); + + test('Has aspectRatio after setting size', () { + var cv = const CameraValue.uninitialized(); + var cameraValue = + cv.copyWith(isInitialized: true, previewSize: Size(20, 10)); + + expect(cameraValue.aspectRatio, 2.0); + }); + + test('hasError is true after setting errorDescription', () { + var cv = const CameraValue.uninitialized(); + var cameraValue = cv.copyWith(errorDescription: 'error'); + + expect(cameraValue.hasError, isTrue); + expect(cameraValue.errorDescription, 'error'); + }); + + test('Recording paused is false when not recording', () { + var cv = const CameraValue.uninitialized(); + var cameraValue = cv.copyWith( + isInitialized: true, + isRecordingVideo: false, + isRecordingPaused: true); + + expect(cameraValue.isRecordingPaused, isFalse); + }); + + test('toString() works as expected', () { + var cameraValue = const CameraValue( + isInitialized: false, + errorDescription: null, + previewSize: Size(10, 10), + isRecordingPaused: false, + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + flashMode: FlashMode.auto, + exposureMode: ExposureMode.auto, + focusMode: FocusMode.auto, + exposurePointSupported: true, + focusPointSupported: true, + deviceOrientation: DeviceOrientation.portraitUp, + lockedCaptureOrientation: DeviceOrientation.portraitUp, + recordingOrientation: DeviceOrientation.portraitUp, + ); + + expect(cameraValue.toString(), + 'CameraValue(isRecordingVideo: false, isInitialized: false, errorDescription: null, previewSize: Size(10.0, 10.0), isStreamingImages: false, flashMode: FlashMode.auto, exposureMode: ExposureMode.auto, focusMode: FocusMode.auto, exposurePointSupported: true, focusPointSupported: true, deviceOrientation: DeviceOrientation.portraitUp, lockedCaptureOrientation: DeviceOrientation.portraitUp, recordingOrientation: DeviceOrientation.portraitUp)'); + }); + }); +} diff --git a/packages/camera/camera/test/utils/method_channel_mock.dart b/packages/camera/camera/test/utils/method_channel_mock.dart new file mode 100644 index 000000000000..60d8def6a2e3 --- /dev/null +++ b/packages/camera/camera/test/utils/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 { + final Duration? delay; + final MethodChannel methodChannel; + final Map methods; + final log = []; + + MethodChannelMock({ + required String channelName, + this.delay, + required this.methods, + }) : methodChannel = MethodChannel(channelName) { + methodChannel.setMockMethodCallHandler(_handler); + } + + Future _handler(MethodCall methodCall) async { + log.add(methodCall); + + if (!methods.containsKey(methodCall.method)) { + throw MissingPluginException('No implementation found for method ' + '${methodCall.method} on channel ${methodChannel.name}'); + } + + return Future.delayed(delay ?? Duration.zero, () { + final result = methods[methodCall.method]; + if (result is Exception) { + throw result; + } + + return Future.value(result); + }); + } +} diff --git a/packages/camera/camera_platform_interface/AUTHORS b/packages/camera/camera_platform_interface/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/camera/camera_platform_interface/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/camera/camera_platform_interface/CHANGELOG.md b/packages/camera/camera_platform_interface/CHANGELOG.md new file mode 100644 index 000000000000..49214d24d18e --- /dev/null +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -0,0 +1,52 @@ +## 2.0.1 + +* Update platform_plugin_interface version requirement. + +## 2.0.0 + +- Stable null safety release. + +## 1.6.0 + +- Added VideoRecordedEvent to support ending a video recording in the native implementation. + +## 1.5.0 + +- Introduces interface methods for locking and unlocking the capture orientation. +- Introduces interface method for listening to the device orientation. + +## 1.4.0 + +- Added interface methods to support auto focus. + +## 1.3.0 + +- Introduces an option to set the image format when initializing. + +## 1.2.0 + +- Added interface to support automatic exposure. + +## 1.1.0 + +- Added an optional `maxVideoDuration` parameter to the `startVideoRecording` method, which allows implementations to limit the duration of a video recording. + +## 1.0.4 + +- Added the torch option to the FlashMode enum, which when implemented indicates the flash light should be turned on continuously. + +## 1.0.3 + +- Update Flutter SDK constraint. + +## 1.0.2 + +- Added interface methods to support zoom features. + +## 1.0.1 + +- Added interface methods for setting flash mode. + +## 1.0.0 + +- Initial open-source release diff --git a/packages/camera/camera_platform_interface/LICENSE b/packages/camera/camera_platform_interface/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/camera/camera_platform_interface/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/camera/camera_platform_interface/README.md b/packages/camera/camera_platform_interface/README.md new file mode 100644 index 000000000000..43be651935b5 --- /dev/null +++ b/packages/camera/camera_platform_interface/README.md @@ -0,0 +1,26 @@ +# camera_platform_interface + +A common platform interface for the [`camera`][1] plugin. + +This interface allows platform-specific implementations of the `camera` +plugin, as well as the plugin itself, to ensure they are supporting the +same interface. + +# Usage + +To implement a new platform-specific implementation of `camera`, extend +[`CameraPlatform`][2] with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`CameraPlatform` by calling +`CameraPlatform.instance = MyPlatformCamera()`. + +# Note on breaking changes + +Strongly prefer non-breaking changes (such as adding a method to the interface) +over breaking changes for this package. + +See https://flutter.dev/go/platform-interface-breaking-changes for a discussion +on why a less-clean interface is preferable to a breaking change. + +[1]: ../camera +[2]: lib/camera_platform_interface.dart diff --git a/packages/camera/camera_platform_interface/lib/camera_platform_interface.dart b/packages/camera/camera_platform_interface/lib/camera_platform_interface.dart new file mode 100644 index 000000000000..3ec66dd54894 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/camera_platform_interface.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. + +export 'src/events/camera_event.dart'; +export 'src/events/device_event.dart'; +export 'src/platform_interface/camera_platform.dart'; +export 'src/types/types.dart'; + +/// Expose XFile +export 'package:cross_file/cross_file.dart'; diff --git a/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart b/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart new file mode 100644 index 000000000000..591d5a336356 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/events/camera_event.dart @@ -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. + +import 'package:camera_platform_interface/src/types/focus_mode.dart'; + +import '../../camera_platform_interface.dart'; + +/// Generic Event coming from the native side of Camera, +/// related to a specific camera module. +/// +/// All [CameraEvent]s contain the `cameraId` that originated the event. This +/// should never be `null`. +/// +/// This class is used as a base class for all the events that might be +/// triggered from a Camera, but it is never used directly as an event type. +/// +/// Do NOT instantiate new events like `CameraEvent(cameraId)` directly, +/// use a specific class instead: +/// +/// Do `class NewEvent extend CameraEvent` when creating your own events. +/// See below for examples: `CameraClosingEvent`, `CameraErrorEvent`... +/// These events are more semantic and more pleasant to use than raw generics. +/// They can be (and in fact, are) filtered by the `instanceof`-operator. +abstract class CameraEvent { + /// The ID of the Camera this event is associated to. + final int cameraId; + + /// Build a Camera Event, that relates a `cameraId`. + /// + /// The `cameraId` is the ID of the camera that triggered the event. + CameraEvent(this.cameraId) : assert(cameraId != null); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is CameraEvent && + runtimeType == other.runtimeType && + cameraId == other.cameraId; + + @override + int get hashCode => cameraId.hashCode; +} + +/// An event fired when the camera has finished initializing. +class CameraInitializedEvent extends CameraEvent { + /// The width of the preview in pixels. + final double previewWidth; + + /// The height of the preview in pixels. + final double previewHeight; + + /// The default exposure mode + final ExposureMode exposureMode; + + /// The default focus mode + final FocusMode focusMode; + + /// Whether setting exposure points is supported. + final bool exposurePointSupported; + + /// Whether setting focus points is supported. + final bool focusPointSupported; + + /// Build a CameraInitialized event triggered from the camera represented by + /// `cameraId`. + /// + /// The `previewWidth` represents the width of the generated preview in pixels. + /// The `previewHeight` represents the height of the generated preview in pixels. + CameraInitializedEvent( + int cameraId, + this.previewWidth, + this.previewHeight, + this.exposureMode, + this.exposurePointSupported, + this.focusMode, + this.focusPointSupported, + ) : super(cameraId); + + /// Converts the supplied [Map] to an instance of the [CameraInitializedEvent] + /// class. + CameraInitializedEvent.fromJson(Map json) + : previewWidth = json['previewWidth'], + previewHeight = json['previewHeight'], + exposureMode = deserializeExposureMode(json['exposureMode']), + exposurePointSupported = json['exposurePointSupported'] ?? false, + focusMode = deserializeFocusMode(json['focusMode']), + focusPointSupported = json['focusPointSupported'] ?? false, + super(json['cameraId']); + + /// Converts the [CameraInitializedEvent] instance into a [Map] instance that + /// can be serialized to JSON. + Map toJson() => { + 'cameraId': cameraId, + 'previewWidth': previewWidth, + 'previewHeight': previewHeight, + 'exposureMode': serializeExposureMode(exposureMode), + 'exposurePointSupported': exposurePointSupported, + 'focusMode': serializeFocusMode(focusMode), + 'focusPointSupported': focusPointSupported, + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + super == other && + other is CameraInitializedEvent && + runtimeType == other.runtimeType && + previewWidth == other.previewWidth && + previewHeight == other.previewHeight && + exposureMode == other.exposureMode && + exposurePointSupported == other.exposurePointSupported && + focusMode == other.focusMode && + focusPointSupported == other.focusPointSupported; + + @override + int get hashCode => + super.hashCode ^ + previewWidth.hashCode ^ + previewHeight.hashCode ^ + exposureMode.hashCode ^ + exposurePointSupported.hashCode ^ + focusMode.hashCode ^ + focusPointSupported.hashCode; +} + +/// An event fired when the resolution preset of the camera has changed. +class CameraResolutionChangedEvent extends CameraEvent { + /// The capture width in pixels. + final double captureWidth; + + /// The capture height in pixels. + final double captureHeight; + + /// Build a CameraResolutionChanged event triggered from the camera + /// represented by `cameraId`. + /// + /// The `captureWidth` represents the width of the resulting image in pixels. + /// The `captureHeight` represents the height of the resulting image in pixels. + CameraResolutionChangedEvent( + int cameraId, + this.captureWidth, + this.captureHeight, + ) : super(cameraId); + + /// Converts the supplied [Map] to an instance of the + /// [CameraResolutionChangedEvent] class. + CameraResolutionChangedEvent.fromJson(Map json) + : captureWidth = json['captureWidth'], + captureHeight = json['captureHeight'], + super(json['cameraId']); + + /// Converts the [CameraResolutionChangedEvent] instance into a [Map] instance + /// that can be serialized to JSON. + Map toJson() => { + 'cameraId': cameraId, + 'captureWidth': captureWidth, + 'captureHeight': captureHeight, + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is CameraResolutionChangedEvent && + super == (other) && + runtimeType == other.runtimeType && + captureWidth == other.captureWidth && + captureHeight == other.captureHeight; + + @override + int get hashCode => + super.hashCode ^ captureWidth.hashCode ^ captureHeight.hashCode; +} + +/// An event fired when the camera is going to close. +class CameraClosingEvent extends CameraEvent { + /// Build a CameraClosing event triggered from the camera represented by + /// `cameraId`. + CameraClosingEvent(int cameraId) : super(cameraId); + + /// Converts the supplied [Map] to an instance of the [CameraClosingEvent] + /// class. + CameraClosingEvent.fromJson(Map json) + : super(json['cameraId']); + + /// Converts the [CameraClosingEvent] instance into a [Map] instance that can + /// be serialized to JSON. + Map toJson() => { + 'cameraId': cameraId, + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + super == (other) && + other is CameraClosingEvent && + runtimeType == other.runtimeType; + + @override + int get hashCode => super.hashCode; +} + +/// An event fired when an error occured while operating the camera. +class CameraErrorEvent extends CameraEvent { + /// Description of the error. + final String description; + + /// Build a CameraError event triggered from the camera represented by + /// `cameraId`. + /// + /// The `description` represents the error occured on the camera. + CameraErrorEvent(int cameraId, this.description) : super(cameraId); + + /// Converts the supplied [Map] to an instance of the [CameraErrorEvent] + /// class. + CameraErrorEvent.fromJson(Map json) + : description = json['description'], + super(json['cameraId']); + + /// Converts the [CameraErrorEvent] instance into a [Map] instance that can be + /// serialized to JSON. + Map toJson() => { + 'cameraId': cameraId, + 'description': description, + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + super == (other) && + other is CameraErrorEvent && + runtimeType == other.runtimeType && + description == other.description; + + @override + int get hashCode => super.hashCode ^ description.hashCode; +} + +/// An event fired when a video has finished recording. +class VideoRecordedEvent extends CameraEvent { + /// XFile of the recorded video. + final XFile file; + + /// Maximum duration of the recorded video. + final Duration? maxVideoDuration; + + /// Build a VideoRecordedEvent triggered from the camera with the `cameraId`. + /// + /// The `file` represents the file of the video. + /// The `maxVideoDuration` shows if a maxVideoDuration shows if a maximum + /// video duration was set. + VideoRecordedEvent(int cameraId, this.file, this.maxVideoDuration) + : super(cameraId); + + /// Converts the supplied [Map] to an instance of the [VideoRecordedEvent] + /// class. + VideoRecordedEvent.fromJson(Map json) + : file = XFile(json['path']), + maxVideoDuration = json['maxVideoDuration'] != null + ? Duration(milliseconds: json['maxVideoDuration'] as int) + : null, + super(json['cameraId']); + + /// Converts the [VideoRecordedEvent] instance into a [Map] instance that can be + /// serialized to JSON. + Map toJson() => { + 'cameraId': cameraId, + 'path': file.path, + 'maxVideoDuration': maxVideoDuration?.inMilliseconds + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + super == other && + other is VideoRecordedEvent && + runtimeType == other.runtimeType && + maxVideoDuration == other.maxVideoDuration; + + @override + int get hashCode => + super.hashCode ^ file.hashCode ^ maxVideoDuration.hashCode; +} diff --git a/packages/camera/camera_platform_interface/lib/src/events/device_event.dart b/packages/camera/camera_platform_interface/lib/src/events/device_event.dart new file mode 100644 index 000000000000..c6cedd135fed --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/events/device_event.dart @@ -0,0 +1,52 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera_platform_interface/src/utils/utils.dart'; +import 'package:flutter/services.dart'; + +/// Generic Event coming from the native side of Camera, +/// not related to a specific camera module. +/// +/// This class is used as a base class for all the events that might be +/// triggered from a device, but it is never used directly as an event type. +/// +/// Do NOT instantiate new events like `DeviceEvent()` directly, +/// use a specific class instead: +/// +/// Do `class NewEvent extend DeviceEvent` when creating your own events. +/// See below for examples: `DeviceOrientationChangedEvent`... +/// These events are more semantic and more pleasant to use than raw generics. +/// They can be (and in fact, are) filtered by the `instanceof`-operator. +abstract class DeviceEvent {} + +/// The [DeviceOrientationChangedEvent] is fired every time the user changes the +/// physical orientation of the device. +class DeviceOrientationChangedEvent extends DeviceEvent { + /// The new orientation of the device + final DeviceOrientation orientation; + + /// Build a new orientation changed event. + DeviceOrientationChangedEvent(this.orientation); + + /// Converts the supplied [Map] to an instance of the [DeviceOrientationChangedEvent] + /// class. + DeviceOrientationChangedEvent.fromJson(Map json) + : orientation = deserializeDeviceOrientation(json['orientation']); + + /// Converts the [DeviceOrientationChangedEvent] instance into a [Map] instance that + /// can be serialized to JSON. + Map toJson() => { + 'orientation': serializeDeviceOrientation(orientation), + }; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is DeviceOrientationChangedEvent && + runtimeType == other.runtimeType && + orientation == other.orientation; + + @override + int get hashCode => orientation.hashCode; +} diff --git a/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart new file mode 100644 index 000000000000..c6c363a56d65 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart @@ -0,0 +1,507 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:math'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/events/device_event.dart'; +import 'package:camera_platform_interface/src/types/focus_mode.dart'; +import 'package:camera_platform_interface/src/types/image_format_group.dart'; +import 'package:camera_platform_interface/src/utils/utils.dart'; +import 'package:cross_file/cross_file.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:meta/meta.dart'; +import 'package:stream_transform/stream_transform.dart'; + +const MethodChannel _channel = MethodChannel('plugins.flutter.io/camera'); + +/// An implementation of [CameraPlatform] that uses method channels. +class MethodChannelCamera extends CameraPlatform { + final Map _channels = {}; + + /// The controller we need to broadcast the different events coming + /// from handleMethodCall, specific to camera events. + /// + /// It is a `broadcast` because multiple controllers will connect to + /// different stream views of this Controller. + /// This is only exposed for test purposes. It shouldn't be used by clients of + /// the plugin as it may break or change at any time. + @visibleForTesting + final StreamController cameraEventStreamController = + StreamController.broadcast(); + + /// The controller we need to broadcast the different events coming + /// from handleMethodCall, specific to general device events. + /// + /// It is a `broadcast` because multiple controllers will connect to + /// different stream views of this Controller. + /// This is only exposed for test purposes. It shouldn't be used by clients of + /// the plugin as it may break or change at any time. + @visibleForTesting + final StreamController deviceEventStreamController = + StreamController.broadcast(); + + Stream _cameraEvents(int cameraId) => + cameraEventStreamController.stream + .where((event) => event.cameraId == cameraId); + + /// Construct a new method channel camera instance. + MethodChannelCamera() { + final channel = MethodChannel('flutter.io/cameraPlugin/device'); + channel.setMethodCallHandler( + (MethodCall call) => handleDeviceMethodCall(call)); + } + + @override + Future> availableCameras() async { + try { + final List>? cameras = await _channel + .invokeListMethod>('availableCameras'); + + if (cameras == null) { + return []; + } + + return cameras.map((Map camera) { + return CameraDescription( + name: camera['name'], + lensDirection: parseCameraLensDirection(camera['lensFacing']), + sensorOrientation: camera['sensorOrientation'], + ); + }).toList(); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + @override + Future createCamera( + CameraDescription cameraDescription, + ResolutionPreset? resolutionPreset, { + bool enableAudio = false, + }) async { + try { + final reply = await _channel + .invokeMapMethod('create', { + 'cameraName': cameraDescription.name, + 'resolutionPreset': resolutionPreset != null + ? _serializeResolutionPreset(resolutionPreset) + : null, + 'enableAudio': enableAudio, + }); + + return reply!['cameraId']; + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + @override + Future initializeCamera( + int cameraId, { + ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, + }) { + _channels.putIfAbsent(cameraId, () { + final channel = MethodChannel('flutter.io/cameraPlugin/camera$cameraId'); + channel.setMethodCallHandler( + (MethodCall call) => handleCameraMethodCall(call, cameraId)); + return channel; + }); + + Completer _completer = Completer(); + + onCameraInitialized(cameraId).first.then((value) { + _completer.complete(); + }); + + _channel.invokeMapMethod( + 'initialize', + { + 'cameraId': cameraId, + 'imageFormatGroup': imageFormatGroup.name(), + }, + ); + + return _completer.future; + } + + @override + Future dispose(int cameraId) async { + if (_channels.containsKey(cameraId)) { + final cameraChannel = _channels[cameraId]; + cameraChannel?.setMethodCallHandler(null); + _channels.remove(cameraId); + } + + await _channel.invokeMethod( + 'dispose', + {'cameraId': cameraId}, + ); + } + + @override + Stream onCameraInitialized(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onCameraResolutionChanged(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onCameraClosing(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onCameraError(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onVideoRecordedEvent(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onDeviceOrientationChanged() { + return deviceEventStreamController.stream + .whereType(); + } + + @override + Future lockCaptureOrientation( + int cameraId, + DeviceOrientation orientation, + ) async { + await _channel.invokeMethod( + 'lockCaptureOrientation', + { + 'cameraId': cameraId, + 'orientation': serializeDeviceOrientation(orientation) + }, + ); + } + + @override + Future unlockCaptureOrientation(int cameraId) async { + await _channel.invokeMethod( + 'unlockCaptureOrientation', + {'cameraId': cameraId}, + ); + } + + @override + Future takePicture(int cameraId) async { + final path = await _channel.invokeMethod( + 'takePicture', + {'cameraId': cameraId}, + ); + + if (path == null) { + throw CameraException( + 'INVALID_PATH', + 'The platform "$defaultTargetPlatform" did not return a path while reporting success. The platform should always return a valid path or report an error.', + ); + } + + return XFile(path); + } + + @override + Future prepareForVideoRecording() => + _channel.invokeMethod('prepareForVideoRecording'); + + @override + Future startVideoRecording(int cameraId, + {Duration? maxVideoDuration}) async { + await _channel.invokeMethod( + 'startVideoRecording', + { + 'cameraId': cameraId, + 'maxVideoDuration': maxVideoDuration?.inMilliseconds, + }, + ); + } + + @override + Future stopVideoRecording(int cameraId) async { + final path = await _channel.invokeMethod( + 'stopVideoRecording', + {'cameraId': cameraId}, + ); + + if (path == null) { + throw CameraException( + 'INVALID_PATH', + 'The platform "$defaultTargetPlatform" did not return a path while reporting success. The platform should always return a valid path or report an error.', + ); + } + + return XFile(path); + } + + @override + Future pauseVideoRecording(int cameraId) => _channel.invokeMethod( + 'pauseVideoRecording', + {'cameraId': cameraId}, + ); + + @override + Future resumeVideoRecording(int cameraId) => + _channel.invokeMethod( + 'resumeVideoRecording', + {'cameraId': cameraId}, + ); + + @override + Future setFlashMode(int cameraId, FlashMode mode) => + _channel.invokeMethod( + 'setFlashMode', + { + 'cameraId': cameraId, + 'mode': _serializeFlashMode(mode), + }, + ); + + @override + Future setExposureMode(int cameraId, ExposureMode mode) => + _channel.invokeMethod( + 'setExposureMode', + { + 'cameraId': cameraId, + 'mode': serializeExposureMode(mode), + }, + ); + + @override + Future setExposurePoint(int cameraId, Point? point) { + assert(point == null || point.x >= 0 && point.x <= 1); + assert(point == null || point.y >= 0 && point.y <= 1); + + return _channel.invokeMethod( + 'setExposurePoint', + { + 'cameraId': cameraId, + 'reset': point == null, + 'x': point?.x, + 'y': point?.y, + }, + ); + } + + @override + Future getMinExposureOffset(int cameraId) async { + final minExposureOffset = await _channel.invokeMethod( + 'getMinExposureOffset', + {'cameraId': cameraId}, + ); + + return minExposureOffset!; + } + + @override + Future getMaxExposureOffset(int cameraId) async { + final maxExposureOffset = await _channel.invokeMethod( + 'getMaxExposureOffset', + {'cameraId': cameraId}, + ); + + return maxExposureOffset!; + } + + @override + Future getExposureOffsetStepSize(int cameraId) async { + final stepSize = await _channel.invokeMethod( + 'getExposureOffsetStepSize', + {'cameraId': cameraId}, + ); + + return stepSize!; + } + + @override + Future setExposureOffset(int cameraId, double offset) async { + final appliedOffset = await _channel.invokeMethod( + 'setExposureOffset', + { + 'cameraId': cameraId, + 'offset': offset, + }, + ); + + return appliedOffset!; + } + + @override + Future setFocusMode(int cameraId, FocusMode mode) => + _channel.invokeMethod( + 'setFocusMode', + { + 'cameraId': cameraId, + 'mode': serializeFocusMode(mode), + }, + ); + + @override + Future setFocusPoint(int cameraId, Point? point) { + assert(point == null || point.x >= 0 && point.x <= 1); + assert(point == null || point.y >= 0 && point.y <= 1); + + return _channel.invokeMethod( + 'setFocusPoint', + { + 'cameraId': cameraId, + 'reset': point == null, + 'x': point?.x, + 'y': point?.y, + }, + ); + } + + @override + Future getMaxZoomLevel(int cameraId) async { + final maxZoomLevel = await _channel.invokeMethod( + 'getMaxZoomLevel', + {'cameraId': cameraId}, + ); + + return maxZoomLevel!; + } + + @override + Future getMinZoomLevel(int cameraId) async { + final minZoomLevel = await _channel.invokeMethod( + 'getMinZoomLevel', + {'cameraId': cameraId}, + ); + + return minZoomLevel!; + } + + @override + Future setZoomLevel(int cameraId, double zoom) async { + try { + await _channel.invokeMethod( + 'setZoomLevel', + { + 'cameraId': cameraId, + 'zoom': zoom, + }, + ); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + @override + Widget buildPreview(int cameraId) { + return Texture(textureId: cameraId); + } + + /// Returns the flash mode as a String. + String _serializeFlashMode(FlashMode flashMode) { + switch (flashMode) { + case FlashMode.off: + return 'off'; + case FlashMode.auto: + return 'auto'; + case FlashMode.always: + return 'always'; + case FlashMode.torch: + return 'torch'; + default: + throw ArgumentError('Unknown FlashMode value'); + } + } + + /// Returns the resolution preset as a String. + String _serializeResolutionPreset(ResolutionPreset resolutionPreset) { + switch (resolutionPreset) { + case ResolutionPreset.max: + return 'max'; + case ResolutionPreset.ultraHigh: + return 'ultraHigh'; + case ResolutionPreset.veryHigh: + return 'veryHigh'; + case ResolutionPreset.high: + return 'high'; + case ResolutionPreset.medium: + return 'medium'; + case ResolutionPreset.low: + return 'low'; + default: + throw ArgumentError('Unknown ResolutionPreset value'); + } + } + + /// Converts messages received from the native platform into device events. + /// + /// This is only exposed for test purposes. It shouldn't be used by clients of + /// the plugin as it may break or change at any time. + Future handleDeviceMethodCall(MethodCall call) async { + switch (call.method) { + case 'orientation_changed': + deviceEventStreamController.add(DeviceOrientationChangedEvent( + deserializeDeviceOrientation(call.arguments['orientation']))); + break; + default: + throw MissingPluginException(); + } + } + + /// Converts messages received from the native platform into camera events. + /// + /// This is only exposed for test purposes. It shouldn't be used by clients of + /// the plugin as it may break or change at any time. + @visibleForTesting + Future handleCameraMethodCall(MethodCall call, int cameraId) async { + switch (call.method) { + case 'initialized': + cameraEventStreamController.add(CameraInitializedEvent( + cameraId, + call.arguments['previewWidth'], + call.arguments['previewHeight'], + deserializeExposureMode(call.arguments['exposureMode']), + call.arguments['exposurePointSupported'], + deserializeFocusMode(call.arguments['focusMode']), + call.arguments['focusPointSupported'], + )); + break; + case 'resolution_changed': + cameraEventStreamController.add(CameraResolutionChangedEvent( + cameraId, + call.arguments['captureWidth'], + call.arguments['captureHeight'], + )); + break; + case 'camera_closing': + cameraEventStreamController.add(CameraClosingEvent( + cameraId, + )); + break; + case 'video_recorded': + cameraEventStreamController.add(VideoRecordedEvent( + cameraId, + XFile(call.arguments['path']), + call.arguments['maxVideoDuration'] != null + ? Duration(milliseconds: call.arguments['maxVideoDuration']) + : null, + )); + break; + case 'error': + cameraEventStreamController.add(CameraErrorEvent( + cameraId, + call.arguments['description'], + )); + break; + default: + throw MissingPluginException(); + } + } +} diff --git a/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart new file mode 100644 index 000000000000..4437d3b0593a --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart @@ -0,0 +1,247 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:math'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/events/device_event.dart'; +import 'package:camera_platform_interface/src/method_channel/method_channel_camera.dart'; +import 'package:camera_platform_interface/src/types/exposure_mode.dart'; +import 'package:camera_platform_interface/src/types/focus_mode.dart'; +import 'package:camera_platform_interface/src/types/image_format_group.dart'; +import 'package:cross_file/cross_file.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +/// The interface that implementations of camera must implement. +/// +/// Platform implementations should extend this class rather than implement it as `camera` +/// does not consider newly added methods to be breaking changes. Extending this class +/// (using `extends`) ensures that the subclass will get the default implementation, while +/// platform implementations that `implements` this interface will be broken by newly added +/// [CameraPlatform] methods. +abstract class CameraPlatform extends PlatformInterface { + /// Constructs a CameraPlatform. + CameraPlatform() : super(token: _token); + + static final Object _token = Object(); + + static CameraPlatform _instance = MethodChannelCamera(); + + /// The default instance of [CameraPlatform] to use. + /// + /// Defaults to [MethodChannelCamera]. + static CameraPlatform get instance => _instance; + + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [CameraPlatform] when they register themselves. + static set instance(CameraPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + /// Completes with a list of available cameras. + /// + /// This method returns an empty list when no cameras are available. + Future> availableCameras() { + throw UnimplementedError('availableCameras() is not implemented.'); + } + + /// Creates an uninitialized camera instance and returns the cameraId. + Future createCamera( + CameraDescription cameraDescription, + ResolutionPreset? resolutionPreset, { + bool enableAudio = false, + }) { + throw UnimplementedError('createCamera() is not implemented.'); + } + + /// Initializes the camera on the device. + /// + /// [imageFormatGroup] is used to specify the image formatting used. + /// On Android this defaults to ImageFormat.YUV_420_888 and applies only to the imageStream. + /// On iOS this defaults to kCVPixelFormatType_32BGRA. + Future initializeCamera( + int cameraId, { + ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, + }) { + throw UnimplementedError('initializeCamera() is not implemented.'); + } + + /// The camera has been initialized + Stream onCameraInitialized(int cameraId) { + throw UnimplementedError('onCameraInitialized() is not implemented.'); + } + + /// The camera's resolution has changed + Stream onCameraResolutionChanged(int cameraId) { + throw UnimplementedError('onResolutionChanged() is not implemented.'); + } + + /// The camera started to close. + Stream onCameraClosing(int cameraId) { + throw UnimplementedError('onCameraClosing() is not implemented.'); + } + + /// The camera experienced an error. + Stream onCameraError(int cameraId) { + throw UnimplementedError('onCameraError() is not implemented.'); + } + + /// The camera finished recording a video + Stream onVideoRecordedEvent(int cameraId) { + throw UnimplementedError('onCameraTimeLimitReached() is not implemented.'); + } + + /// The device orientation changed. + /// + /// Implementations for this: + /// - Should support all 4 orientations. + /// - Should not emit new values when the screen orientation is locked. + Stream onDeviceOrientationChanged() { + throw UnimplementedError( + 'onDeviceOrientationChanged() is not implemented.'); + } + + /// Locks the capture orientation. + Future lockCaptureOrientation( + int cameraId, DeviceOrientation orientation) { + throw UnimplementedError('lockCaptureOrientation() is not implemented.'); + } + + /// Unlocks the capture orientation. + Future unlockCaptureOrientation(int cameraId) { + throw UnimplementedError('unlockCaptureOrientation() is not implemented.'); + } + + /// Captures an image and returns the file where it was saved. + Future takePicture(int cameraId) { + throw UnimplementedError('takePicture() is not implemented.'); + } + + /// Prepare the capture session for video recording. + Future prepareForVideoRecording() { + throw UnimplementedError('prepareForVideoRecording() is not implemented.'); + } + + /// Starts a video recording. + /// + /// The length of the recording can be limited by specifying the [maxVideoDuration]. + /// By default no maximum duration is specified, + /// meaning the recording will continue until manually stopped. + /// With [maxVideoDuration] set the video is returned in a [VideoRecordedEvent] + /// through the [onVideoRecordedEvent] stream when the set duration is reached. + Future startVideoRecording(int cameraId, {Duration? maxVideoDuration}) { + throw UnimplementedError('startVideoRecording() is not implemented.'); + } + + /// Stops the video recording and returns the file where it was saved. + Future stopVideoRecording(int cameraId) { + throw UnimplementedError('stopVideoRecording() is not implemented.'); + } + + /// Pause video recording. + Future pauseVideoRecording(int cameraId) { + throw UnimplementedError('pauseVideoRecording() is not implemented.'); + } + + /// Resume video recording after pausing. + Future resumeVideoRecording(int cameraId) { + throw UnimplementedError('resumeVideoRecording() is not implemented.'); + } + + /// Sets the flash mode for the selected camera. + Future setFlashMode(int cameraId, FlashMode mode) { + throw UnimplementedError('setFlashMode() is not implemented.'); + } + + /// Sets the exposure mode for taking pictures. + Future setExposureMode(int cameraId, ExposureMode mode) { + throw UnimplementedError('setExposureMode() is not implemented.'); + } + + /// Sets the exposure point for automatically determining the exposure values. + /// + /// Supplying `null` for the [point] argument will result in resetting to the + /// original exposure point value. + Future setExposurePoint(int cameraId, Point? point) { + throw UnimplementedError('setExposurePoint() is not implemented.'); + } + + /// Gets the minimum supported exposure offset for the selected camera in EV units. + Future getMinExposureOffset(int cameraId) { + throw UnimplementedError('getMinExposureOffset() is not implemented.'); + } + + /// Gets the maximum supported exposure offset for the selected camera in EV units. + Future getMaxExposureOffset(int cameraId) { + throw UnimplementedError('getMaxExposureOffset() is not implemented.'); + } + + /// Gets the supported step size for exposure offset for the selected camera in EV units. + /// + /// Returns 0 when the camera supports using a free value without stepping. + Future getExposureOffsetStepSize(int cameraId) { + throw UnimplementedError('getMinExposureOffset() is not implemented.'); + } + + /// Sets the exposure offset for the selected camera. + /// + /// The supplied [offset] value should be in EV units. 1 EV unit represents a + /// doubling in brightness. It should be between the minimum and maximum offsets + /// obtained through `getMinExposureOffset` and `getMaxExposureOffset` respectively. + /// Throws a `CameraException` when an illegal offset is supplied. + /// + /// When the supplied [offset] value does not align with the step size obtained + /// through `getExposureStepSize`, it will automatically be rounded to the nearest step. + /// + /// Returns the (rounded) offset value that was set. + Future setExposureOffset(int cameraId, double offset) { + throw UnimplementedError('setExposureOffset() is not implemented.'); + } + + /// Sets the focus mode for taking pictures. + Future setFocusMode(int cameraId, FocusMode mode) { + throw UnimplementedError('setFocusMode() is not implemented.'); + } + + /// Sets the focus point for automatically determining the focus values. + /// + /// Supplying `null` for the [point] argument will result in resetting to the + /// original focus point value. + Future setFocusPoint(int cameraId, Point? point) { + throw UnimplementedError('setFocusPoint() is not implemented.'); + } + + /// Gets the maximum supported zoom level for the selected camera. + Future getMaxZoomLevel(int cameraId) { + throw UnimplementedError('getMaxZoomLevel() is not implemented.'); + } + + /// Gets the minimum supported zoom level for the selected camera. + Future getMinZoomLevel(int cameraId) { + throw UnimplementedError('getMinZoomLevel() is not implemented.'); + } + + /// Set the zoom level for the selected camera. + /// + /// The supplied [zoom] value should be between 1.0 and the maximum supported + /// zoom level returned by the `getMaxZoomLevel`. Throws a `CameraException` + /// when an illegal zoom level is supplied. + Future setZoomLevel(int cameraId, double zoom) { + throw UnimplementedError('setZoomLevel() is not implemented.'); + } + + /// Returns a widget showing a live camera preview. + Widget buildPreview(int cameraId) { + throw UnimplementedError('buildView() has not been implemented.'); + } + + /// Releases the resources of this camera. + Future dispose(int cameraId) { + throw UnimplementedError('dispose() is not implemented.'); + } +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/camera_description.dart b/packages/camera/camera_platform_interface/lib/src/types/camera_description.dart new file mode 100644 index 000000000000..98a39fd6c65e --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/camera_description.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. + +/// The direction the camera is facing. +enum CameraLensDirection { + /// Front facing camera (a user looking at the screen is seen by the camera). + front, + + /// Back facing camera (a user looking at the screen is not seen by the camera). + back, + + /// External camera which may not be mounted to the device. + external, +} + +/// Properties of a camera device. +class CameraDescription { + /// Creates a new camera description with the given properties. + CameraDescription({ + required this.name, + required this.lensDirection, + required this.sensorOrientation, + }); + + /// The name of the camera device. + final String name; + + /// The direction the camera is facing. + final CameraLensDirection lensDirection; + + /// Clockwise angle through which the output image needs to be rotated to be upright on the device screen in its native orientation. + /// + /// **Range of valid values:** + /// 0, 90, 180, 270 + /// + /// On Android, also defines the direction of rolling shutter readout, which + /// is from top to bottom in the sensor's coordinate system. + final int sensorOrientation; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is CameraDescription && + runtimeType == other.runtimeType && + name == other.name && + lensDirection == other.lensDirection; + + @override + int get hashCode => name.hashCode ^ lensDirection.hashCode; + + @override + String toString() { + return '$runtimeType($name, $lensDirection, $sensorOrientation)'; + } +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/camera_exception.dart b/packages/camera/camera_platform_interface/lib/src/types/camera_exception.dart new file mode 100644 index 000000000000..d112f9f6f6e3 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/camera_exception.dart @@ -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. + +/// This is thrown when the plugin reports an error. +class CameraException implements Exception { + /// Creates a new camera exception with the given error code and description. + CameraException(this.code, this.description); + + /// Error code. + // TODO(bparrishMines): Document possible error codes. + // https://github.com/flutter/flutter/issues/69298 + String code; + + /// Textual description of the error. + String? description; + + @override + String toString() => 'CameraException($code, $description)'; +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/exposure_mode.dart b/packages/camera/camera_platform_interface/lib/src/types/exposure_mode.dart new file mode 100644 index 000000000000..1debd19b3a26 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/exposure_mode.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. + +/// The possible exposure modes that can be set for a camera. +enum ExposureMode { + /// Automatically determine exposure settings. + auto, + + /// Lock the currently determined exposure settings. + locked, +} + +/// Returns the exposure mode as a String. +String serializeExposureMode(ExposureMode exposureMode) { + switch (exposureMode) { + case ExposureMode.locked: + return 'locked'; + case ExposureMode.auto: + return 'auto'; + default: + throw ArgumentError('Unknown ExposureMode value'); + } +} + +/// Returns the exposure mode for a given String. +ExposureMode deserializeExposureMode(String str) { + switch (str) { + case "locked": + return ExposureMode.locked; + case "auto": + return ExposureMode.auto; + default: + throw ArgumentError('"$str" is not a valid ExposureMode value'); + } +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/flash_mode.dart b/packages/camera/camera_platform_interface/lib/src/types/flash_mode.dart new file mode 100644 index 000000000000..b9f146d373d3 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/flash_mode.dart @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// The possible flash modes that can be set for a camera +enum FlashMode { + /// Do not use the flash when taking a picture. + off, + + /// Let the device decide whether to flash the camera when taking a picture. + auto, + + /// Always use the flash when taking a picture. + always, + + /// Turns on the flash light and keeps it on until switched off. + torch, +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/focus_mode.dart b/packages/camera/camera_platform_interface/lib/src/types/focus_mode.dart new file mode 100644 index 000000000000..60a419155149 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/focus_mode.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. + +/// The possible focus modes that can be set for a camera. +enum FocusMode { + /// Automatically determine focus settings. + auto, + + /// Lock the currently determined focus settings. + locked, +} + +/// Returns the focus mode as a String. +String serializeFocusMode(FocusMode focusMode) { + switch (focusMode) { + case FocusMode.locked: + return 'locked'; + case FocusMode.auto: + return 'auto'; + default: + throw ArgumentError('Unknown FocusMode value'); + } +} + +/// Returns the focus mode for a given String. +FocusMode deserializeFocusMode(String str) { + switch (str) { + case "locked": + return FocusMode.locked; + case "auto": + return FocusMode.auto; + default: + throw ArgumentError('"$str" is not a valid FocusMode value'); + } +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart b/packages/camera/camera_platform_interface/lib/src/types/image_format_group.dart new file mode 100644 index 000000000000..edbf7d24098c --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/image_format_group.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. + +/// Group of image formats that are comparable across Android and iOS platforms. +enum ImageFormatGroup { + /// The image format does not fit into any specific group. + unknown, + + /// Multi-plane YUV 420 format. + /// + /// This format is a generic YCbCr format, capable of describing any 4:2:0 + /// chroma-subsampled planar or semiplanar buffer (but not fully interleaved), + /// with 8 bits per color sample. + /// + /// On Android, this is `android.graphics.ImageFormat.YUV_420_888`. See + /// https://developer.android.com/reference/android/graphics/ImageFormat.html#YUV_420_888 + /// + /// On iOS, this is `kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange`. See + /// https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers/kcvpixelformattype_420ypcbcr8biplanarvideorange?language=objc + yuv420, + + /// 32-bit BGRA. + /// + /// On iOS, this is `kCVPixelFormatType_32BGRA`. See + /// https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers/kcvpixelformattype_32bgra?language=objc + bgra8888, + + /// 32-big RGB image encoded into JPEG bytes. + /// + /// On Android, this is `android.graphics.ImageFormat.JPEG`. See + /// https://developer.android.com/reference/android/graphics/ImageFormat#JPEG + jpeg, +} + +/// Extension on [ImageFormatGroup] to stringify the enum +extension ImageFormatGroupName on ImageFormatGroup { + /// returns a String value for [ImageFormatGroup] + /// returns 'unknown' if platform is not supported + /// or if [ImageFormatGroup] is not supported for the platform + String name() { + switch (this) { + case ImageFormatGroup.bgra8888: + return 'bgra8888'; + case ImageFormatGroup.yuv420: + return 'yuv420'; + case ImageFormatGroup.jpeg: + return 'jpeg'; + case ImageFormatGroup.unknown: + default: + return 'unknown'; + } + } +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/resolution_preset.dart b/packages/camera/camera_platform_interface/lib/src/types/resolution_preset.dart new file mode 100644 index 000000000000..1724f6c97517 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/resolution_preset.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. + +/// Affect the quality of video recording and image capture: +/// +/// If a preset is not available on the camera being used a preset of lower quality will be selected automatically. +enum ResolutionPreset { + /// 352x288 on iOS, 240p (320x240) on Android + low, + + /// 480p (640x480 on iOS, 720x480 on Android) + medium, + + /// 720p (1280x720) + high, + + /// 1080p (1920x1080) + veryHigh, + + /// 2160p (3840x2160) + ultraHigh, + + /// The highest resolution available. + max, +} diff --git a/packages/camera/camera_platform_interface/lib/src/types/types.dart b/packages/camera/camera_platform_interface/lib/src/types/types.dart new file mode 100644 index 000000000000..0927458299df --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/types/types.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. + +export 'camera_description.dart'; +export 'resolution_preset.dart'; +export 'camera_exception.dart'; +export 'flash_mode.dart'; +export 'image_format_group.dart'; +export 'exposure_mode.dart'; +export 'focus_mode.dart'; diff --git a/packages/camera/camera_platform_interface/lib/src/utils/utils.dart b/packages/camera/camera_platform_interface/lib/src/utils/utils.dart new file mode 100644 index 000000000000..8c455867762f --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/utils/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_platform_interface/pubspec.yaml b/packages/camera/camera_platform_interface/pubspec.yaml new file mode 100644 index 000000000000..def06019c268 --- /dev/null +++ b/packages/camera/camera_platform_interface/pubspec.yaml @@ -0,0 +1,25 @@ +name: camera_platform_interface +description: A common platform interface for the camera plugin. +repository: https://github.com/flutter/plugins/tree/master/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.0.1 + +environment: + sdk: '>=2.12.0 <3.0.0' + flutter: ">=2.0.0" + +dependencies: + cross_file: ^0.3.1 + flutter: + sdk: flutter + meta: ^1.3.0 + plugin_platform_interface: ^2.0.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 new file mode 100644 index 000000000000..c8f38efc4e2d --- /dev/null +++ b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart @@ -0,0 +1,419 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/method_channel/method_channel_camera.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$CameraPlatform', () { + test('$MethodChannelCamera is the default instance', () { + expect(CameraPlatform.instance, isA()); + }); + + test('Cannot be implemented with `implements`', () { + expect(() { + CameraPlatform.instance = ImplementsCameraPlatform(); + }, throwsNoSuchMethodError); + }); + + test('Can be extended', () { + CameraPlatform.instance = ExtendsCameraPlatform(); + }); + + test( + 'Default implementation of availableCameras() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.availableCameras(), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of onCameraInitialized() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.onCameraInitialized(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of onResolutionChanged() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.onCameraResolutionChanged(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of onCameraClosing() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.onCameraClosing(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of onCameraError() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.onCameraError(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of onDeviceOrientationChanged() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.onDeviceOrientationChanged(), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of lockCaptureOrientation() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.lockCaptureOrientation( + 1, DeviceOrientation.portraitUp), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of unlockCaptureOrientation() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.unlockCaptureOrientation(1), + throwsUnimplementedError, + ); + }); + + test('Default implementation of dispose() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.dispose(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of createCamera() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.createCamera( + CameraDescription( + name: 'back', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of initializeCamera() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.initializeCamera(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of pauseVideoRecording() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.pauseVideoRecording(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of prepareForVideoRecording() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.prepareForVideoRecording(), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of resumeVideoRecording() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.resumeVideoRecording(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of setFlashMode() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.setFlashMode(1, FlashMode.auto), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of setExposureMode() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.setExposureMode(1, ExposureMode.auto), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of setExposurePoint() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.setExposurePoint(1, null), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of getMinExposureOffset() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.getMinExposureOffset(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of getMaxExposureOffset() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.getMaxExposureOffset(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of getExposureOffsetStepSize() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.getExposureOffsetStepSize(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of setExposureOffset() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.setExposureOffset(1, 2.0), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of setFocusMode() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.setFocusMode(1, FocusMode.auto), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of setFocusPoint() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.setFocusPoint(1, null), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of startVideoRecording() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.startVideoRecording(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of stopVideoRecording() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.stopVideoRecording(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of takePicture() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.takePicture(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of getMaxZoomLevel() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.getMaxZoomLevel(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of getMinZoomLevel() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.getMinZoomLevel(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of setZoomLevel() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.setZoomLevel(1, 1.0), + throwsUnimplementedError, + ); + }); + }); +} + +class ImplementsCameraPlatform implements CameraPlatform { + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class ExtendsCameraPlatform extends CameraPlatform {} diff --git a/packages/camera/camera_platform_interface/test/events/camera_event_test.dart b/packages/camera/camera_platform_interface/test/events/camera_event_test.dart new file mode 100644 index 000000000000..637358f557c3 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/events/camera_event_test.dart @@ -0,0 +1,322 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/types/exposure_mode.dart'; +import 'package:camera_platform_interface/src/types/focus_mode.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraInitializedEvent tests', () { + test('Constructor should initialize all properties', () { + final event = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + + expect(event.cameraId, 1); + expect(event.previewWidth, 1024); + expect(event.previewHeight, 640); + expect(event.exposureMode, ExposureMode.auto); + expect(event.focusMode, FocusMode.auto); + expect(event.exposurePointSupported, true); + expect(event.focusPointSupported, true); + }); + + test('fromJson should initialize all properties', () { + final event = CameraInitializedEvent.fromJson({ + 'cameraId': 1, + 'previewWidth': 1024.0, + 'previewHeight': 640.0, + 'exposureMode': 'auto', + 'exposurePointSupported': true, + 'focusMode': 'auto', + 'focusPointSupported': true + }); + + expect(event.cameraId, 1); + expect(event.previewWidth, 1024); + expect(event.previewHeight, 640); + expect(event.exposureMode, ExposureMode.auto); + expect(event.exposurePointSupported, true); + expect(event.focusMode, FocusMode.auto); + expect(event.focusPointSupported, true); + }); + + test('toJson should return a map with all fields', () { + final event = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + + final jsonMap = event.toJson(); + + expect(jsonMap.length, 7); + expect(jsonMap['cameraId'], 1); + expect(jsonMap['previewWidth'], 1024); + expect(jsonMap['previewHeight'], 640); + expect(jsonMap['exposureMode'], 'auto'); + expect(jsonMap['exposurePointSupported'], true); + expect(jsonMap['focusMode'], 'auto'); + expect(jsonMap['focusPointSupported'], true); + }); + + test('equals should return true if objects are the same', () { + final firstEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + final secondEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + + expect(firstEvent == secondEvent, true); + }); + + test('equals should return false if cameraId is different', () { + final firstEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + final secondEvent = CameraInitializedEvent( + 2, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if previewWidth is different', () { + final firstEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + final secondEvent = CameraInitializedEvent( + 1, 2048, 640, ExposureMode.auto, true, FocusMode.auto, true); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if previewHeight is different', () { + final firstEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + final secondEvent = CameraInitializedEvent( + 1, 1024, 980, ExposureMode.auto, true, FocusMode.auto, true); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if exposureMode is different', () { + final firstEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + final secondEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.locked, true, FocusMode.auto, true); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if exposurePointSupported is different', + () { + final firstEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + final secondEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, false, FocusMode.auto, true); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if focusMode is different', () { + final firstEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + final secondEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.locked, true); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if focusPointSupported is different', () { + final firstEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + final secondEvent = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, false); + + expect(firstEvent == secondEvent, false); + }); + + test('hashCode should match hashCode of all properties', () { + final event = CameraInitializedEvent( + 1, 1024, 640, ExposureMode.auto, true, FocusMode.auto, true); + final expectedHashCode = event.cameraId.hashCode ^ + event.previewWidth.hashCode ^ + event.previewHeight.hashCode ^ + event.exposureMode.hashCode ^ + event.exposurePointSupported.hashCode ^ + event.focusMode.hashCode ^ + event.focusPointSupported.hashCode; + + expect(event.hashCode, expectedHashCode); + }); + }); + + group('CameraResolutionChangesEvent tests', () { + test('Constructor should initialize all properties', () { + final event = CameraResolutionChangedEvent(1, 1024, 640); + + expect(event.cameraId, 1); + expect(event.captureWidth, 1024); + expect(event.captureHeight, 640); + }); + + test('fromJson should initialize all properties', () { + final event = CameraResolutionChangedEvent.fromJson({ + 'cameraId': 1, + 'captureWidth': 1024.0, + 'captureHeight': 640.0, + }); + + expect(event.cameraId, 1); + expect(event.captureWidth, 1024); + expect(event.captureHeight, 640); + }); + + test('toJson should return a map with all fields', () { + final event = CameraResolutionChangedEvent(1, 1024, 640); + + final jsonMap = event.toJson(); + + expect(jsonMap.length, 3); + expect(jsonMap['cameraId'], 1); + expect(jsonMap['captureWidth'], 1024); + expect(jsonMap['captureHeight'], 640); + }); + + test('equals should return true if objects are the same', () { + final firstEvent = CameraResolutionChangedEvent(1, 1024, 640); + final secondEvent = CameraResolutionChangedEvent(1, 1024, 640); + + expect(firstEvent == secondEvent, true); + }); + + test('equals should return false if cameraId is different', () { + final firstEvent = CameraResolutionChangedEvent(1, 1024, 640); + final secondEvent = CameraResolutionChangedEvent(2, 1024, 640); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if captureWidth is different', () { + final firstEvent = CameraResolutionChangedEvent(1, 1024, 640); + final secondEvent = CameraResolutionChangedEvent(1, 2048, 640); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if captureHeight is different', () { + final firstEvent = CameraResolutionChangedEvent(1, 1024, 640); + final secondEvent = CameraResolutionChangedEvent(1, 1024, 980); + + expect(firstEvent == secondEvent, false); + }); + + test('hashCode should match hashCode of all properties', () { + final event = CameraResolutionChangedEvent(1, 1024, 640); + final expectedHashCode = event.cameraId.hashCode ^ + event.captureWidth.hashCode ^ + event.captureHeight.hashCode; + + expect(event.hashCode, expectedHashCode); + }); + }); + + group('CameraClosingEvent tests', () { + test('Constructor should initialize all properties', () { + final event = CameraClosingEvent(1); + + expect(event.cameraId, 1); + }); + + test('fromJson should initialize all properties', () { + final event = CameraClosingEvent.fromJson({ + 'cameraId': 1, + }); + + expect(event.cameraId, 1); + }); + + test('toJson should return a map with all fields', () { + final event = CameraClosingEvent(1); + + final jsonMap = event.toJson(); + + expect(jsonMap.length, 1); + expect(jsonMap['cameraId'], 1); + }); + + test('equals should return true if objects are the same', () { + final firstEvent = CameraClosingEvent(1); + final secondEvent = CameraClosingEvent(1); + + expect(firstEvent == secondEvent, true); + }); + + test('equals should return false if cameraId is different', () { + final firstEvent = CameraClosingEvent(1); + final secondEvent = CameraClosingEvent(2); + + expect(firstEvent == secondEvent, false); + }); + + test('hashCode should match hashCode of all properties', () { + final event = CameraClosingEvent(1); + final expectedHashCode = event.cameraId.hashCode; + + expect(event.hashCode, expectedHashCode); + }); + }); + + group('CameraErrorEvent tests', () { + test('Constructor should initialize all properties', () { + final event = CameraErrorEvent(1, 'Error'); + + expect(event.cameraId, 1); + expect(event.description, 'Error'); + }); + + test('fromJson should initialize all properties', () { + final event = CameraErrorEvent.fromJson( + {'cameraId': 1, 'description': 'Error'}); + + expect(event.cameraId, 1); + expect(event.description, 'Error'); + }); + + test('toJson should return a map with all fields', () { + final event = CameraErrorEvent(1, 'Error'); + + final jsonMap = event.toJson(); + + expect(jsonMap.length, 2); + expect(jsonMap['cameraId'], 1); + expect(jsonMap['description'], 'Error'); + }); + + test('equals should return true if objects are the same', () { + final firstEvent = CameraErrorEvent(1, 'Error'); + final secondEvent = CameraErrorEvent(1, 'Error'); + + expect(firstEvent == secondEvent, true); + }); + + test('equals should return false if cameraId is different', () { + final firstEvent = CameraErrorEvent(1, 'Error'); + final secondEvent = CameraErrorEvent(2, 'Error'); + + expect(firstEvent == secondEvent, false); + }); + + test('equals should return false if description is different', () { + final firstEvent = CameraErrorEvent(1, 'Error'); + final secondEvent = CameraErrorEvent(1, 'Ooops'); + + expect(firstEvent == secondEvent, false); + }); + + test('hashCode should match hashCode of all properties', () { + final event = CameraErrorEvent(1, 'Error'); + final expectedHashCode = + event.cameraId.hashCode ^ event.description.hashCode; + + expect(event.hashCode, expectedHashCode); + }); + }); +} diff --git a/packages/camera/camera_platform_interface/test/events/device_event_test.dart b/packages/camera/camera_platform_interface/test/events/device_event_test.dart new file mode 100644 index 000000000000..f7cb657725a9 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/events/device_event_test.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:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('DeviceOrientationChangedEvent tests', () { + test('Constructor should initialize all properties', () { + final event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + + expect(event.orientation, DeviceOrientation.portraitUp); + }); + + test('fromJson should initialize all properties', () { + final event = DeviceOrientationChangedEvent.fromJson({ + 'orientation': 'portraitUp', + }); + + expect(event.orientation, DeviceOrientation.portraitUp); + }); + + test('toJson should return a map with all fields', () { + final event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + + final jsonMap = event.toJson(); + + expect(jsonMap.length, 1); + expect(jsonMap['orientation'], 'portraitUp'); + }); + + test('equals should return true if objects are the same', () { + final firstEvent = + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + final secondEvent = + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + + expect(firstEvent == secondEvent, true); + }); + + test('equals should return false if orientation is different', () { + final firstEvent = + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + final secondEvent = + DeviceOrientationChangedEvent(DeviceOrientation.landscapeLeft); + + expect(firstEvent == secondEvent, false); + }); + + test('hashCode should match hashCode of all properties', () { + final event = DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + final expectedHashCode = event.orientation.hashCode; + + expect(event.hashCode, expectedHashCode); + }); + }); +} diff --git a/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart new file mode 100644 index 000000000000..8a618545535b --- /dev/null +++ b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart @@ -0,0 +1,928 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:math'; + +import 'package:async/async.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/events/device_event.dart'; +import 'package:camera_platform_interface/src/method_channel/method_channel_camera.dart'; +import 'package:camera_platform_interface/src/types/focus_mode.dart'; +import 'package:camera_platform_interface/src/utils/utils.dart'; +import 'package:flutter/services.dart' hide DeviceOrientation; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../utils/method_channel_mock.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$MethodChannelCamera', () { + group('Creation, Initialization & Disposal Tests', () { + test('Should send creation data and receive back a camera id', () async { + // Arrange + MethodChannelMock cameraMockChannel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'create': { + 'cameraId': 1, + 'imageFormatGroup': 'unknown', + } + }); + final camera = MethodChannelCamera(); + + // Act + final cameraId = await camera.createCamera( + CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0), + ResolutionPreset.high, + ); + + // Assert + expect(cameraMockChannel.log, [ + isMethodCall( + 'create', + arguments: { + 'cameraName': 'Test', + 'resolutionPreset': 'high', + 'enableAudio': false + }, + ), + ]); + expect(cameraId, 1); + }); + + test( + 'Should throw CameraException when create throws a PlatformException', + () { + // Arrange + MethodChannelMock(channelName: 'plugins.flutter.io/camera', methods: { + 'create': PlatformException( + code: 'TESTING_ERROR_CODE', + message: 'Mock error message used during testing.', + ) + }); + final camera = MethodChannelCamera(); + + // Act + expect( + () => camera.createCamera( + CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ), + throwsA( + isA() + .having((e) => e.code, 'code', 'TESTING_ERROR_CODE') + .having((e) => e.description, 'description', + 'Mock error message used during testing.'), + ), + ); + }); + + test( + 'Should throw CameraException when create throws a PlatformException', + () { + // Arrange + MethodChannelMock(channelName: 'plugins.flutter.io/camera', methods: { + 'create': PlatformException( + code: 'TESTING_ERROR_CODE', + message: 'Mock error message used during testing.', + ) + }); + final camera = MethodChannelCamera(); + + // Act + expect( + () => camera.createCamera( + CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ), + throwsA( + isA() + .having((e) => e.code, 'code', 'TESTING_ERROR_CODE') + .having((e) => e.description, 'description', + 'Mock error message used during testing.'), + ), + ); + }); + + test('Should send initialization data', () async { + // Arrange + MethodChannelMock cameraMockChannel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'create': { + 'cameraId': 1, + 'imageFormatGroup': 'unknown', + }, + 'initialize': null + }); + final camera = MethodChannelCamera(); + final cameraId = await camera.createCamera( + CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ); + + // Act + Future initializeFuture = camera.initializeCamera(cameraId); + camera.cameraEventStreamController.add(CameraInitializedEvent( + cameraId, + 1920, + 1080, + ExposureMode.auto, + true, + FocusMode.auto, + true, + )); + await initializeFuture; + + // Assert + expect(cameraId, 1); + expect(cameraMockChannel.log, [ + anything, + isMethodCall( + 'initialize', + arguments: { + 'cameraId': 1, + 'imageFormatGroup': 'unknown', + }, + ), + ]); + }); + + test('Should send a disposal call on dispose', () async { + // Arrange + MethodChannelMock cameraMockChannel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'create': {'cameraId': 1}, + 'initialize': null, + 'dispose': {'cameraId': 1} + }); + + final camera = MethodChannelCamera(); + final cameraId = await camera.createCamera( + CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ); + Future initializeFuture = camera.initializeCamera(cameraId); + camera.cameraEventStreamController.add(CameraInitializedEvent( + cameraId, + 1920, + 1080, + ExposureMode.auto, + true, + FocusMode.auto, + true, + )); + await initializeFuture; + + // Act + await camera.dispose(cameraId); + + // Assert + expect(cameraId, 1); + expect(cameraMockChannel.log, [ + anything, + anything, + isMethodCall( + 'dispose', + arguments: {'cameraId': 1}, + ), + ]); + }); + }); + + group('Event Tests', () { + late MethodChannelCamera camera; + late int cameraId; + setUp(() async { + MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'create': {'cameraId': 1}, + 'initialize': null + }, + ); + camera = MethodChannelCamera(); + cameraId = await camera.createCamera( + CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ); + Future initializeFuture = camera.initializeCamera(cameraId); + camera.cameraEventStreamController.add(CameraInitializedEvent( + cameraId, + 1920, + 1080, + ExposureMode.auto, + true, + FocusMode.auto, + true, + )); + await initializeFuture; + }); + + test('Should receive initialized event', () async { + // Act + final Stream eventStream = + camera.onCameraInitialized(cameraId); + final streamQueue = StreamQueue(eventStream); + + // Emit test events + final event = CameraInitializedEvent( + cameraId, + 3840, + 2160, + ExposureMode.auto, + true, + FocusMode.auto, + true, + ); + await camera.handleCameraMethodCall( + MethodCall('initialized', event.toJson()), cameraId); + + // Assert + expect(await streamQueue.next, event); + + // Clean up + await streamQueue.cancel(); + }); + + test('Should receive resolution changes', () async { + // Act + final Stream resolutionStream = + camera.onCameraResolutionChanged(cameraId); + final streamQueue = StreamQueue(resolutionStream); + + // Emit test events + final fhdEvent = CameraResolutionChangedEvent(cameraId, 1920, 1080); + final uhdEvent = CameraResolutionChangedEvent(cameraId, 3840, 2160); + await camera.handleCameraMethodCall( + MethodCall('resolution_changed', fhdEvent.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('resolution_changed', uhdEvent.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('resolution_changed', fhdEvent.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('resolution_changed', uhdEvent.toJson()), cameraId); + + // Assert + expect(await streamQueue.next, fhdEvent); + expect(await streamQueue.next, uhdEvent); + expect(await streamQueue.next, fhdEvent); + expect(await streamQueue.next, uhdEvent); + + // Clean up + await streamQueue.cancel(); + }); + + test('Should receive camera closing events', () async { + // Act + final Stream eventStream = + camera.onCameraClosing(cameraId); + final streamQueue = StreamQueue(eventStream); + + // Emit test events + final event = CameraClosingEvent(cameraId); + await camera.handleCameraMethodCall( + MethodCall('camera_closing', event.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('camera_closing', event.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('camera_closing', event.toJson()), cameraId); + + // Assert + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + + // Clean up + await streamQueue.cancel(); + }); + + test('Should receive camera error events', () async { + // Act + final errorStream = camera.onCameraError(cameraId); + final streamQueue = StreamQueue(errorStream); + + // Emit test events + final event = CameraErrorEvent(cameraId, 'Error Description'); + await camera.handleCameraMethodCall( + MethodCall('error', event.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('error', event.toJson()), cameraId); + await camera.handleCameraMethodCall( + MethodCall('error', event.toJson()), cameraId); + + // Assert + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + + // Clean up + await streamQueue.cancel(); + }); + + test('Should receive device orientation change events', () async { + // Act + final eventStream = camera.onDeviceOrientationChanged(); + final streamQueue = StreamQueue(eventStream); + + // Emit test events + final event = + DeviceOrientationChangedEvent(DeviceOrientation.portraitUp); + await camera.handleDeviceMethodCall( + MethodCall('orientation_changed', event.toJson())); + await camera.handleDeviceMethodCall( + MethodCall('orientation_changed', event.toJson())); + await camera.handleDeviceMethodCall( + MethodCall('orientation_changed', event.toJson())); + + // Assert + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + expect(await streamQueue.next, event); + + // Clean up + await streamQueue.cancel(); + }); + }); + + group('Function Tests', () { + late MethodChannelCamera camera; + late int cameraId; + + setUp(() async { + MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'create': {'cameraId': 1}, + 'initialize': null + }, + ); + camera = MethodChannelCamera(); + cameraId = await camera.createCamera( + CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.high, + ); + Future initializeFuture = camera.initializeCamera(cameraId); + camera.cameraEventStreamController.add( + CameraInitializedEvent( + cameraId, + 1920, + 1080, + ExposureMode.auto, + true, + FocusMode.auto, + true, + ), + ); + await initializeFuture; + }); + + test('Should fetch CameraDescription instances for available cameras', + () async { + // Arrange + List> returnData = [ + {'name': 'Test 1', 'lensFacing': 'front', 'sensorOrientation': 1}, + {'name': 'Test 2', 'lensFacing': 'back', 'sensorOrientation': 2} + ]; + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'availableCameras': returnData}, + ); + + // Act + List cameras = await camera.availableCameras(); + + // Assert + expect(channel.log, [ + isMethodCall('availableCameras', arguments: null), + ]); + expect(cameras.length, returnData.length); + for (int i = 0; i < returnData.length; i++) { + CameraDescription cameraDescription = CameraDescription( + name: returnData[i]['name'], + lensDirection: + parseCameraLensDirection(returnData[i]['lensFacing']), + sensorOrientation: returnData[i]['sensorOrientation'], + ); + expect(cameras[i], cameraDescription); + } + }); + + test( + 'Should throw CameraException when availableCameras throws a PlatformException', + () { + // Arrange + MethodChannelMock(channelName: 'plugins.flutter.io/camera', methods: { + 'availableCameras': PlatformException( + code: 'TESTING_ERROR_CODE', + message: 'Mock error message used during testing.', + ) + }); + + // Act + expect( + camera.availableCameras, + throwsA( + isA() + .having((e) => e.code, 'code', 'TESTING_ERROR_CODE') + .having((e) => e.description, 'description', + 'Mock error message used during testing.'), + ), + ); + }); + + test('Should take a picture and return an XFile instance', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'takePicture': '/test/path.jpg'}); + + // Act + XFile file = await camera.takePicture(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('takePicture', arguments: { + 'cameraId': cameraId, + }), + ]); + expect(file.path, '/test/path.jpg'); + }); + + test('Should prepare for video recording', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'prepareForVideoRecording': null}, + ); + + // Act + await camera.prepareForVideoRecording(); + + // Assert + expect(channel.log, [ + isMethodCall('prepareForVideoRecording', arguments: null), + ]); + }); + + test('Should start recording a video', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'startVideoRecording': null}, + ); + + // Act + await camera.startVideoRecording(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('startVideoRecording', arguments: { + 'cameraId': cameraId, + 'maxVideoDuration': null, + }), + ]); + }); + + test('Should pass maxVideoDuration when starting recording a video', + () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'startVideoRecording': null}, + ); + + // Act + await camera.startVideoRecording( + cameraId, + maxVideoDuration: Duration(seconds: 10), + ); + + // Assert + expect(channel.log, [ + isMethodCall('startVideoRecording', + arguments: {'cameraId': cameraId, 'maxVideoDuration': 10000}), + ]); + }); + + test('Should stop a video recording and return the file', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'stopVideoRecording': '/test/path.mp4'}, + ); + + // Act + XFile file = await camera.stopVideoRecording(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('stopVideoRecording', arguments: { + 'cameraId': cameraId, + }), + ]); + expect(file.path, '/test/path.mp4'); + }); + + test('Should pause a video recording', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'pauseVideoRecording': null}, + ); + + // Act + await camera.pauseVideoRecording(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('pauseVideoRecording', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should resume a video recording', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'resumeVideoRecording': null}, + ); + + // Act + await camera.resumeVideoRecording(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('resumeVideoRecording', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should set the flash mode', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'setFlashMode': null}, + ); + + // Act + await camera.setFlashMode(cameraId, FlashMode.torch); + await camera.setFlashMode(cameraId, FlashMode.always); + await camera.setFlashMode(cameraId, FlashMode.auto); + await camera.setFlashMode(cameraId, FlashMode.off); + + // Assert + expect(channel.log, [ + isMethodCall('setFlashMode', + arguments: {'cameraId': cameraId, 'mode': 'torch'}), + isMethodCall('setFlashMode', + arguments: {'cameraId': cameraId, 'mode': 'always'}), + isMethodCall('setFlashMode', + arguments: {'cameraId': cameraId, 'mode': 'auto'}), + isMethodCall('setFlashMode', + arguments: {'cameraId': cameraId, 'mode': 'off'}), + ]); + }); + + test('Should set the exposure mode', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'setExposureMode': null}, + ); + + // Act + await camera.setExposureMode(cameraId, ExposureMode.auto); + await camera.setExposureMode(cameraId, ExposureMode.locked); + + // Assert + expect(channel.log, [ + isMethodCall('setExposureMode', + arguments: {'cameraId': cameraId, 'mode': 'auto'}), + isMethodCall('setExposureMode', + arguments: {'cameraId': cameraId, 'mode': 'locked'}), + ]); + }); + + test('Should set the exposure point', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'setExposurePoint': null}, + ); + + // Act + await camera.setExposurePoint(cameraId, Point(0.5, 0.5)); + await camera.setExposurePoint(cameraId, null); + + // Assert + expect(channel.log, [ + isMethodCall('setExposurePoint', arguments: { + 'cameraId': cameraId, + 'x': 0.5, + 'y': 0.5, + 'reset': false + }), + isMethodCall('setExposurePoint', arguments: { + 'cameraId': cameraId, + 'x': null, + 'y': null, + 'reset': true + }), + ]); + }); + + test('Should get the min exposure offset', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'getMinExposureOffset': 2.0}, + ); + + // Act + final minExposureOffset = await camera.getMinExposureOffset(cameraId); + + // Assert + expect(minExposureOffset, 2.0); + expect(channel.log, [ + isMethodCall('getMinExposureOffset', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should get the max exposure offset', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'getMaxExposureOffset': 2.0}, + ); + + // Act + final maxExposureOffset = await camera.getMaxExposureOffset(cameraId); + + // Assert + expect(maxExposureOffset, 2.0); + expect(channel.log, [ + isMethodCall('getMaxExposureOffset', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should get the exposure offset step size', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'getExposureOffsetStepSize': 0.25}, + ); + + // Act + final stepSize = await camera.getExposureOffsetStepSize(cameraId); + + // Assert + expect(stepSize, 0.25); + expect(channel.log, [ + isMethodCall('getExposureOffsetStepSize', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should set the exposure offset', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'setExposureOffset': 0.6}, + ); + + // Act + final actualOffset = await camera.setExposureOffset(cameraId, 0.5); + + // Assert + expect(actualOffset, 0.6); + expect(channel.log, [ + isMethodCall('setExposureOffset', arguments: { + 'cameraId': cameraId, + 'offset': 0.5, + }), + ]); + }); + + test('Should set the focus mode', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'setFocusMode': null}, + ); + + // Act + await camera.setFocusMode(cameraId, FocusMode.auto); + await camera.setFocusMode(cameraId, FocusMode.locked); + + // Assert + expect(channel.log, [ + isMethodCall('setFocusMode', + arguments: {'cameraId': cameraId, 'mode': 'auto'}), + isMethodCall('setFocusMode', + arguments: {'cameraId': cameraId, 'mode': 'locked'}), + ]); + }); + + test('Should set the exposure point', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'setFocusPoint': null}, + ); + + // Act + await camera.setFocusPoint(cameraId, Point(0.5, 0.5)); + await camera.setFocusPoint(cameraId, null); + + // Assert + expect(channel.log, [ + isMethodCall('setFocusPoint', arguments: { + 'cameraId': cameraId, + 'x': 0.5, + 'y': 0.5, + 'reset': false + }), + isMethodCall('setFocusPoint', arguments: { + 'cameraId': cameraId, + 'x': null, + 'y': null, + 'reset': true + }), + ]); + }); + + test('Should build a texture widget as preview widget', () async { + // Act + Widget widget = camera.buildPreview(cameraId); + + // Act + expect(widget is Texture, isTrue); + expect((widget as Texture).textureId, cameraId); + }); + + test('Should throw MissingPluginException when handling unknown method', + () { + final camera = MethodChannelCamera(); + + expect( + () => + camera.handleCameraMethodCall(MethodCall('unknown_method'), 1), + throwsA(isA())); + }); + + test('Should get the max zoom level', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'getMaxZoomLevel': 10.0}, + ); + + // Act + final maxZoomLevel = await camera.getMaxZoomLevel(cameraId); + + // Assert + expect(maxZoomLevel, 10.0); + expect(channel.log, [ + isMethodCall('getMaxZoomLevel', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should get the min zoom level', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'getMinZoomLevel': 1.0}, + ); + + // Act + final maxZoomLevel = await camera.getMinZoomLevel(cameraId); + + // Assert + expect(maxZoomLevel, 1.0); + expect(channel.log, [ + isMethodCall('getMinZoomLevel', arguments: { + 'cameraId': cameraId, + }), + ]); + }); + + test('Should set the zoom level', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'setZoomLevel': null}, + ); + + // Act + await camera.setZoomLevel(cameraId, 2.0); + + // Assert + expect(channel.log, [ + isMethodCall('setZoomLevel', + arguments: {'cameraId': cameraId, 'zoom': 2.0}), + ]); + }); + + test('Should throw CameraException when illegal zoom level is supplied', + () async { + // Arrange + MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: { + 'setZoomLevel': PlatformException( + code: 'ZOOM_ERROR', + message: 'Illegal zoom error', + details: null, + ) + }, + ); + + // Act & assert + expect( + () => camera.setZoomLevel(cameraId, -1.0), + throwsA(isA() + .having((e) => e.code, 'code', 'ZOOM_ERROR') + .having((e) => e.description, 'description', + 'Illegal zoom error'))); + }); + + test('Should lock the capture orientation', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'lockCaptureOrientation': null}, + ); + + // Act + await camera.lockCaptureOrientation( + cameraId, DeviceOrientation.portraitUp); + + // Assert + expect(channel.log, [ + isMethodCall('lockCaptureOrientation', + arguments: {'cameraId': cameraId, 'orientation': 'portraitUp'}), + ]); + }); + + test('Should unlock the capture orientation', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'unlockCaptureOrientation': null}, + ); + + // Act + await camera.unlockCaptureOrientation(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('unlockCaptureOrientation', + arguments: {'cameraId': cameraId}), + ]); + }); + }); + }); +} diff --git a/packages/camera/camera_platform_interface/test/types/camera_description_test.dart b/packages/camera/camera_platform_interface/test/types/camera_description_test.dart new file mode 100644 index 000000000000..11a5210e831b --- /dev/null +++ b/packages/camera/camera_platform_interface/test/types/camera_description_test.dart @@ -0,0 +1,113 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraLensDirection tests', () { + test('CameraLensDirection should contain 3 options', () { + final values = CameraLensDirection.values; + + expect(values.length, 3); + }); + + test("CameraLensDirection enum should have items in correct index", () { + final values = CameraLensDirection.values; + + expect(values[0], CameraLensDirection.front); + expect(values[1], CameraLensDirection.back); + expect(values[2], CameraLensDirection.external); + }); + }); + + group('CameraDescription tests', () { + test('Constructor should initialize all properties', () { + final description = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + sensorOrientation: 90, + ); + + expect(description.name, 'Test'); + expect(description.lensDirection, CameraLensDirection.front); + expect(description.sensorOrientation, 90); + }); + + test('equals should return true if objects are the same', () { + final firstDescription = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + sensorOrientation: 90, + ); + final secondDescription = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + sensorOrientation: 90, + ); + + expect(firstDescription == secondDescription, true); + }); + + test('equals should return false if name is different', () { + final firstDescription = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + sensorOrientation: 90, + ); + final secondDescription = CameraDescription( + name: 'Testing', + lensDirection: CameraLensDirection.front, + sensorOrientation: 90, + ); + + expect(firstDescription == secondDescription, false); + }); + + test('equals should return false if lens direction is different', () { + final firstDescription = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + sensorOrientation: 90, + ); + final secondDescription = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90, + ); + + expect(firstDescription == secondDescription, false); + }); + + test('equals should return true if sensor orientation is different', () { + final firstDescription = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + sensorOrientation: 0, + ); + final secondDescription = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + sensorOrientation: 90, + ); + + expect(firstDescription == secondDescription, true); + }); + + test('hashCode should match hashCode of all properties', () { + final description = CameraDescription( + name: 'Test', + lensDirection: CameraLensDirection.front, + sensorOrientation: 0, + ); + final expectedHashCode = description.name.hashCode ^ + description.lensDirection.hashCode ^ + description.sensorOrientation.hashCode; + + expect(description.hashCode, expectedHashCode); + }); + }); +} diff --git a/packages/camera/camera_platform_interface/test/types/camera_exception_test.dart b/packages/camera/camera_platform_interface/test/types/camera_exception_test.dart new file mode 100644 index 000000000000..5fb753fb3616 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/types/camera_exception_test.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:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('constructor should initialize properties', () { + final code = 'TEST_ERROR'; + final description = 'This is a test error'; + final exception = CameraException(code, description); + + expect(exception.code, code); + expect(exception.description, description); + }); + + test('toString: Should return a description of the exception', () { + final code = 'TEST_ERROR'; + final description = 'This is a test error'; + final expected = 'CameraException($code, $description)'; + final exception = CameraException(code, description); + + final actual = exception.toString(); + + expect(actual, expected); + }); +} diff --git a/packages/camera/camera_platform_interface/test/types/exposure_mode_test.dart b/packages/camera/camera_platform_interface/test/types/exposure_mode_test.dart new file mode 100644 index 000000000000..659b75e017f1 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/types/exposure_mode_test.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:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/types/exposure_mode.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('ExposureMode should contain 2 options', () { + final values = ExposureMode.values; + + expect(values.length, 2); + }); + + test("ExposureMode enum should have items in correct index", () { + final values = ExposureMode.values; + + expect(values[0], ExposureMode.auto); + expect(values[1], ExposureMode.locked); + }); + + test("serializeExposureMode() should serialize correctly", () { + expect(serializeExposureMode(ExposureMode.auto), "auto"); + expect(serializeExposureMode(ExposureMode.locked), "locked"); + }); + + test("deserializeExposureMode() should deserialize correctly", () { + expect(deserializeExposureMode('auto'), ExposureMode.auto); + expect(deserializeExposureMode('locked'), ExposureMode.locked); + }); +} diff --git a/packages/camera/camera_platform_interface/test/types/flash_mode_test.dart b/packages/camera/camera_platform_interface/test/types/flash_mode_test.dart new file mode 100644 index 000000000000..d313bcf52b77 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/types/flash_mode_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:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('FlashMode should contain 4 options', () { + final values = FlashMode.values; + + expect(values.length, 4); + }); + + test("FlashMode enum should have items in correct index", () { + final values = FlashMode.values; + + expect(values[0], FlashMode.off); + expect(values[1], FlashMode.auto); + expect(values[2], FlashMode.always); + expect(values[3], FlashMode.torch); + }); +} diff --git a/packages/camera/camera_platform_interface/test/types/focus_mode_test.dart b/packages/camera/camera_platform_interface/test/types/focus_mode_test.dart new file mode 100644 index 000000000000..866f8ce07909 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/types/focus_mode_test.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. + +import 'package:camera_platform_interface/src/types/focus_mode.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('FocusMode should contain 2 options', () { + final values = FocusMode.values; + + expect(values.length, 2); + }); + + test("FocusMode enum should have items in correct index", () { + final values = FocusMode.values; + + expect(values[0], FocusMode.auto); + expect(values[1], FocusMode.locked); + }); + + test("serializeFocusMode() should serialize correctly", () { + expect(serializeFocusMode(FocusMode.auto), "auto"); + expect(serializeFocusMode(FocusMode.locked), "locked"); + }); + + test("deserializeFocusMode() should deserialize correctly", () { + expect(deserializeFocusMode('auto'), FocusMode.auto); + expect(deserializeFocusMode('locked'), FocusMode.locked); + }); +} diff --git a/packages/camera/camera_platform_interface/test/types/image_group_test.dart b/packages/camera/camera_platform_interface/test/types/image_group_test.dart new file mode 100644 index 000000000000..89585cc1ae35 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/types/image_group_test.dart @@ -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 'package:camera_platform_interface/src/types/types.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('$ImageFormatGroup tests', () { + test('ImageFormatGroupName extension returns correct values', () { + expect(ImageFormatGroup.bgra8888.name(), 'bgra8888'); + expect(ImageFormatGroup.yuv420.name(), 'yuv420'); + expect(ImageFormatGroup.jpeg.name(), 'jpeg'); + expect(ImageFormatGroup.unknown.name(), 'unknown'); + }); + }); +} diff --git a/packages/camera/camera_platform_interface/test/types/resolution_preset_test.dart b/packages/camera/camera_platform_interface/test/types/resolution_preset_test.dart new file mode 100644 index 000000000000..55a4ac56cd9d --- /dev/null +++ b/packages/camera/camera_platform_interface/test/types/resolution_preset_test.dart @@ -0,0 +1,25 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('ResolutionPreset should contain 6 options', () { + final values = ResolutionPreset.values; + + expect(values.length, 6); + }); + + test("ResolutionPreset enum should have items in correct index", () { + final values = ResolutionPreset.values; + + expect(values[0], ResolutionPreset.low); + expect(values[1], ResolutionPreset.medium); + expect(values[2], ResolutionPreset.high); + expect(values[3], ResolutionPreset.veryHigh); + expect(values[4], ResolutionPreset.ultraHigh); + expect(values[5], ResolutionPreset.max); + }); +} diff --git a/packages/camera/camera_platform_interface/test/utils/method_channel_mock.dart b/packages/camera/camera_platform_interface/test/utils/method_channel_mock.dart new file mode 100644 index 000000000000..60d8def6a2e3 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/utils/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 { + final Duration? delay; + final MethodChannel methodChannel; + final Map methods; + final log = []; + + MethodChannelMock({ + required String channelName, + this.delay, + required this.methods, + }) : methodChannel = MethodChannel(channelName) { + methodChannel.setMockMethodCallHandler(_handler); + } + + Future _handler(MethodCall methodCall) async { + log.add(methodCall); + + if (!methods.containsKey(methodCall.method)) { + throw MissingPluginException('No implementation found for method ' + '${methodCall.method} on channel ${methodChannel.name}'); + } + + return Future.delayed(delay ?? Duration.zero, () { + final result = methods[methodCall.method]; + if (result is Exception) { + throw result; + } + + return Future.value(result); + }); + } +} diff --git a/packages/camera/camera_platform_interface/test/utils/utils_test.dart b/packages/camera/camera_platform_interface/test/utils/utils_test.dart new file mode 100644 index 000000000000..f1960eeb2e72 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/utils/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_platform_interface/camera_platform_interface.dart'; +import 'package:camera_platform_interface/src/utils/utils.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Utility methods', () { + test( + 'Should return CameraLensDirection when valid value is supplied when parsing camera lens direction', + () { + expect( + parseCameraLensDirection('back'), + CameraLensDirection.back, + ); + expect( + parseCameraLensDirection('front'), + CameraLensDirection.front, + ); + expect( + parseCameraLensDirection('external'), + CameraLensDirection.external, + ); + }); + + test( + 'Should throw ArgumentException when invalid value is supplied when parsing camera lens direction', + () { + expect( + () => parseCameraLensDirection('test'), + throwsA(isArgumentError), + ); + }); + + test("serializeDeviceOrientation() should serialize correctly", () { + expect(serializeDeviceOrientation(DeviceOrientation.portraitUp), + "portraitUp"); + expect(serializeDeviceOrientation(DeviceOrientation.portraitDown), + "portraitDown"); + expect(serializeDeviceOrientation(DeviceOrientation.landscapeRight), + "landscapeRight"); + expect(serializeDeviceOrientation(DeviceOrientation.landscapeLeft), + "landscapeLeft"); + }); + + test("deserializeDeviceOrientation() should deserialize correctly", () { + expect(deserializeDeviceOrientation('portraitUp'), + DeviceOrientation.portraitUp); + expect(deserializeDeviceOrientation('portraitDown'), + DeviceOrientation.portraitDown); + expect(deserializeDeviceOrientation('landscapeRight'), + DeviceOrientation.landscapeRight); + expect(deserializeDeviceOrientation('landscapeLeft'), + DeviceOrientation.landscapeLeft); + }); + }); +} diff --git a/packages/camera/example/android/app/build.gradle b/packages/camera/example/android/app/build.gradle deleted file mode 100644 index e47b6db5e21e..000000000000 --- a/packages/camera/example/android/app/build.gradle +++ /dev/null @@ -1,64 +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 28 - - 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/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/EmbeddingV1ActivityTest.java b/packages/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/EmbeddingV1ActivityTest.java deleted file mode 100644 index 8a97e47072e9..000000000000 --- a/packages/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.flutter.plugins.cameraexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -@SuppressWarnings("deprecation") -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/FlutterActivityTest.java b/packages/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/FlutterActivityTest.java deleted file mode 100644 index c90c66defc32..000000000000 --- a/packages/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/FlutterActivityTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.flutter.plugins.cameraexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import io.flutter.embedding.android.FlutterActivity; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -public class FlutterActivityTest { - @Rule - public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); -} diff --git a/packages/camera/example/android/app/src/main/AndroidManifest.xml b/packages/camera/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index f37f82306024..000000000000 --- a/packages/camera/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/packages/camera/example/android/app/src/main/java/io/flutter/plugins/cameraexample/EmbeddingV1Activity.java b/packages/camera/example/android/app/src/main/java/io/flutter/plugins/cameraexample/EmbeddingV1Activity.java deleted file mode 100644 index a53dd6b756ad..000000000000 --- a/packages/camera/example/android/app/src/main/java/io/flutter/plugins/cameraexample/EmbeddingV1Activity.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.flutter.plugins.cameraexample; - -import android.os.Bundle; -import dev.flutter.plugins.integration_test.IntegrationTestPlugin; -import io.flutter.plugins.camera.CameraPlugin; -import io.flutter.plugins.pathprovider.PathProviderPlugin; -import io.flutter.plugins.videoplayer.VideoPlayerPlugin; - -@SuppressWarnings("deprecation") -public class EmbeddingV1Activity extends io.flutter.app.FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - CameraPlugin.registerWith(registrarFor("io.flutter.plugins.camera.CameraPlugin")); - IntegrationTestPlugin.registerWith( - registrarFor("dev.flutter.plugins.integration_test.IntegrationTestPlugin")); - PathProviderPlugin.registerWith( - registrarFor("io.flutter.plugins.pathprovider.PathProviderPlugin")); - VideoPlayerPlugin.registerWith( - registrarFor("io.flutter.plugins.videoplayer.VideoPlayerPlugin")); - } -} diff --git a/packages/camera/example/android/build.gradle b/packages/camera/example/android/build.gradle deleted file mode 100644 index 112aa2a87c27..000000000000 --- a/packages/camera/example/android/build.gradle +++ /dev/null @@ -1,32 +0,0 @@ -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' - } -} - -allprojects { - repositories { - google() - jcenter() - maven { - url 'https://google.bintray.com/exoplayer/' - } - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { - project.evaluationDependsOn(':app') -} - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/packages/camera/example/integration_test/camera_test.dart b/packages/camera/example/integration_test/camera_test.dart deleted file mode 100644 index ef4646f5ced9..000000000000 --- a/packages/camera/example/integration_test/camera_test.dart +++ /dev/null @@ -1,238 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:ui'; - -import 'package:flutter/painting.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:camera/camera.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:video_player/video_player.dart'; -import 'package:integration_test/integration_test.dart'; - -void main() { - Directory testDir; - - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - setUpAll(() async { - 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: - Platform.isAndroid ? const Size(240, 320) : const Size(288, 352), - ResolutionPreset.medium: - Platform.isAndroid ? const Size(480, 720) : 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 String filePath = - '${testDir.path}/${DateTime.now().millisecondsSinceEpoch}.jpg'; - await controller.takePicture(filePath); - - // Load picture - final File fileImage = File(filePath); - 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 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(); - } - } - }, skip: !Platform.isAndroid); - - // 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 - final String filePath = - '${testDir.path}/${DateTime.now().millisecondsSinceEpoch}.mp4'; - await controller.startVideoRecording(filePath); - sleep(const Duration(milliseconds: 300)); - await controller.stopVideoRecording(); - - // Load video metadata - final File videoFile = File(filePath); - 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 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(); - } - } - }, skip: !Platform.isAndroid); - - testWidgets('Pause and resume video recording', (WidgetTester tester) async { - final List cameras = await availableCameras(); - if (cameras.isEmpty) { - return; - } - - final CameraController controller = CameraController( - cameras[0], - ResolutionPreset.low, - enableAudio: false, - ); - - await controller.initialize(); - await controller.prepareForVideoRecording(); - - final String filePath = - '${testDir.path}/${DateTime.now().millisecondsSinceEpoch}.mp4'; - - int startPause; - int timePaused = 0; - - await controller.startVideoRecording(filePath); - 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)); - - await controller.stopVideoRecording(); - final int recordingTime = - DateTime.now().millisecondsSinceEpoch - recordingStart; - - final File videoFile = File(filePath); - final VideoPlayerController videoController = VideoPlayerController.file( - videoFile, - ); - await videoController.initialize(); - final int duration = videoController.value.duration.inMilliseconds; - await videoController.dispose(); - - expect(duration, lessThan(recordingTime - timePaused)); - }, skip: !Platform.isAndroid); - - testWidgets( - 'Android image streaming', - (WidgetTester tester) async { - final List cameras = await availableCameras(); - if (cameras.isEmpty) { - return; - } - - final CameraController controller = CameraController( - cameras[0], - ResolutionPreset.low, - enableAudio: false, - ); - - await controller.initialize(); - bool _isDetecting = false; - - await controller.startImageStream((CameraImage 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(); - }, - skip: !Platform.isAndroid, - ); -} diff --git a/packages/camera/example/ios/Flutter/AppFrameworkInfo.plist b/packages/camera/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6c2de8086bcd..000000000000 --- a/packages/camera/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/camera/example/ios/Runner.xcodeproj/project.pbxproj b/packages/camera/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 862ee64fb666..000000000000 --- a/packages/camera/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,490 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 75201D617916C49BDEDF852A /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 620DDA07C00B5FF2F937CB5B /* libPods-Runner.a */; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; - 483D985F075B951ADBAD218E /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 620DDA07C00B5FF2F937CB5B /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 9AC7510327AD6A32B7CBD9A5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - 75201D617916C49BDEDF852A /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 8A1387E89A6BBC071B75FD6F /* Frameworks */ = { - isa = PBXGroup; - children = ( - 620DDA07C00B5FF2F937CB5B /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B80C3931E831B6300D905FE /* App.framework */, - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - C52D9D4A70956403860EBEB5 /* Pods */, - 8A1387E89A6BBC071B75FD6F /* 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 = ""; - }; - C52D9D4A70956403860EBEB5 /* Pods */ = { - isa = PBXGroup; - children = ( - 9AC7510327AD6A32B7CBD9A5 /* Pods-Runner.debug.xcconfig */, - 483D985F075B951ADBAD218E /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 3E30118C54AB12C3EB9EDF27 /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - FE224661708E6DA2A0F8B952 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; - }; - 3E30118C54AB12C3EB9EDF27 /* [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"; - }; - FE224661708E6DA2A0F8B952 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.cameraExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.cameraExample; - 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/camera/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/camera/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a16ed0f..000000000000 --- a/packages/camera/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/packages/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 44b873626ab3..000000000000 --- a/packages/camera/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/camera/example/ios/Runner/AppDelegate.h b/packages/camera/example/ios/Runner/AppDelegate.h deleted file mode 100644 index 36e21bbf9cf4..000000000000 --- a/packages/camera/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,6 +0,0 @@ -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/camera/example/ios/Runner/AppDelegate.m b/packages/camera/example/ios/Runner/AppDelegate.m deleted file mode 100644 index 59a72e90be12..000000000000 --- a/packages/camera/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,13 +0,0 @@ -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/packages/camera/example/ios/Runner/Info.plist b/packages/camera/example/ios/Runner/Info.plist deleted file mode 100644 index f389a129e028..000000000000 --- a/packages/camera/example/ios/Runner/Info.plist +++ /dev/null @@ -1,55 +0,0 @@ - - - - - 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 - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/camera/example/ios/Runner/main.m b/packages/camera/example/ios/Runner/main.m deleted file mode 100644 index dff6597e4513..000000000000 --- a/packages/camera/example/ios/Runner/main.m +++ /dev/null @@ -1,9 +0,0 @@ -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/camera/example/lib/main.dart b/packages/camera/example/lib/main.dart deleted file mode 100644 index ce8d37457123..000000000000 --- a/packages/camera/example/lib/main.dart +++ /dev/null @@ -1,488 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// ignore_for_file: public_member_api_docs - -import 'dart:async'; -import 'dart:io'; - -import 'package:camera/camera.dart'; -import 'package:flutter/material.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:video_player/video_player.dart'; - -class CameraExampleHome extends StatefulWidget { - @override - _CameraExampleHomeState 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; - } - throw ArgumentError('Unknown lens direction'); -} - -void logError(String code, String message) => - print('Error: $code\nError Message: $message'); - -class _CameraExampleHomeState extends State - with WidgetsBindingObserver { - CameraController controller; - String imagePath; - String videoPath; - VideoPlayerController videoController; - VoidCallback videoPlayerListener; - bool enableAudio = true; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addObserver(this); - } - - @override - void dispose() { - WidgetsBinding.instance.removeObserver(this); - super.dispose(); - } - - @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); - } - } - } - - final GlobalKey _scaffoldKey = GlobalKey(); - - @override - Widget build(BuildContext context) { - return Scaffold( - key: _scaffoldKey, - appBar: AppBar( - title: const Text('Camera example'), - ), - body: Column( - children: [ - Expanded( - child: Container( - child: Padding( - padding: const EdgeInsets.all(1.0), - child: Center( - child: _cameraPreviewWidget(), - ), - ), - decoration: BoxDecoration( - color: Colors.black, - border: Border.all( - color: controller != null && controller.value.isRecordingVideo - ? Colors.redAccent - : Colors.grey, - width: 3.0, - ), - ), - ), - ), - _captureControlRowWidget(), - _toggleAudioWidget(), - 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() { - if (controller == null || !controller.value.isInitialized) { - return const Text( - 'Tap a camera', - style: TextStyle( - color: Colors.white, - fontSize: 24.0, - fontWeight: FontWeight.w900, - ), - ); - } else { - return AspectRatio( - aspectRatio: controller.value.aspectRatio, - child: CameraPreview(controller), - ); - } - } - - /// Toggle recording audio - Widget _toggleAudioWidget() { - return Padding( - padding: const EdgeInsets.only(left: 25), - child: Row( - children: [ - const Text('Enable Audio:'), - Switch( - value: enableAudio, - onChanged: (bool value) { - enableAudio = value; - if (controller != null) { - onNewCameraSelected(controller.description); - } - }, - ), - ], - ), - ); - } - - /// Display the thumbnail of the captured image or video. - Widget _thumbnailWidget() { - return Expanded( - child: Align( - alignment: Alignment.centerRight, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - videoController == null && imagePath == null - ? Container() - : SizedBox( - child: (videoController == null) - ? Image.file(File(imagePath)) - : Container( - child: Center( - child: AspectRatio( - aspectRatio: - videoController.value.size != null - ? videoController.value.aspectRatio - : 1.0, - child: VideoPlayer(videoController)), - ), - decoration: BoxDecoration( - border: Border.all(color: Colors.pink)), - ), - width: 64.0, - height: 64.0, - ), - ], - ), - ), - ); - } - - /// Display the control bar with buttons to take pictures and record videos. - Widget _captureControlRowWidget() { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - mainAxisSize: MainAxisSize.max, - children: [ - IconButton( - icon: const Icon(Icons.camera_alt), - color: Colors.blue, - onPressed: controller != null && - controller.value.isInitialized && - !controller.value.isRecordingVideo - ? onTakePictureButtonPressed - : null, - ), - IconButton( - icon: const Icon(Icons.videocam), - color: Colors.blue, - onPressed: controller != null && - controller.value.isInitialized && - !controller.value.isRecordingVideo - ? onVideoRecordButtonPressed - : null, - ), - IconButton( - icon: controller != null && controller.value.isRecordingPaused - ? Icon(Icons.play_arrow) - : Icon(Icons.pause), - color: Colors.blue, - onPressed: controller != null && - controller.value.isInitialized && - controller.value.isRecordingVideo - ? (controller != null && controller.value.isRecordingPaused - ? onResumeButtonPressed - : onPauseButtonPressed) - : null, - ), - IconButton( - icon: const Icon(Icons.stop), - color: Colors.red, - onPressed: controller != null && - controller.value.isInitialized && - controller.value.isRecordingVideo - ? onStopButtonPressed - : null, - ) - ], - ); - } - - /// Display a row of toggle to select the camera (or a message if no camera is available). - Widget _cameraTogglesRowWidget() { - final List toggles = []; - - if (cameras.isEmpty) { - return const Text('No camera found'); - } else { - for (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 - : onNewCameraSelected, - ), - ), - ); - } - } - - return Row(children: toggles); - } - - String timestamp() => DateTime.now().millisecondsSinceEpoch.toString(); - - void showInSnackBar(String message) { - _scaffoldKey.currentState.showSnackBar(SnackBar(content: Text(message))); - } - - void onNewCameraSelected(CameraDescription cameraDescription) async { - if (controller != null) { - await controller.dispose(); - } - controller = CameraController( - cameraDescription, - ResolutionPreset.medium, - enableAudio: enableAudio, - ); - - // If the controller is updated then update the UI. - controller.addListener(() { - if (mounted) setState(() {}); - if (controller.value.hasError) { - showInSnackBar('Camera error ${controller.value.errorDescription}'); - } - }); - - try { - await controller.initialize(); - } on CameraException catch (e) { - _showCameraException(e); - } - - if (mounted) { - setState(() {}); - } - } - - void onTakePictureButtonPressed() { - takePicture().then((String filePath) { - if (mounted) { - setState(() { - imagePath = filePath; - videoController?.dispose(); - videoController = null; - }); - if (filePath != null) showInSnackBar('Picture saved to $filePath'); - } - }); - } - - void onVideoRecordButtonPressed() { - startVideoRecording().then((String filePath) { - if (mounted) setState(() {}); - if (filePath != null) showInSnackBar('Saving video to $filePath'); - }); - } - - void onStopButtonPressed() { - stopVideoRecording().then((_) { - if (mounted) setState(() {}); - showInSnackBar('Video recorded to: $videoPath'); - }); - } - - 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 { - if (!controller.value.isInitialized) { - showInSnackBar('Error: select a camera first.'); - return null; - } - - final Directory extDir = await getApplicationDocumentsDirectory(); - final String dirPath = '${extDir.path}/Movies/flutter_test'; - await Directory(dirPath).create(recursive: true); - final String filePath = '$dirPath/${timestamp()}.mp4'; - - if (controller.value.isRecordingVideo) { - // A recording is already started, do nothing. - return null; - } - - try { - videoPath = filePath; - await controller.startVideoRecording(filePath); - } on CameraException catch (e) { - _showCameraException(e); - return null; - } - return filePath; - } - - Future stopVideoRecording() async { - if (!controller.value.isRecordingVideo) { - return null; - } - - try { - await controller.stopVideoRecording(); - } on CameraException catch (e) { - _showCameraException(e); - return null; - } - - await _startVideoPlayer(); - } - - Future pauseVideoRecording() async { - if (!controller.value.isRecordingVideo) { - return null; - } - - try { - await controller.pauseVideoRecording(); - } on CameraException catch (e) { - _showCameraException(e); - rethrow; - } - } - - Future resumeVideoRecording() async { - if (!controller.value.isRecordingVideo) { - return null; - } - - try { - await controller.resumeVideoRecording(); - } on CameraException catch (e) { - _showCameraException(e); - rethrow; - } - } - - Future _startVideoPlayer() async { - final VideoPlayerController vcontroller = - VideoPlayerController.file(File(videoPath)); - 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(() { - imagePath = null; - videoController = vcontroller; - }); - } - await vcontroller.play(); - } - - Future takePicture() async { - if (!controller.value.isInitialized) { - showInSnackBar('Error: select a camera first.'); - return null; - } - final Directory extDir = await getApplicationDocumentsDirectory(); - final String dirPath = '${extDir.path}/Pictures/flutter_test'; - await Directory(dirPath).create(recursive: true); - final String filePath = '$dirPath/${timestamp()}.jpg'; - - if (controller.value.isTakingPicture) { - // A capture is already pending, do nothing. - return null; - } - - try { - await controller.takePicture(filePath); - } on CameraException catch (e) { - _showCameraException(e); - return null; - } - return filePath; - } - - void _showCameraException(CameraException e) { - logError(e.code, e.description); - showInSnackBar('Error: ${e.code}\n${e.description}'); - } -} - -class CameraApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return MaterialApp( - home: CameraExampleHome(), - ); - } -} - -List cameras = []; - -Future main() async { - // Fetch the available cameras before initializing the app. - try { - WidgetsFlutterBinding.ensureInitialized(); - cameras = await availableCameras(); - } on CameraException catch (e) { - logError(e.code, e.description); - } - runApp(CameraApp()); -} diff --git a/packages/camera/example/pubspec.yaml b/packages/camera/example/pubspec.yaml deleted file mode 100644 index bc33087a5877..000000000000 --- a/packages/camera/example/pubspec.yaml +++ /dev/null @@ -1,26 +0,0 @@ -name: camera_example -description: Demonstrates how to use the camera plugin. - -dependencies: - camera: - path: ../ - path_provider: ^0.5.0 - flutter: - sdk: flutter - video_player: ^0.10.0 - integration_test: - path: ../../integration_test - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_driver: - sdk: flutter - pedantic: ^1.8.0 - -flutter: - uses-material-design: true - -environment: - sdk: ">=2.0.0-dev.28.0 <3.0.0" - flutter: ">=1.9.1+hotfix.4 <2.0.0" diff --git a/packages/camera/example/test_driver/integration_test.dart b/packages/camera/example/test_driver/integration_test.dart deleted file mode 100644 index 1e6e3ba7941f..000000000000 --- a/packages/camera/example/test_driver/integration_test.dart +++ /dev/null @@ -1,60 +0,0 @@ -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('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); - exit(result['result'] == 'true' ? 0 : 1); -} diff --git a/packages/camera/ios/Classes/CameraPlugin.m b/packages/camera/ios/Classes/CameraPlugin.m deleted file mode 100644 index 525c1286717a..000000000000 --- a/packages/camera/ios/Classes/CameraPlugin.m +++ /dev/null @@ -1,899 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "CameraPlugin.h" -#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; -@property(readonly, nonatomic) CMMotionManager *motionManager; -@property(readonly, nonatomic) AVCaptureDevicePosition cameraPosition; -@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 - motionManager:(CMMotionManager *)motionManager - cameraPosition:(AVCaptureDevicePosition)cameraPosition { - self = [super init]; - NSAssert(self, @"super init cannot be nil"); - _path = path; - _result = result; - _motionManager = motionManager; - _cameraPosition = cameraPosition; - selfReference = self; - 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]; - UIImage *image = [UIImage imageWithCGImage:[UIImage imageWithData:data].CGImage - scale:1.0 - orientation:[self getImageRotation]]; - // TODO(sigurdm): Consider writing file asynchronously. - bool success = [UIImageJPEGRepresentation(image, 1.0) writeToFile:_path atomically:YES]; - if (!success) { - _result([FlutterError errorWithCode:@"IOError" message:@"Unable to write file" details:nil]); - return; - } - _result(nil); -} - -- (UIImageOrientation)getImageRotation { - float const threshold = 45.0; - BOOL (^isNearValue)(float value1, float value2) = ^BOOL(float value1, float value2) { - return fabsf(value1 - value2) < threshold; - }; - BOOL (^isNearValueABS)(float value1, float value2) = ^BOOL(float value1, float value2) { - return isNearValue(fabsf(value1), fabsf(value2)); - }; - float yxAtan = (atan2(_motionManager.accelerometerData.acceleration.y, - _motionManager.accelerometerData.acceleration.x)) * - 180 / M_PI; - if (isNearValue(-90.0, yxAtan)) { - return UIImageOrientationRight; - } else if (isNearValueABS(180.0, yxAtan)) { - return _cameraPosition == AVCaptureDevicePositionBack ? UIImageOrientationUp - : UIImageOrientationDown; - } else if (isNearValueABS(0.0, yxAtan)) { - return _cameraPosition == AVCaptureDevicePositionBack ? UIImageOrientationDown /*rotate 180* */ - : UIImageOrientationUp /*do not rotate*/; - } else if (isNearValue(90.0, yxAtan)) { - return UIImageOrientationLeft; - } - // If none of the above, then the device is likely facing straight down or straight up -- just - // pick something arbitrary - // TODO: Maybe use the UIInterfaceOrientation if in these scenarios - return UIImageOrientationUp; -} -@end - -// 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) FlutterEventChannel *eventChannel; -@property(nonatomic) FLTImageStreamHandler *imageStreamHandler; -@property(nonatomic) FlutterEventSink eventSink; -@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(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) ResolutionPreset resolutionPreset; -@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; -} -// Format used for video and image streaming. -FourCharCode const videoFormat = kCVPixelFormatType_32BGRA; - -- (instancetype)initWithCameraName:(NSString *)cameraName - resolutionPreset:(NSString *)resolutionPreset - enableAudio:(BOOL)enableAudio - 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]; - 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; - } - connection.videoOrientation = AVCaptureVideoOrientationPortrait; - [_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]; - return self; -} - -- (void)start { - [_captureSession startRunning]; -} - -- (void)stop { - [_captureSession stopRunning]; -} - -- (void)captureToFile:(NSString *)path result:(FlutterResult)result API_AVAILABLE(ios(10)) { - AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; - if (_resolutionPreset == max) { - [settings setHighResolutionPhotoEnabled:YES]; - } - [_capturePhotoOutput - capturePhotoWithSettings:settings - delegate:[[FLTSavePhotoDelegate alloc] initWithPath:path - result:result - motionManager:_motionManager - cameraPosition:_captureDevice.position]]; -} - -- (void)setCaptureSessionPreset:(ResolutionPreset)resolutionPreset { - switch (resolutionPreset) { - case max: - case ultraHigh: - if (@available(iOS 9.0, *)) { - 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)) { - _eventSink(@{ - @"event" : @"error", - @"errorDescription" : @"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; - - _imageStreamHandler.eventSink(imageBuffer); - - CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); - } - } - if (_isRecording && !_isRecordingPaused) { - if (_videoWriter.status == AVAssetWriterStatusFailed) { - _eventSink(@{ - @"event" : @"error", - @"errorDescription" : [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) { - _eventSink(@{ - @"event" : @"error", - @"errorDescription" : [NSString stringWithFormat:@"%@", _videoWriter.error] - }); - } - return; - } - if (_videoWriterInput.readyForMoreMediaData) { - if (![_videoWriterInput appendSampleBuffer:sampleBuffer]) { - _eventSink(@{ - @"event" : @"error", - @"errorDescription" : - [NSString stringWithFormat:@"%@", @"Unable to write to video input"] - }); - } - } -} - -- (void)newAudioSample:(CMSampleBufferRef)sampleBuffer { - if (_videoWriter.status != AVAssetWriterStatusWriting) { - if (_videoWriter.status == AVAssetWriterStatusFailed) { - _eventSink(@{ - @"event" : @"error", - @"errorDescription" : [NSString stringWithFormat:@"%@", _videoWriter.error] - }); - } - return; - } - if (_audioWriterInput.readyForMoreMediaData) { - if (![_audioWriterInput appendSampleBuffer:sampleBuffer]) { - _eventSink(@{ - @"event" : @"error", - @"errorDescription" : - [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; -} - -- (FlutterError *_Nullable)onCancelWithArguments:(id _Nullable)arguments { - _eventSink = nil; - // need to unregister stream handler when disposing the camera - [_eventChannel setStreamHandler:nil]; - return nil; -} - -- (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments - eventSink:(nonnull FlutterEventSink)events { - _eventSink = events; - return nil; -} - -- (void)startVideoRecordingAtPath:(NSString *)path result:(FlutterResult)result { - if (!_isRecording) { - if (![self setupWriterForPath:path]) { - _eventSink(@{@"event" : @"error", @"errorDescription" : @"Setup Writer Failed"}); - return; - } - _isRecording = YES; - _isRecordingPaused = NO; - _videoTimeOffset = CMTimeMake(0, 1); - _audioTimeOffset = CMTimeMake(0, 1); - _videoIsDisconnected = NO; - _audioIsDisconnected = NO; - result(nil); - } else { - _eventSink(@{@"event" : @"error", @"errorDescription" : @"Video is already recording!"}); - } -} - -- (void)stopVideoRecordingWithResult:(FlutterResult)result { - if (_isRecording) { - _isRecording = NO; - if (_videoWriter.status != AVAssetWriterStatusUnknown) { - [_videoWriter finishWritingWithCompletionHandler:^{ - if (self->_videoWriter.status == AVAssetWriterStatusCompleted) { - result(nil); - } else { - self->_eventSink(@{ - @"event" : @"error", - @"errorDescription" : @"AVAssetWriter could not finish writing!" - }); - } - }]; - } - } else { - NSError *error = - [NSError errorWithDomain:NSCocoaErrorDomain - code:NSURLErrorResourceUnavailable - userInfo:@{NSLocalizedDescriptionKey : @"Video is not recording!"}]; - result(getFlutterError(error)); - } -} - -- (void)pauseVideoRecording { - _isRecordingPaused = YES; - _videoIsDisconnected = YES; - _audioIsDisconnected = YES; -} - -- (void)resumeVideoRecording { - _isRecordingPaused = NO; -} - -- (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 { - _eventSink( - @{@"event" : @"error", @"errorDescription" : @"Images from camera are already streaming!"}); - } -} - -- (void)stopImageStream { - if (_isStreamingImages) { - _isStreamingImages = NO; - _imageStreamHandler = nil; - } else { - _eventSink( - @{@"event" : @"error", @"errorDescription" : @"Images from camera are not streaming!"}); - } -} - -- (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:AVFileTypeQuickTimeMovie - error:&error]; - NSParameterAssert(_videoWriter); - if (error) { - _eventSink(@{@"event" : @"error", @"errorDescription" : error.description}); - return NO; - } - NSDictionary *videoSettings = [NSDictionary - dictionaryWithObjectsAndKeys:AVVideoCodecH264, AVVideoCodecKey, - [NSNumber numberWithInt:_previewSize.height], AVVideoWidthKey, - [NSNumber numberWithInt:_previewSize.width], AVVideoHeightKey, - nil]; - _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 = [NSDictionary - dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:kAudioFormatMPEG4AAC], AVFormatIDKey, - [NSNumber numberWithFloat:44100.0], AVSampleRateKey, - [NSNumber numberWithInt:1], AVNumberOfChannelsKey, - [NSData dataWithBytes:&acl length:sizeof(acl)], - AVChannelLayoutKey, nil]; - _audioWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio - outputSettings:audioOutputSettings]; - _audioWriterInput.expectsMediaDataInRealTime = YES; - - [_videoWriter addInput:_audioWriterInput]; - [_audioOutput setSampleBufferDelegate:self queue:_dispatchQueue]; - } - - [_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) { - _eventSink(@{@"event" : @"error", @"errorDescription" : 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 { - _eventSink(@{ - @"event" : @"error", - @"errorDescription" : @"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; -@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; - return self; -} - -- (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 ([@"initialize" 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] - dispatchQueue:_dispatchQueue - error:&error]; - if (error) { - result(getFlutterError(error)); - } else { - if (_camera) { - [_camera close]; - } - int64_t textureId = [_registry registerTexture:cam]; - _camera = cam; - __weak CameraPlugin *weakSelf = self; - cam.onFrameAvailable = ^{ - [weakSelf.registry textureFrameAvailable:textureId]; - }; - FlutterEventChannel *eventChannel = [FlutterEventChannel - eventChannelWithName:[NSString - stringWithFormat:@"flutter.io/cameraPlugin/cameraEvents%lld", - textureId] - binaryMessenger:_messenger]; - [eventChannel setStreamHandler:cam]; - cam.eventChannel = eventChannel; - result(@{ - @"textureId" : @(textureId), - @"previewWidth" : @(cam.previewSize.width), - @"previewHeight" : @(cam.previewSize.height), - @"captureWidth" : @(cam.captureSize.width), - @"captureHeight" : @(cam.captureSize.height), - }); - [cam start]; - } - } else if ([@"startImageStream" isEqualToString:call.method]) { - [_camera startImageStreamWithMessenger:_messenger]; - result(nil); - } else if ([@"stopImageStream" isEqualToString:call.method]) { - [_camera stopImageStream]; - result(nil); - } else if ([@"pauseVideoRecording" isEqualToString:call.method]) { - [_camera pauseVideoRecording]; - result(nil); - } else if ([@"resumeVideoRecording" isEqualToString:call.method]) { - [_camera resumeVideoRecording]; - result(nil); - } else { - NSDictionary *argsMap = call.arguments; - NSUInteger textureId = ((NSNumber *)argsMap[@"textureId"]).unsignedIntegerValue; - if ([@"takePicture" isEqualToString:call.method]) { - if (@available(iOS 10.0, *)) { - [_camera captureToFile:call.arguments[@"path"] result:result]; - } else { - result(FlutterMethodNotImplemented); - } - } else if ([@"dispose" isEqualToString:call.method]) { - [_registry unregisterTexture:textureId]; - [_camera close]; - _dispatchQueue = nil; - result(nil); - } else if ([@"prepareForVideoRecording" isEqualToString:call.method]) { - [_camera setUpCaptureSessionForAudio]; - result(nil); - } else if ([@"startVideoRecording" isEqualToString:call.method]) { - [_camera startVideoRecordingAtPath:call.arguments[@"filePath"] result:result]; - } else if ([@"stopVideoRecording" isEqualToString:call.method]) { - [_camera stopVideoRecordingWithResult:result]; - } else { - result(FlutterMethodNotImplemented); - } - } -} - -@end diff --git a/packages/camera/ios/Tests/CameraPluginTests.m b/packages/camera/ios/Tests/CameraPluginTests.m deleted file mode 100644 index e5be3980bad0..000000000000 --- a/packages/camera/ios/Tests/CameraPluginTests.m +++ /dev/null @@ -1,16 +0,0 @@ -@import camera; -@import XCTest; - -@interface CameraPluginTests : XCTestCase -@end - -@implementation CameraPluginTests - -- (void)testModuleImport { - // This test will fail to compile if the module cannot be imported. - // Make sure this plugin supports modules. See https://github.com/flutter/flutter/issues/41007. - // If not already present, add this line to the podspec: - // s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } -} - -@end diff --git a/packages/camera/lib/camera.dart b/packages/camera/lib/camera.dart deleted file mode 100644 index ce9fd9430dde..000000000000 --- a/packages/camera/lib/camera.dart +++ /dev/null @@ -1,589 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:typed_data'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; - -part 'camera_image.dart'; - -final MethodChannel _channel = const MethodChannel('plugins.flutter.io/camera'); - -enum CameraLensDirection { front, back, external } - -/// Affect the quality of video recording and image capture: -/// -/// If a preset is not available on the camera being used a preset of lower quality will be selected automatically. -enum ResolutionPreset { - /// 352x288 on iOS, 240p (320x240) on Android - low, - - /// 480p (640x480 on iOS, 720x480 on Android) - medium, - - /// 720p (1280x720) - high, - - /// 1080p (1920x1080) - veryHigh, - - /// 2160p (3840x2160) - ultraHigh, - - /// The highest resolution available. - max, -} - -// ignore: inference_failure_on_function_return_type -typedef onLatestImageAvailable = Function(CameraImage image); - -/// Returns the resolution preset as a String. -String serializeResolutionPreset(ResolutionPreset resolutionPreset) { - switch (resolutionPreset) { - case ResolutionPreset.max: - return 'max'; - case ResolutionPreset.ultraHigh: - return 'ultraHigh'; - case ResolutionPreset.veryHigh: - return 'veryHigh'; - case ResolutionPreset.high: - return 'high'; - case ResolutionPreset.medium: - return 'medium'; - case ResolutionPreset.low: - return 'low'; - } - throw ArgumentError('Unknown ResolutionPreset value'); -} - -CameraLensDirection _parseCameraLensDirection(String string) { - switch (string) { - case 'front': - return CameraLensDirection.front; - case 'back': - return CameraLensDirection.back; - case 'external': - return CameraLensDirection.external; - } - throw ArgumentError('Unknown CameraLensDirection value'); -} - -/// Completes with a list of available cameras. -/// -/// May throw a [CameraException]. -Future> availableCameras() async { - try { - final List> cameras = await _channel - .invokeListMethod>('availableCameras'); - return cameras.map((Map camera) { - return CameraDescription( - name: camera['name'], - lensDirection: _parseCameraLensDirection(camera['lensFacing']), - sensorOrientation: camera['sensorOrientation'], - ); - }).toList(); - } on PlatformException catch (e) { - throw CameraException(e.code, e.message); - } -} - -class CameraDescription { - CameraDescription({this.name, this.lensDirection, this.sensorOrientation}); - - final String name; - final CameraLensDirection lensDirection; - - /// Clockwise angle through which the output image needs to be rotated to be upright on the device screen in its native orientation. - /// - /// **Range of valid values:** - /// 0, 90, 180, 270 - /// - /// On Android, also defines the direction of rolling shutter readout, which - /// is from top to bottom in the sensor's coordinate system. - final int sensorOrientation; - - @override - bool operator ==(Object o) { - return o is CameraDescription && - o.name == name && - o.lensDirection == lensDirection; - } - - @override - int get hashCode { - return hashValues(name, lensDirection); - } - - @override - String toString() { - return '$runtimeType($name, $lensDirection, $sensorOrientation)'; - } -} - -/// This is thrown when the plugin reports an error. -class CameraException implements Exception { - CameraException(this.code, this.description); - - String code; - String description; - - @override - String toString() => '$runtimeType($code, $description)'; -} - -// Build the UI texture view of the video data with textureId. -class CameraPreview extends StatelessWidget { - const CameraPreview(this.controller); - - final CameraController controller; - - @override - Widget build(BuildContext context) { - return controller.value.isInitialized - ? Texture(textureId: controller._textureId) - : Container(); - } -} - -/// The state of a [CameraController]. -class CameraValue { - const CameraValue({ - this.isInitialized, - this.errorDescription, - this.previewSize, - this.isRecordingVideo, - this.isTakingPicture, - this.isStreamingImages, - bool isRecordingPaused, - }) : _isRecordingPaused = isRecordingPaused; - - const CameraValue.uninitialized() - : this( - isInitialized: false, - isRecordingVideo: false, - isTakingPicture: false, - isStreamingImages: false, - isRecordingPaused: false, - ); - - /// True after [CameraController.initialize] has completed successfully. - final bool isInitialized; - - /// True when a picture capture request has been sent but as not yet returned. - final bool isTakingPicture; - - /// True when the camera is recording (not the same as previewing). - final bool isRecordingVideo; - - /// True when images from the camera are being streamed. - final bool isStreamingImages; - - final bool _isRecordingPaused; - - /// True when camera [isRecordingVideo] and recording is paused. - bool get isRecordingPaused => isRecordingVideo && _isRecordingPaused; - - final String errorDescription; - - /// The size of the preview in pixels. - /// - /// Is `null` until [isInitialized] is `true`. - final Size previewSize; - - /// Convenience getter for `previewSize.height / previewSize.width`. - /// - /// Can only be called when [initialize] is done. - double get aspectRatio => previewSize.height / previewSize.width; - - bool get hasError => errorDescription != null; - - CameraValue copyWith({ - bool isInitialized, - bool isRecordingVideo, - bool isTakingPicture, - bool isStreamingImages, - String errorDescription, - Size previewSize, - bool isRecordingPaused, - }) { - return CameraValue( - isInitialized: isInitialized ?? this.isInitialized, - errorDescription: errorDescription, - previewSize: previewSize ?? this.previewSize, - isRecordingVideo: isRecordingVideo ?? this.isRecordingVideo, - isTakingPicture: isTakingPicture ?? this.isTakingPicture, - isStreamingImages: isStreamingImages ?? this.isStreamingImages, - isRecordingPaused: isRecordingPaused ?? _isRecordingPaused, - ); - } - - @override - String toString() { - return '$runtimeType(' - 'isRecordingVideo: $isRecordingVideo, ' - 'isRecordingVideo: $isRecordingVideo, ' - 'isInitialized: $isInitialized, ' - 'errorDescription: $errorDescription, ' - 'previewSize: $previewSize, ' - 'isStreamingImages: $isStreamingImages)'; - } -} - -/// Controls a device camera. -/// -/// Use [availableCameras] to get a list of available cameras. -/// -/// Before using a [CameraController] a call to [initialize] must complete. -/// -/// To show the camera preview on the screen use a [CameraPreview] widget. -class CameraController extends ValueNotifier { - CameraController( - this.description, - this.resolutionPreset, { - this.enableAudio = true, - }) : super(const CameraValue.uninitialized()); - - final CameraDescription description; - final ResolutionPreset resolutionPreset; - - /// Whether to include audio when recording a video. - final bool enableAudio; - - int _textureId; - bool _isDisposed = false; - StreamSubscription _eventSubscription; - StreamSubscription _imageStreamSubscription; - Completer _creatingCompleter; - - /// Initializes the camera on the device. - /// - /// Throws a [CameraException] if the initialization fails. - Future initialize() async { - if (_isDisposed) { - return Future.value(); - } - try { - _creatingCompleter = Completer(); - final Map reply = - await _channel.invokeMapMethod( - 'initialize', - { - 'cameraName': description.name, - 'resolutionPreset': serializeResolutionPreset(resolutionPreset), - 'enableAudio': enableAudio, - }, - ); - _textureId = reply['textureId']; - value = value.copyWith( - isInitialized: true, - previewSize: Size( - reply['previewWidth'].toDouble(), - reply['previewHeight'].toDouble(), - ), - ); - } on PlatformException catch (e) { - throw CameraException(e.code, e.message); - } - _eventSubscription = - EventChannel('flutter.io/cameraPlugin/cameraEvents$_textureId') - .receiveBroadcastStream() - .listen(_listener); - _creatingCompleter.complete(); - return _creatingCompleter.future; - } - - /// Prepare the capture session for video recording. - /// - /// Use of this method is optional, but it may be called for performance - /// reasons on iOS. - /// - /// Preparing audio can cause a minor delay in the CameraPreview view on iOS. - /// If video recording is intended, calling this early eliminates this delay - /// that would otherwise be experienced when video recording is started. - /// This operation is a no-op on Android. - /// - /// Throws a [CameraException] if the prepare fails. - Future prepareForVideoRecording() async { - await _channel.invokeMethod('prepareForVideoRecording'); - } - - /// Listen to events from the native plugins. - /// - /// A "cameraClosing" event is sent when the camera is closed automatically by the system (for example when the app go to background). The plugin will try to reopen the camera automatically but any ongoing recording will end. - void _listener(dynamic event) { - final Map map = event; - if (_isDisposed) { - return; - } - - switch (map['eventType']) { - case 'error': - value = value.copyWith(errorDescription: event['errorDescription']); - break; - case 'cameraClosing': - value = value.copyWith(isRecordingVideo: false); - break; - } - } - - /// Captures an image and saves it to [path]. - /// - /// A path can for example be obtained using - /// [path_provider](https://pub.dartlang.org/packages/path_provider). - /// - /// If a file already exists at the provided path an error will be thrown. - /// The file can be read as this function returns. - /// - /// Throws a [CameraException] if the capture fails. - Future takePicture(String path) async { - if (!value.isInitialized || _isDisposed) { - throw CameraException( - 'Uninitialized CameraController.', - 'takePicture was called on uninitialized CameraController', - ); - } - if (value.isTakingPicture) { - throw CameraException( - 'Previous capture has not returned yet.', - 'takePicture was called before the previous capture returned.', - ); - } - try { - value = value.copyWith(isTakingPicture: true); - await _channel.invokeMethod( - 'takePicture', - {'textureId': _textureId, 'path': path}, - ); - value = value.copyWith(isTakingPicture: false); - } on PlatformException catch (e) { - value = value.copyWith(isTakingPicture: false); - throw CameraException(e.code, e.message); - } - } - - /// Start streaming images from platform camera. - /// - /// Settings for capturing images on iOS and Android is set to always use the - /// latest image available from the camera and will drop all other images. - /// - /// When running continuously with [CameraPreview] widget, this function runs - /// best with [ResolutionPreset.low]. Running on [ResolutionPreset.high] can - /// have significant frame rate drops for [CameraPreview] on lower end - /// devices. - /// - /// Throws a [CameraException] if image streaming or video recording has - /// already started. - // TODO(bmparr): Add settings for resolution and fps. - Future startImageStream(onLatestImageAvailable onAvailable) async { - if (!value.isInitialized || _isDisposed) { - throw CameraException( - 'Uninitialized CameraController', - 'startImageStream was called on uninitialized CameraController.', - ); - } - if (value.isRecordingVideo) { - throw CameraException( - 'A video recording is already started.', - 'startImageStream was called while a video is being recorded.', - ); - } - if (value.isStreamingImages) { - throw CameraException( - 'A camera has started streaming images.', - 'startImageStream was called while a camera was streaming images.', - ); - } - - try { - await _channel.invokeMethod('startImageStream'); - value = value.copyWith(isStreamingImages: true); - } on PlatformException catch (e) { - throw CameraException(e.code, e.message); - } - const EventChannel cameraEventChannel = - EventChannel('plugins.flutter.io/camera/imageStream'); - _imageStreamSubscription = - cameraEventChannel.receiveBroadcastStream().listen( - (dynamic imageData) { - onAvailable(CameraImage._fromPlatformData(imageData)); - }, - ); - } - - /// Stop streaming images from platform camera. - /// - /// Throws a [CameraException] if image streaming was not started or video - /// recording was started. - Future stopImageStream() async { - if (!value.isInitialized || _isDisposed) { - throw CameraException( - 'Uninitialized CameraController', - 'stopImageStream was called on uninitialized CameraController.', - ); - } - if (value.isRecordingVideo) { - throw CameraException( - 'A video recording is already started.', - 'stopImageStream was called while a video is being recorded.', - ); - } - if (!value.isStreamingImages) { - throw CameraException( - 'No camera is streaming images', - 'stopImageStream was called when no camera is streaming images.', - ); - } - - try { - value = value.copyWith(isStreamingImages: false); - await _channel.invokeMethod('stopImageStream'); - } on PlatformException catch (e) { - throw CameraException(e.code, e.message); - } - - await _imageStreamSubscription.cancel(); - _imageStreamSubscription = null; - } - - /// Start a video recording and save the file to [path]. - /// - /// A path can for example be obtained using - /// [path_provider](https://pub.dartlang.org/packages/path_provider). - /// - /// The file is written on the flight as the video is being recorded. - /// If a file already exists at the provided path an error will be thrown. - /// The file can be read as soon as [stopVideoRecording] returns. - /// - /// Throws a [CameraException] if the capture fails. - Future startVideoRecording(String filePath) async { - if (!value.isInitialized || _isDisposed) { - throw CameraException( - 'Uninitialized CameraController', - 'startVideoRecording was called on uninitialized CameraController', - ); - } - if (value.isRecordingVideo) { - throw CameraException( - 'A video recording is already started.', - 'startVideoRecording was called when a recording is already started.', - ); - } - if (value.isStreamingImages) { - throw CameraException( - 'A camera has started streaming images.', - 'startVideoRecording was called while a camera was streaming images.', - ); - } - - try { - await _channel.invokeMethod( - 'startVideoRecording', - {'textureId': _textureId, 'filePath': filePath}, - ); - value = value.copyWith(isRecordingVideo: true, isRecordingPaused: false); - } on PlatformException catch (e) { - throw CameraException(e.code, e.message); - } - } - - /// Stop recording. - Future stopVideoRecording() async { - if (!value.isInitialized || _isDisposed) { - throw CameraException( - 'Uninitialized CameraController', - 'stopVideoRecording was called on uninitialized CameraController', - ); - } - if (!value.isRecordingVideo) { - throw CameraException( - 'No video is recording', - 'stopVideoRecording was called when no video is recording.', - ); - } - try { - value = value.copyWith(isRecordingVideo: false); - await _channel.invokeMethod( - 'stopVideoRecording', - {'textureId': _textureId}, - ); - } on PlatformException catch (e) { - throw CameraException(e.code, e.message); - } - } - - /// Pause video recording. - /// - /// This feature is only available on iOS and Android sdk 24+. - Future pauseVideoRecording() async { - if (!value.isInitialized || _isDisposed) { - throw CameraException( - 'Uninitialized CameraController', - 'pauseVideoRecording was called on uninitialized CameraController', - ); - } - if (!value.isRecordingVideo) { - throw CameraException( - 'No video is recording', - 'pauseVideoRecording was called when no video is recording.', - ); - } - try { - value = value.copyWith(isRecordingPaused: true); - await _channel.invokeMethod( - 'pauseVideoRecording', - {'textureId': _textureId}, - ); - } on PlatformException catch (e) { - throw CameraException(e.code, e.message); - } - } - - /// Resume video recording after pausing. - /// - /// This feature is only available on iOS and Android sdk 24+. - Future resumeVideoRecording() async { - if (!value.isInitialized || _isDisposed) { - throw CameraException( - 'Uninitialized CameraController', - 'resumeVideoRecording was called on uninitialized CameraController', - ); - } - if (!value.isRecordingVideo) { - throw CameraException( - 'No video is recording', - 'resumeVideoRecording was called when no video is recording.', - ); - } - try { - value = value.copyWith(isRecordingPaused: false); - await _channel.invokeMethod( - 'resumeVideoRecording', - {'textureId': _textureId}, - ); - } on PlatformException catch (e) { - throw CameraException(e.code, e.message); - } - } - - /// Releases the resources of this camera. - @override - Future dispose() async { - if (_isDisposed) { - return; - } - _isDisposed = true; - super.dispose(); - if (_creatingCompleter != null) { - await _creatingCompleter.future; - await _channel.invokeMethod( - 'dispose', - {'textureId': _textureId}, - ); - await _eventSubscription?.cancel(); - } - } -} diff --git a/packages/camera/lib/camera_image.dart b/packages/camera/lib/camera_image.dart deleted file mode 100644 index cebc14873f52..000000000000 --- a/packages/camera/lib/camera_image.dart +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -part of 'camera.dart'; - -/// 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._fromPlatformData(Map data) - : bytes = data['bytes'], - bytesPerPixel = data['bytesPerPixel'], - bytesPerRow = data['bytesPerRow'], - height = data['height'], - width = data['width']; - - /// Bytes representing this plane. - final Uint8List bytes; - - /// The distance between adjacent pixel samples on Android, in bytes. - /// - /// Will be `null` on iOS. - final int bytesPerPixel; - - /// The row stride for this color plane, in bytes. - final int bytesPerRow; - - /// Height of the pixel buffer on iOS. - /// - /// Will be `null` on Android - final int height; - - /// Width of the pixel buffer on iOS. - /// - /// Will be `null` on Android. - final int width; -} - -// TODO:(bmparr) Turn [ImageFormatGroup] to a class with int values. -/// Group of image formats that are comparable across Android and iOS platforms. -enum ImageFormatGroup { - /// The image format does not fit into any specific group. - unknown, - - /// Multi-plane YUV 420 format. - /// - /// This format is a generic YCbCr format, capable of describing any 4:2:0 - /// chroma-subsampled planar or semiplanar buffer (but not fully interleaved), - /// with 8 bits per color sample. - /// - /// On Android, this is `android.graphics.ImageFormat.YUV_420_888`. See - /// https://developer.android.com/reference/android/graphics/ImageFormat.html#YUV_420_888 - /// - /// On iOS, this is `kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange`. See - /// https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers/kcvpixelformattype_420ypcbcr8biplanarvideorange?language=objc - yuv420, - - /// 32-bit BGRA. - /// - /// On iOS, this is `kCVPixelFormatType_32BGRA`. See - /// https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers/kcvpixelformattype_32bgra?language=objc - bgra8888, -} - -/// Describes how pixels are represented in an image. -class ImageFormat { - ImageFormat._fromPlatformData(this.raw) : group = _asImageFormatGroup(raw); - - /// Describes the format group the raw image format falls into. - final ImageFormatGroup group; - - /// Raw version of the format from the Android or iOS platform. - /// - /// On Android, this is an `int` from class `android.graphics.ImageFormat`. See - /// https://developer.android.com/reference/android/graphics/ImageFormat - /// - /// On iOS, this is a `FourCharCode` constant from Pixel Format Identifiers. - /// See https://developer.apple.com/documentation/corevideo/1563591-pixel_format_identifiers?language=objc - final dynamic raw; -} - -ImageFormatGroup _asImageFormatGroup(dynamic rawFormat) { - if (defaultTargetPlatform == TargetPlatform.android) { - // android.graphics.ImageFormat.YUV_420_888 - if (rawFormat == 35) { - return ImageFormatGroup.yuv420; - } - } - - if (defaultTargetPlatform == TargetPlatform.iOS) { - switch (rawFormat) { - // kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange - case 875704438: - return ImageFormatGroup.yuv420; - // kCVPixelFormatType_32BGRA - case 1111970369: - return ImageFormatGroup.bgra8888; - } - } - - return ImageFormatGroup.unknown; -} - -/// A single complete image buffer from the platform camera. -/// -/// This class allows for direct application access to the pixel data of an -/// Image through one or more [Uint8List]. Each buffer is encapsulated in a -/// [Plane] that describes the layout of the pixel data in that plane. The -/// [CameraImage] is not directly usable as a UI resource. -/// -/// Although not all image formats are planar on iOS, we treat 1-dimensional -/// images as single planar images. -class CameraImage { - CameraImage._fromPlatformData(Map data) - : format = ImageFormat._fromPlatformData(data['format']), - height = data['height'], - width = data['width'], - planes = List.unmodifiable(data['planes'] - .map((dynamic planeData) => Plane._fromPlatformData(planeData))); - - /// Format of the image provided. - /// - /// Determines the number of planes needed to represent the image, and - /// the general layout of the pixel data in each [Uint8List]. - final ImageFormat format; - - /// Height of the image in pixels. - /// - /// For formats where some color channels are subsampled, this is the height - /// of the largest-resolution plane. - final int height; - - /// Width of the image in pixels. - /// - /// For formats where some color channels are subsampled, this is the width - /// of the largest-resolution plane. - final int width; - - /// The pixels planes for this image. - /// - /// The number of planes is determined by the format of the image. - final List planes; -} diff --git a/packages/camera/lib/new/camera.dart b/packages/camera/lib/new/camera.dart deleted file mode 100644 index 08b085f8e2c8..000000000000 --- a/packages/camera/lib/new/camera.dart +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -export 'src/camera_controller.dart'; -export 'src/camera_testing.dart'; -export 'src/common/camera_interface.dart'; -export 'src/common/native_texture.dart'; -export 'src/support_android/camera.dart'; -export 'src/support_android/camera_info.dart'; diff --git a/packages/camera/lib/new/src/camera_controller.dart b/packages/camera/lib/new/src/camera_controller.dart deleted file mode 100644 index 4296f39d7002..000000000000 --- a/packages/camera/lib/new/src/camera_controller.dart +++ /dev/null @@ -1,171 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; - -import 'common/camera_interface.dart'; - -/// Controls a device camera. -/// -/// Use [CameraController.availableCameras] to get a list of available cameras. -/// -/// This class is used as a simple interface to control a camera on Android or -/// iOS. -/// -/// Only one instance of [CameraController] can be active at a time. If you call -/// [initialize] on a [CameraController] while another is active, the old -/// controller will be disposed before initializing the new controller. -/// -/// Example using [CameraController]: -/// -/// ```dart -/// final List cameras = async CameraController.availableCameras(); -/// final CameraController controller = CameraController(description: cameras[0]); -/// controller.initialize(); -/// controller.start(); -/// ``` -class CameraController { - /// Default constructor. - /// - /// Use [CameraController.availableCameras] to get a list of available - /// cameras. - /// - /// This will choose the best [CameraConfigurator] for the current device. - factory CameraController({@required CameraDescription description}) { - return CameraController._( - description: description, - configurator: _createDefaultConfigurator(description), - api: _getCameraApi(description), - ); - } - - CameraController._({ - @required this.description, - @required this.configurator, - @required this.api, - }) : assert(description != null), - assert(configurator != null), - assert(api != null); - - /// Constructor for defining your own [CameraConfigurator]. - /// - /// Use [CameraController.availableCameras] to get a list of available - /// cameras. - factory CameraController.customConfigurator({ - @required CameraDescription description, - @required CameraConfigurator configurator, - }) { - return CameraController._( - description: description, - configurator: configurator, - api: _getCameraApi(description), - ); - } - - static const String _isNotInitializedMessage = 'Initialize was not called.'; - static const String _isDisposedMessage = 'This controller has been disposed.'; - - // Keep only one active instance of CameraController. - static CameraController _instance; - - bool _isDisposed = false; - - /// Details for the camera this controller accesses. - final CameraDescription description; - - /// Configurator used to control the camera. - final CameraConfigurator configurator; - - /// Api used by the [configurator]. - final CameraApi api; - - bool get isDisposed => _isDisposed; - - /// Retrieves a list of available cameras for the current device. - /// - /// This will choose the best [CameraAPI] for the current device. - static Future> availableCameras() async { - throw UnimplementedError('$defaultTargetPlatform not supported'); - } - - /// Initializes the camera on the device. - /// - /// You must call [dispose] when you are done using the camera, otherwise it - /// will remain locked and be unavailable to other applications. - /// - /// Only one instance of [CameraController] can be active at a time. If you - /// call [initialize] on a [CameraController] while another is active, the old - /// controller will be disposed before initializing the new controller. - Future initialize() { - if (_instance == this) { - return Future.value(); - } - - final Completer completer = Completer(); - - if (_instance != null) { - _instance - .dispose() - .then((_) => configurator.initialize()) - .then((_) => completer.complete()); - } - _instance = this; - - return completer.future; - } - - /// Begins the flow of data between the inputs and outputs connected to the camera instance. - Future start() { - assert(!_isDisposed, _isDisposedMessage); - assert(_instance != this, _isNotInitializedMessage); - - return configurator.start(); - } - - /// Stops the flow of data between the inputs and outputs connected to the camera instance. - Future stop() { - assert(!_isDisposed, _isDisposedMessage); - assert(_instance != this, _isNotInitializedMessage); - - return configurator.stop(); - } - - /// Deallocate all resources and disables further use of the controller. - Future dispose() { - _instance = null; - _isDisposed = true; - return configurator.dispose(); - } - - static CameraConfigurator _createDefaultConfigurator( - CameraDescription description, - ) { - final CameraApi api = _getCameraApi(description); - switch (api) { - case CameraApi.android: - throw UnimplementedError(); - case CameraApi.iOS: - throw UnimplementedError(); - case CameraApi.supportAndroid: - throw UnimplementedError(); - } - - return null; // Unreachable code - } - - static CameraApi _getCameraApi(CameraDescription description) { - return CameraApi.iOS; - - // TODO(bparrishMines): Uncomment this when platform specific code is added. - /* - throw ArgumentError.value( - description.runtimeType, - 'description.runtimeType', - 'Failed to get $CameraApi from', - ); - */ - } -} diff --git a/packages/camera/lib/new/src/camera_testing.dart b/packages/camera/lib/new/src/camera_testing.dart deleted file mode 100644 index 8022216ff8c8..000000000000 --- a/packages/camera/lib/new/src/camera_testing.dart +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; - -import 'common/camera_channel.dart'; - -@visibleForTesting -class CameraTesting { - CameraTesting._(); - - static final MethodChannel channel = CameraChannel.channel; - static int get nextHandle => CameraChannel.nextHandle; - static set nextHandle(int handle) => CameraChannel.nextHandle = handle; -} diff --git a/packages/camera/lib/new/src/common/camera_channel.dart b/packages/camera/lib/new/src/common/camera_channel.dart deleted file mode 100644 index 12036b85be74..000000000000 --- a/packages/camera/lib/new/src/common/camera_channel.dart +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/services.dart'; - -typedef CameraCallback = void Function(dynamic result); - -// Non exported class -class CameraChannel { - static final Map callbacks = {}; - - static final MethodChannel channel = const MethodChannel( - 'flutter.plugins.io/camera', - )..setMethodCallHandler( - (MethodCall call) async { - assert(call.method == 'handleCallback'); - - final int handle = call.arguments['handle']; - if (callbacks[handle] != null) callbacks[handle](call.arguments); - }, - ); - - static int nextHandle = 0; - - static void registerCallback(int handle, CameraCallback callback) { - assert(handle != null); - assert(CameraCallback != null); - - assert(!callbacks.containsKey(handle)); - callbacks[handle] = callback; - } - - static void unregisterCallback(int handle) { - assert(handle != null); - callbacks.remove(handle); - } -} diff --git a/packages/camera/lib/new/src/common/camera_interface.dart b/packages/camera/lib/new/src/common/camera_interface.dart deleted file mode 100644 index 99ead09550c9..000000000000 --- a/packages/camera/lib/new/src/common/camera_interface.dart +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -/// Available APIs compatible with [CameraController]. -enum CameraApi { - /// [Camera2](https://developer.android.com/reference/android/hardware/camera2/package-summary) - android, - - /// [AVFoundation](https://developer.apple.com/av-foundation/) - iOS, - - /// [Camera](https://developer.android.com/reference/android/hardware/Camera) - supportAndroid, -} - -/// Location of the camera on the device. -enum LensDirection { front, back, unknown } - -/// Abstract class used to create a common interface to describe a camera from different platform APIs. -/// -/// This provides information such as the [name] of the camera and [direction] -/// the lens face. -abstract class CameraDescription { - /// Location of the camera on the device. - LensDirection get direction; - - /// Identifier for this camera. - String get name; -} - -/// Abstract class used to create a common interface across platform APIs. -abstract class CameraConfigurator { - /// Texture id that can be used to send camera frames to a [Texture] widget. - /// - /// You must call [addPreviewTexture] first or this will only return null. - int get previewTextureId; - - /// Initializes the camera on the device. - Future initialize(); - - /// Begins the flow of data between the inputs and outputs connected to the camera instance. - /// - /// This will start updating the texture with id: [previewTextureId]. - Future start(); - - /// Stops the flow of data between the inputs and outputs connected to the camera instance. - Future stop(); - - /// Dispose all resources and disables further use of this configurator. - Future dispose(); - - /// Retrieves a valid texture Id to be used with a [Texture] widget. - Future addPreviewTexture(); -} diff --git a/packages/camera/lib/new/src/common/camera_mixins.dart b/packages/camera/lib/new/src/common/camera_mixins.dart deleted file mode 100644 index bb27e4881d1f..000000000000 --- a/packages/camera/lib/new/src/common/camera_mixins.dart +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'camera_channel.dart'; - -mixin NativeMethodCallHandler { - /// Identifier for an object on the native side of the plugin. - /// - /// Only used internally and for debugging. - final int handle = CameraChannel.nextHandle++; -} - -mixin CameraMappable { - /// Creates a description of the object compatible with [PlatformChannel]s. - /// - /// Only used as an internal method and for debugging. - Map asMap(); -} diff --git a/packages/camera/lib/new/src/common/native_texture.dart b/packages/camera/lib/new/src/common/native_texture.dart deleted file mode 100644 index 1deb7e3a10b6..000000000000 --- a/packages/camera/lib/new/src/common/native_texture.dart +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; - -import 'camera_channel.dart'; -import 'camera_mixins.dart'; - -/// Used to allocate a buffer for displaying a preview camera texture. -/// -/// This is used to for a developer to have a control over the -/// `TextureRegistry.SurfaceTextureEntry` (Android) and FlutterTexture (iOS). -/// This gives direct access to the textureId and can be reused with separate -/// camera instances. -/// -/// The [textureId] can be passed to a [Texture] widget. -class NativeTexture with CameraMappable { - NativeTexture._({@required int handle, @required this.textureId}) - : _handle = handle, - assert(handle != null), - assert(textureId != null); - - final int _handle; - - bool _isClosed = false; - - /// Id that can be passed to a [Texture] widget. - final int textureId; - - static Future allocate() async { - final int handle = CameraChannel.nextHandle++; - - final int textureId = await CameraChannel.channel.invokeMethod( - '$NativeTexture#allocate', - {'textureHandle': handle}, - ); - - return NativeTexture._(handle: handle, textureId: textureId); - } - - /// Deallocate this texture. - Future release() { - if (_isClosed) return Future.value(); - - _isClosed = true; - return CameraChannel.channel.invokeMethod( - '$NativeTexture#release', - {'handle': _handle}, - ); - } - - @override - Map asMap() { - return {'handle': _handle}; - } -} diff --git a/packages/camera/lib/new/src/support_android/camera.dart b/packages/camera/lib/new/src/support_android/camera.dart deleted file mode 100644 index d78753d24355..000000000000 --- a/packages/camera/lib/new/src/support_android/camera.dart +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import '../common/camera_channel.dart'; -import '../common/camera_mixins.dart'; -import '../common/native_texture.dart'; -import 'camera_info.dart'; - -/// The Camera class used to set image capture settings, start/stop preview, snap pictures, and retrieve frames for encoding for video. -/// -/// This class is a client for the Camera service, which manages the actual -/// camera hardware. -/// -/// This exposes the deprecated Android -/// [Camera](https://developer.android.com/reference/android/hardware/Camera) -/// API. This should only be used with Android sdk versions less than 21. -class Camera with NativeMethodCallHandler { - Camera._(); - - bool _isClosed = false; - - /// Retrieves the number of physical cameras available on this device. - static Future getNumberOfCameras() { - return CameraChannel.channel.invokeMethod( - 'Camera#getNumberOfCameras', - ); - } - - /// Creates a new [Camera] object to access a particular hardware camera. - /// - /// If the same camera is opened by other applications, this will throw a - /// [PlatformException]. - /// - /// You must call [release] when you are done using the camera, otherwise it - /// will remain locked and be unavailable to other applications. - /// - /// Your application should only have one [Camera] object active at a time for - /// a particular hardware camera. - static Camera open(int cameraId) { - final Camera camera = Camera._(); - - CameraChannel.channel.invokeMethod( - 'Camera#open', - {'cameraId': cameraId, 'cameraHandle': camera.handle}, - ); - - return camera; - } - - /// Retrieves information about a particular camera. - /// - /// If [getNumberOfCameras] returns N, the valid id is 0 to N-1. - static Future getCameraInfo(int cameraId) async { - final Map infoMap = - await CameraChannel.channel.invokeMapMethod( - 'Camera#getCameraInfo', - {'cameraId': cameraId}, - ); - - return CameraInfo.fromMap(infoMap); - } - - /// Sets the [NativeTexture] to be used for live preview. - /// - /// This method must be called before [startPreview]. - /// - /// The one exception is that if the preview native texture is not set (or - /// set to null) before [startPreview] is called, then this method may be - /// called once with a non-null parameter to set the preview texture. - /// (This allows camera setup and surface creation to happen in parallel, - /// saving time.) The preview native texture may not otherwise change while - /// preview is running. - set previewTexture(NativeTexture texture) { - assert(!_isClosed); - - CameraChannel.channel.invokeMethod( - 'Camera#previewTexture', - {'handle': handle, 'nativeTexture': texture?.asMap()}, - ); - } - - /// Starts capturing and drawing preview frames to the screen. - /// - /// Preview will not actually start until a surface is supplied with - /// [previewTexture]. - Future startPreview() { - assert(!_isClosed); - - return CameraChannel.channel.invokeMethod( - 'Camera#startPreview', - {'handle': handle}, - ); - } - - /// Stops capturing and drawing preview frames to the [previewTexture], and resets the camera for a future call to [startPreview]. - Future stopPreview() { - assert(!_isClosed); - - return CameraChannel.channel.invokeMethod( - 'Camera#stopPreview', - {'handle': handle}, - ); - } - - /// Disconnects and releases the Camera object resources. - /// - /// You must call this as soon as you're done with the Camera object. - Future release() { - if (_isClosed) return Future.value(); - - _isClosed = true; - return CameraChannel.channel.invokeMethod( - 'Camera#release', - {'handle': handle}, - ); - } -} diff --git a/packages/camera/lib/new/src/support_android/camera_info.dart b/packages/camera/lib/new/src/support_android/camera_info.dart deleted file mode 100644 index 033fecfea6d9..000000000000 --- a/packages/camera/lib/new/src/support_android/camera_info.dart +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/foundation.dart'; - -import '../common/camera_interface.dart'; - -/// The direction that the camera faces. -enum Facing { back, front } - -/// Information about a camera. -/// -/// Retrieved from [Camera.getCameraInfo]. -class CameraInfo implements CameraDescription { - const CameraInfo({ - @required this.id, - @required this.facing, - @required this.orientation, - }) : assert(id != null), - assert(facing != null), - assert(orientation != null); - - factory CameraInfo.fromMap(Map map) { - return CameraInfo( - id: map['id'], - orientation: map['orientation'], - facing: Facing.values.firstWhere( - (Facing facing) => facing.toString() == map['facing'], - ), - ); - } - - /// Identifier for a particular camera. - final int id; - - /// The direction that the camera faces. - final Facing facing; - - /// The orientation of the camera image. - /// - /// The value is the angle that the camera image needs to be rotated clockwise - /// so it shows correctly on the display in its natural orientation. - /// It should be 0, 90, 180, or 270. - /// - /// For example, suppose a device has a naturally tall screen. The back-facing - /// camera sensor is mounted in landscape. You are looking at the screen. If - /// the top side of the camera sensor is aligned with the right edge of the - /// screen in natural orientation, the value should be 90. If the top side of - /// a front-facing camera sensor is aligned with the right of the screen, the - /// value should be 270. - final int orientation; - - @override - String get name => id.toString(); - - @override - LensDirection get direction { - switch (facing) { - case Facing.front: - return LensDirection.front; - case Facing.back: - return LensDirection.back; - } - - return null; - } -} diff --git a/packages/camera/pubspec.yaml b/packages/camera/pubspec.yaml deleted file mode 100644 index cbe1b1b6d9dd..000000000000 --- a/packages/camera/pubspec.yaml +++ /dev/null @@ -1,33 +0,0 @@ -name: camera -description: A Flutter plugin for getting information about and controlling the - camera on Android and iOS. Supports previewing the camera feed, capturing images, capturing video, - and streaming image buffers to dart. -version: 0.5.8+7 - -homepage: https://github.com/flutter/plugins/tree/master/packages/camera - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - path_provider: ^0.5.0 - video_player: ^0.10.0 - flutter_test: - sdk: flutter - flutter_driver: - sdk: flutter - pedantic: ^1.8.0 - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.camera - pluginClass: CameraPlugin - ios: - pluginClass: CameraPlugin - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" diff --git a/packages/camera/test/camera_test.dart b/packages/camera/test/camera_test.dart deleted file mode 100644 index fbb955689e48..000000000000 --- a/packages/camera/test/camera_test.dart +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:camera/new/camera.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:camera/new/src/camera_testing.dart'; -import 'package:camera/new/src/common/native_texture.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('Camera', () { - final List log = []; - - setUpAll(() { - CameraTesting.channel - .setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - switch (methodCall.method) { - case 'NativeTexture#allocate': - return 15; - } - - throw ArgumentError.value( - methodCall.method, - 'methodCall.method', - 'No method found for', - ); - }); - }); - - setUp(() { - log.clear(); - CameraTesting.nextHandle = 0; - }); - - group('$CameraController', () { - test('Initializing a second controller closes the first', () { - final MockCameraDescription description = MockCameraDescription(); - final MockCameraConfigurator configurator = MockCameraConfigurator(); - - final CameraController controller1 = - CameraController.customConfigurator( - description: description, - configurator: configurator, - ); - - controller1.initialize(); - - final CameraController controller2 = - CameraController.customConfigurator( - description: description, - configurator: configurator, - ); - - controller2.initialize(); - - expect( - () => controller1.start(), - throwsA(isInstanceOf()), - ); - - expect( - () => controller1.stop(), - throwsA(isInstanceOf()), - ); - - expect(controller1.isDisposed, isTrue); - }); - }); - - group('$NativeTexture', () { - test('allocate', () async { - final NativeTexture texture = await NativeTexture.allocate(); - - expect(texture.textureId, 15); - expect(log, [ - isMethodCall( - '$NativeTexture#allocate', - arguments: {'textureHandle': 0}, - ) - ]); - }); - }); - }); -} - -class MockCameraDescription extends CameraDescription { - @override - LensDirection get direction => LensDirection.unknown; - - @override - String get name => 'none'; -} - -class MockCameraConfigurator extends CameraConfigurator { - @override - Future addPreviewTexture() => Future.value(7); - - @override - Future dispose() => Future.value(); - - @override - Future initialize() => Future.value(); - - @override - int get previewTextureId => 7; - - @override - Future start() => Future.value(); - - @override - Future stop() => Future.value(); -} diff --git a/packages/camera/test/support_android/support_android_test.dart b/packages/camera/test/support_android/support_android_test.dart deleted file mode 100644 index 399acd3698ce..000000000000 --- a/packages/camera/test/support_android/support_android_test.dart +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:camera/new/src/support_android/camera_info.dart'; -import 'package:camera/new/src/support_android/camera.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:camera/new/src/camera_testing.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('Support Android Camera', () { - group('$Camera', () { - final List log = []; - setUpAll(() { - CameraTesting.channel - .setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - switch (methodCall.method) { - case 'Camera#getNumberOfCameras': - return 3; - case 'Camera#open': - return null; - case 'Camera#getCameraInfo': - return { - 'id': 3, - 'orientation': 90, - 'facing': Facing.front.toString(), - }; - case 'Camera#startPreview': - return null; - case 'Camera#stopPreview': - return null; - case 'Camera#release': - return null; - } - - throw ArgumentError.value( - methodCall.method, - 'methodCall.method', - 'No method found for', - ); - }); - }); - - setUp(() { - log.clear(); - CameraTesting.nextHandle = 0; - }); - - test('getNumberOfCameras', () async { - final int result = await Camera.getNumberOfCameras(); - - expect(result, 3); - expect(log, [ - isMethodCall( - '$Camera#getNumberOfCameras', - arguments: null, - ) - ]); - }); - - test('open', () { - Camera.open(14); - - expect(log, [ - isMethodCall( - '$Camera#open', - arguments: { - 'cameraId': 14, - 'cameraHandle': 0, - }, - ) - ]); - }); - - test('getCameraInfo', () async { - final CameraInfo info = await Camera.getCameraInfo(14); - - expect(info.id, 3); - expect(info.orientation, 90); - expect(info.facing, Facing.front); - - expect(log, [ - isMethodCall( - '$Camera#getCameraInfo', - arguments: {'cameraId': 14}, - ) - ]); - }); - - test('startPreview', () { - final Camera camera = Camera.open(0); - - log.clear(); - camera.startPreview(); - - expect(log, [ - isMethodCall( - '$Camera#startPreview', - arguments: { - 'handle': 0, - }, - ) - ]); - }); - - test('stopPreview', () { - final Camera camera = Camera.open(0); - - log.clear(); - camera.stopPreview(); - - expect(log, [ - isMethodCall( - '$Camera#stopPreview', - arguments: { - 'handle': 0, - }, - ) - ]); - }); - - test('release', () { - final Camera camera = Camera.open(0); - - log.clear(); - camera.release(); - - expect(log, [ - isMethodCall( - '$Camera#release', - arguments: { - 'handle': 0, - }, - ) - ]); - }); - }); - }); -} diff --git a/packages/connectivity/analysis_options.yaml b/packages/connectivity/analysis_options.yaml new file mode 100644 index 000000000000..cda4f6e153e6 --- /dev/null +++ b/packages/connectivity/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options_legacy.yaml diff --git a/packages/connectivity/connectivity/AUTHORS b/packages/connectivity/connectivity/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/connectivity/connectivity/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/connectivity/connectivity/CHANGELOG.md b/packages/connectivity/connectivity/CHANGELOG.md index a42eaf0a359c..89db7aeba9bb 100644 --- a/packages/connectivity/connectivity/CHANGELOG.md +++ b/packages/connectivity/connectivity/CHANGELOG.md @@ -1,3 +1,70 @@ +## 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. diff --git a/packages/connectivity/connectivity/LICENSE b/packages/connectivity/connectivity/LICENSE index a6d6c0749818..c6823b81eb84 100644 --- a/packages/connectivity/connectivity/LICENSE +++ b/packages/connectivity/connectivity/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017 The Chromium Authors. All rights reserved. +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: diff --git a/packages/connectivity/connectivity/README.md b/packages/connectivity/connectivity/README.md index 0a13749b9dc5..d085c18ba1e4 100644 --- a/packages/connectivity/connectivity/README.md +++ b/packages/connectivity/connectivity/README.md @@ -1,5 +1,20 @@ # 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. @@ -7,13 +22,6 @@ 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. -**Please set your constraint to `connectivity: '>=0.4.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.4.y+z`. -Please use `connectivity: '>=0.4.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ## Usage Sample usage to check current status: @@ -59,61 +67,9 @@ dispose() { 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. -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:connectivity/connectivity.dart'; - -var wifiBSSID = await (Connectivity().getWifiBSSID()); -var wifiIP = await (Connectivity().getWifiIP());network -var wifiName = await (Connectivity().getWifiName());wifi network -``` - -### 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/). +[documentation](https://flutter.dev/). -For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code). +For help on editing plugin code, view the [documentation](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin). diff --git a/packages/connectivity/connectivity/android/build.gradle b/packages/connectivity/connectivity/android/build.gradle index 3542dbc6c01d..afd7d9f0a977 100644 --- a/packages/connectivity/connectivity/android/build.gradle +++ b/packages/connectivity/connectivity/android/build.gradle @@ -5,18 +5,18 @@ def args = ["-Xlint:deprecation","-Xlint:unchecked","-Werror"] buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:3.5.0' } } rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } @@ -27,7 +27,7 @@ project.getTasks().withType(JavaCompile){ apply plugin: 'com.android.library' android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { minSdkVersion 16 diff --git a/packages/connectivity/connectivity/android/src/main/AndroidManifest.xml b/packages/connectivity/connectivity/android/src/main/AndroidManifest.xml index f4eafe489d0c..52bbe9edafa0 100644 --- a/packages/connectivity/connectivity/android/src/main/AndroidManifest.xml +++ b/packages/connectivity/connectivity/android/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - 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 index dbf8e25eb7ad..d7e254e84595 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -7,19 +7,14 @@ import android.net.ConnectivityManager; import android.net.Network; import android.net.NetworkCapabilities; -import android.net.NetworkInfo; -import android.net.wifi.WifiInfo; -import android.net.wifi.WifiManager; import android.os.Build; /** Reports connectivity related information such as connectivity type and wifi information. */ -class Connectivity { +public class Connectivity { private ConnectivityManager connectivityManager; - private WifiManager wifiManager; - Connectivity(ConnectivityManager connectivityManager, WifiManager wifiManager) { + public Connectivity(ConnectivityManager connectivityManager) { this.connectivityManager = connectivityManager; - this.wifiManager = wifiManager; } String getNetworkType() { @@ -41,48 +36,10 @@ String getNetworkType() { return getNetworkTypeLegacy(); } - String getWifiName() { - WifiInfo wifiInfo = getWifiInfo(); - String ssid = null; - if (wifiInfo != null) ssid = wifiInfo.getSSID(); - if (ssid != null) ssid = ssid.replaceAll("\"", ""); // Android returns "SSID" - return ssid; - } - - String getWifiBSSID() { - 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(); - } - @SuppressWarnings("deprecation") private String getNetworkTypeLegacy() { // handle type for Android versions less than Android 9 - NetworkInfo info = connectivityManager.getActiveNetworkInfo(); + android.net.NetworkInfo info = connectivityManager.getActiveNetworkInfo(); if (info == null || !info.isConnected()) { return "none"; } diff --git a/packages/connectivity/connectivity/android/src/main/java/io/flutter/plugins/connectivity/ConnectivityBroadcastReceiver.java b/packages/connectivity/connectivity/android/src/main/java/io/flutter/plugins/connectivity/ConnectivityBroadcastReceiver.java index 0c6554c0a5f7..fbda187bd188 100644 --- a/packages/connectivity/connectivity/android/src/main/java/io/flutter/plugins/connectivity/ConnectivityBroadcastReceiver.java +++ b/packages/connectivity/connectivity/android/src/main/java/io/flutter/plugins/connectivity/ConnectivityBroadcastReceiver.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -13,7 +13,6 @@ import android.os.Build; import android.os.Handler; import android.os.Looper; -import androidx.annotation.RequiresApi; import io.flutter.plugin.common.EventChannel; /** @@ -24,15 +23,16 @@ * io.flutter.plugin.common.EventChannel#setStreamHandler(io.flutter.plugin.common.EventChannel.StreamHandler)} * to set up the receiver. */ -class ConnectivityBroadcastReceiver extends BroadcastReceiver +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"; - ConnectivityBroadcastReceiver(Context context, Connectivity connectivity) { + public ConnectivityBroadcastReceiver(Context context, Connectivity connectivity) { this.context = context; this.connectivity = connectivity; } @@ -41,7 +41,19 @@ class ConnectivityBroadcastReceiver extends BroadcastReceiver public void onListen(Object arguments, EventChannel.EventSink events) { this.events = events; if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - connectivity.getConnectivityManager().registerDefaultNetworkCallback(getNetworkCallback()); + 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)); } @@ -50,7 +62,10 @@ public void onListen(Object arguments, EventChannel.EventSink events) { @Override public void onCancel(Object arguments) { if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - connectivity.getConnectivityManager().unregisterNetworkCallback(getNetworkCallback()); + if (networkCallback != null) { + connectivity.getConnectivityManager().unregisterNetworkCallback(networkCallback); + networkCallback = null; + } } else { context.unregisterReceiver(this); } @@ -63,19 +78,8 @@ public void onReceive(Context context, Intent intent) { } } - @RequiresApi(api = Build.VERSION_CODES.N) - ConnectivityManager.NetworkCallback getNetworkCallback() { - return new ConnectivityManager.NetworkCallback() { - @Override - public void onAvailable(Network network) { - sendEvent(); - } - - @Override - public void onLost(Network network) { - sendEvent(); - } - }; + public ConnectivityManager.NetworkCallback getNetworkCallback() { + return networkCallback; } private void sendEvent() { 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 index 931b702d442a..06275498c4a9 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -31,15 +31,6 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { case "check": result.success(connectivity.getNetworkType()); break; - case "wifiName": - result.success(connectivity.getWifiName()); - break; - case "wifiBSSID": - result.success(connectivity.getWifiBSSID()); - break; - case "wifiIPAddress": - result.success(connectivity.getWifiIPAddress()); - 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 index f036a8aa15ae..2287a0a30b86 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -6,7 +6,6 @@ import android.content.Context; import android.net.ConnectivityManager; -import android.net.wifi.WifiManager; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.EventChannel; @@ -41,10 +40,8 @@ private void setupChannels(BinaryMessenger messenger, Context context) { eventChannel = new EventChannel(messenger, "plugins.flutter.io/connectivity_status"); ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - WifiManager wifiManager = - (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); - Connectivity connectivity = new Connectivity(connectivityManager, wifiManager); + Connectivity connectivity = new Connectivity(connectivityManager); ConnectivityMethodChannelHandler methodChannelHandler = new ConnectivityMethodChannelHandler(connectivity); diff --git a/packages/connectivity/connectivity/example/android/app/build.gradle b/packages/connectivity/connectivity/example/android/app/build.gradle index 5d1f138bfe1a..64f3d0626bf4 100644 --- a/packages/connectivity/connectivity/example/android/app/build.gradle +++ b/packages/connectivity/connectivity/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 28 + compileSdkVersion 29 lintOptions { disable 'InvalidPackage' @@ -34,7 +34,7 @@ android { defaultConfig { applicationId "io.flutter.plugins.connectivityexample" minSdkVersion 16 - targetSdkVersion 28 + targetSdkVersion 29 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -55,4 +55,6 @@ 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/java/io/flutter/plugins/connectivityexample/EmbeddingV1Activity.java b/packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/EmbeddingV1Activity.java index 591f1ec604d7..8329fa29e431 100644 --- a/packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/EmbeddingV1Activity.java +++ b/packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/EmbeddingV1Activity.java @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/EmbeddingV1ActivityTest.java b/packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/EmbeddingV1ActivityTest.java index 2adc0c268bde..e2bd59408d4b 100644 --- a/packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/EmbeddingV1ActivityTest.java +++ b/packages/connectivity/connectivity/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/EmbeddingV1ActivityTest.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/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 index 0f0dcf2555f3..330f0050a1d8 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/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 new file mode 100644 index 000000000000..2cf03dd3c2f5 --- /dev/null +++ b/packages/connectivity/connectivity/example/android/app/src/test/java/io/flutter/plugins/connectivityexample/ActivityTest.java @@ -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. + +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/build.gradle b/packages/connectivity/connectivity/example/android/build.gradle index 541636cc492a..456d020f6e2c 100644 --- a/packages/connectivity/connectivity/example/android/build.gradle +++ b/packages/connectivity/connectivity/example/android/build.gradle @@ -1,18 +1,18 @@ buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:3.5.0' } } allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/packages/connectivity/connectivity/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/connectivity/connectivity/example/android/gradle/wrapper/gradle-wrapper.properties index 019065d1d650..01a286e96a21 100644 --- a/packages/connectivity/connectivity/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/connectivity/connectivity/example/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip diff --git a/packages/connectivity/connectivity/example/integration_test/connectivity_test.dart b/packages/connectivity/connectivity/example/integration_test/connectivity_test.dart new file mode 100644 index 000000000000..ab6e71e23bb6 --- /dev/null +++ b/packages/connectivity/connectivity/example/integration_test/connectivity_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: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/Podfile b/packages/connectivity/connectivity/example/ios/Podfile new file mode 100644 index 000000000000..07a4e08abf54 --- /dev/null +++ b/packages/connectivity/connectivity/example/ios/Podfile @@ -0,0 +1,44 @@ +# 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 index e497d093be56..b69d2cc24a6f 100644 --- a/packages/connectivity/connectivity/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/connectivity/connectivity/example/ios/Runner.xcodeproj/project.pbxproj @@ -177,7 +177,7 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; + ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; @@ -437,7 +437,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.connectivityExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.connectivityExample; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -458,7 +458,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.connectivityExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.connectivityExample; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; diff --git a/packages/connectivity/connectivity/example/ios/Runner/AppDelegate.h b/packages/connectivity/connectivity/example/ios/Runner/AppDelegate.h index d9e18e990f2e..0681d288bb70 100644 --- a/packages/connectivity/connectivity/example/ios/Runner/AppDelegate.h +++ b/packages/connectivity/connectivity/example/ios/Runner/AppDelegate.h @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/connectivity/connectivity/example/ios/Runner/AppDelegate.m b/packages/connectivity/connectivity/example/ios/Runner/AppDelegate.m index f08675707182..30b87969f44a 100644 --- a/packages/connectivity/connectivity/example/ios/Runner/AppDelegate.m +++ b/packages/connectivity/connectivity/example/ios/Runner/AppDelegate.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/connectivity/connectivity/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/connectivity/connectivity/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index d22f10b2ab63..8122b0a0c2f2 100644 --- 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 @@ -1,116 +1,121 @@ { "images" : [ { - "size" : "20x20", - "idiom" : "iphone", "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" }, { - "size" : "20x20", - "idiom" : "iphone", "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" }, { - "size" : "29x29", - "idiom" : "iphone", "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" + "idiom" : "iphone", + "scale" : "1x", + "size" : "29x29" }, { - "size" : "29x29", - "idiom" : "iphone", "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" }, { - "size" : "29x29", - "idiom" : "iphone", "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" }, { - "size" : "40x40", - "idiom" : "iphone", "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" }, { - "size" : "40x40", - "idiom" : "iphone", "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" }, { - "size" : "60x60", - "idiom" : "iphone", "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" }, { - "size" : "60x60", - "idiom" : "iphone", "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" }, { - "size" : "20x20", - "idiom" : "ipad", "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" }, { - "size" : "20x20", - "idiom" : "ipad", "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" }, { - "size" : "29x29", - "idiom" : "ipad", "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" }, { - "size" : "29x29", - "idiom" : "ipad", "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" }, { - "size" : "40x40", - "idiom" : "ipad", "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" }, { - "size" : "40x40", - "idiom" : "ipad", "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" }, { - "size" : "76x76", - "idiom" : "ipad", "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" }, { - "size" : "76x76", - "idiom" : "ipad", "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" }, { - "size" : "83.5x83.5", - "idiom" : "ipad", "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } } diff --git a/packages/connectivity/connectivity/example/ios/Runner/Info.plist b/packages/connectivity/connectivity/example/ios/Runner/Info.plist index babbd80f1619..d76382b40acf 100644 --- a/packages/connectivity/connectivity/example/ios/Runner/Info.plist +++ b/packages/connectivity/connectivity/example/ios/Runner/Info.plist @@ -22,10 +22,6 @@ 1 LSRequiresIPhoneOS - NSLocationAlwaysAndWhenInUseUsageDescription - This app requires accessing your location information all the time to get wi-fi information. - NSLocationWhenInUseUsageDescription - This app requires accessing your location information when the app is in foreground to get wi-fi information. UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/packages/connectivity/connectivity/example/ios/Runner/main.m b/packages/connectivity/connectivity/example/ios/Runner/main.m index bec320c0bee0..f97b9ef5c8a1 100644 --- a/packages/connectivity/connectivity/example/ios/Runner/main.m +++ b/packages/connectivity/connectivity/example/ios/Runner/main.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/connectivity/connectivity/example/lib/main.dart b/packages/connectivity/connectivity/example/lib/main.dart index f8486165fa70..b6a6882cb12e 100644 --- a/packages/connectivity/connectivity/example/lib/main.dart +++ b/packages/connectivity/connectivity/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -40,7 +40,7 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); + MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @@ -51,7 +51,7 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { String _connectionStatus = 'Unknown'; final Connectivity _connectivity = Connectivity(); - StreamSubscription _connectivitySubscription; + late StreamSubscription _connectivitySubscription; @override void initState() { @@ -69,7 +69,7 @@ class _MyHomePageState extends State { // Platform messages are asynchronous, so we initialize in an async method. Future initConnectivity() async { - ConnectivityResult result; + ConnectivityResult result = ConnectivityResult.none; // Platform messages may fail, so we use a try/catch PlatformException. try { result = await _connectivity.checkConnectivity(); @@ -100,66 +100,6 @@ class _MyHomePageState extends State { Future _updateConnectionStatus(ConnectivityResult result) async { switch (result) { case ConnectivityResult.wifi: - String wifiName, wifiBSSID, wifiIP; - - try { - if (!kIsWeb && 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 (!kIsWeb && 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()); diff --git a/packages/connectivity/connectivity/example/macos/Podfile b/packages/connectivity/connectivity/example/macos/Podfile new file mode 100644 index 000000000000..dade8dfad0dc --- /dev/null +++ b/packages/connectivity/connectivity/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.11' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/connectivity/connectivity/example/macos/Runner/AppDelegate.swift b/packages/connectivity/connectivity/example/macos/Runner/AppDelegate.swift index d53ef6437726..5cec4c48f620 100644 --- a/packages/connectivity/connectivity/example/macos/Runner/AppDelegate.swift +++ b/packages/connectivity/connectivity/example/macos/Runner/AppDelegate.swift @@ -1,3 +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 Cocoa import FlutterMacOS diff --git a/packages/connectivity/connectivity/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/connectivity/connectivity/example/macos/Runner/Configs/AppInfo.xcconfig index a95148814518..1a9e76c10a78 100644 --- a/packages/connectivity/connectivity/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/packages/connectivity/connectivity/example/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = connectivity_example // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.connectivityExample +PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.connectivityExample // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2019 io.flutter.plugins. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2019 The Flutter Authors.flutter.plugins. All rights reserved. diff --git a/packages/connectivity/connectivity/example/macos/Runner/MainFlutterWindow.swift b/packages/connectivity/connectivity/example/macos/Runner/MainFlutterWindow.swift index 2722837ec918..32aaeedceb1f 100644 --- a/packages/connectivity/connectivity/example/macos/Runner/MainFlutterWindow.swift +++ b/packages/connectivity/connectivity/example/macos/Runner/MainFlutterWindow.swift @@ -1,3 +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 Cocoa import FlutterMacOS diff --git a/packages/connectivity/connectivity/example/pubspec.yaml b/packages/connectivity/connectivity/example/pubspec.yaml index 1d07f7d19e60..1707d3482a98 100644 --- a/packages/connectivity/connectivity/example/pubspec.yaml +++ b/packages/connectivity/connectivity/example/pubspec.yaml @@ -1,19 +1,29 @@ 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: any + test: ^1.16.3 integration_test: - path: ../../../integration_test - pedantic: ^1.8.0 + sdk: flutter + pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/connectivity/connectivity/example/test_driver/integration_test.dart b/packages/connectivity/connectivity/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/connectivity/connectivity/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/test_driver/integration_test/connectivity_test.dart b/packages/connectivity/connectivity/example/test_driver/integration_test/connectivity_test.dart deleted file mode 100644 index 54a67337285a..000000000000 --- a/packages/connectivity/connectivity/example/test_driver/integration_test/connectivity_test.dart +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io'; -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', () { - Connectivity _connectivity; - - setUpAll(() async { - _connectivity = Connectivity(); - }); - - 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/example/test_driver/test/integration_test.dart b/packages/connectivity/connectivity/example/test_driver/test/integration_test.dart deleted file mode 100644 index c0cbdcab2fb6..000000000000 --- a/packages/connectivity/connectivity/example/test_driver/test/integration_test.dart +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert'; -import 'dart:io'; -import 'package:flutter_driver/flutter_driver.dart'; - -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String data = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - await driver.close(); - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} diff --git a/packages/connectivity/connectivity/example/web/index.html b/packages/connectivity/connectivity/example/web/index.html index 9b7a438f823a..c6fa1623be95 100644 --- a/packages/connectivity/connectivity/example/web/index.html +++ b/packages/connectivity/connectivity/example/web/index.html @@ -1,4 +1,7 @@ + diff --git a/packages/connectivity/connectivity/integration_test/connectivity_test.dart b/packages/connectivity/connectivity/integration_test/connectivity_test.dart deleted file mode 100644 index 54a67337285a..000000000000 --- a/packages/connectivity/connectivity/integration_test/connectivity_test.dart +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io'; -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', () { - Connectivity _connectivity; - - setUpAll(() async { - _connectivity = Connectivity(); - }); - - 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/ios/Classes/FLTConnectivityLocationHandler.h b/packages/connectivity/connectivity/ios/Classes/FLTConnectivityLocationHandler.h deleted file mode 100644 index 1731d56fe782..000000000000 --- a/packages/connectivity/connectivity/ios/Classes/FLTConnectivityLocationHandler.h +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. -#import -#import - -NS_ASSUME_NONNULL_BEGIN - -@class FLTConnectivityLocationDelegate; - -typedef void (^FLTConnectivityLocationCompletion)(CLAuthorizationStatus); - -@interface FLTConnectivityLocationHandler : NSObject - -+ (CLAuthorizationStatus)locationAuthorizationStatus; - -- (void)requestLocationAuthorization:(BOOL)always - completion:(_Nonnull FLTConnectivityLocationCompletion)completionHnadler; - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/connectivity/connectivity/ios/Classes/FLTConnectivityLocationHandler.m b/packages/connectivity/connectivity/ios/Classes/FLTConnectivityLocationHandler.m deleted file mode 100644 index bbb93aea6a5b..000000000000 --- a/packages/connectivity/connectivity/ios/Classes/FLTConnectivityLocationHandler.m +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "FLTConnectivityLocationHandler.h" - -@interface FLTConnectivityLocationHandler () - -@property(copy, nonatomic) FLTConnectivityLocationCompletion completion; -@property(strong, nonatomic) CLLocationManager *locationManager; - -@end - -@implementation FLTConnectivityLocationHandler - -+ (CLAuthorizationStatus)locationAuthorizationStatus { - return CLLocationManager.authorizationStatus; -} - -- (void)requestLocationAuthorization:(BOOL)always - completion:(FLTConnectivityLocationCompletion)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/connectivity/connectivity/ios/Classes/FLTConnectivityPlugin.h b/packages/connectivity/connectivity/ios/Classes/FLTConnectivityPlugin.h index 5014624f2f69..aec76adc0b58 100644 --- a/packages/connectivity/connectivity/ios/Classes/FLTConnectivityPlugin.h +++ b/packages/connectivity/connectivity/ios/Classes/FLTConnectivityPlugin.h @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/connectivity/connectivity/ios/Classes/FLTConnectivityPlugin.m b/packages/connectivity/connectivity/ios/Classes/FLTConnectivityPlugin.m index 526bee25d561..ac37c2359137 100644 --- a/packages/connectivity/connectivity/ios/Classes/FLTConnectivityPlugin.m +++ b/packages/connectivity/connectivity/ios/Classes/FLTConnectivityPlugin.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -7,7 +7,6 @@ #import "Reachability/Reachability.h" #import -#import "FLTConnectivityLocationHandler.h" #import "SystemConfiguration/CaptiveNetwork.h" #include @@ -16,8 +15,6 @@ @interface FLTConnectivityPlugin () -@property(strong, nonatomic) FLTConnectivityLocationHandler* locationHandler; - @end @implementation FLTConnectivityPlugin { @@ -39,58 +36,6 @@ + (void)registerWithRegistrar:(NSObject*)registrar { [streamChannel setStreamHandler:instance]; } -- (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; -} - - (NSString*)statusFromReachability:(Reachability*)reachability { NetworkStatus status = [reachability currentReachabilityStatus]; switch (status) { @@ -111,24 +56,6 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { // and the code // gets more involved. So for now, this will do. result([self statusFromReachability:[Reachability reachabilityForInternetConnection]]); - } else 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:[FLTConnectivityLocationHandler - 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); } @@ -139,34 +66,6 @@ - (void)onReachabilityDidChange:(NSNotification*)notification { _eventSink([self statusFromReachability:curReach]); } -- (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"; } - } -} - -- (FLTConnectivityLocationHandler*)locationHandler { - if (!_locationHandler) { - _locationHandler = [FLTConnectivityLocationHandler new]; - } - return _locationHandler; -} - #pragma mark FlutterStreamHandler impl - (FlutterError*)onListenWithArguments:(id)arguments eventSink:(FlutterEventSink)eventSink { diff --git a/packages/connectivity/connectivity/lib/connectivity.dart b/packages/connectivity/connectivity/lib/connectivity.dart index a5d9f25089cf..1b819d7470d2 100644 --- a/packages/connectivity/connectivity/lib/connectivity.dart +++ b/packages/connectivity/connectivity/lib/connectivity.dart @@ -1,10 +1,9 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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:connectivity_platform_interface/connectivity_platform_interface.dart'; // Export enums from the platform_interface so plugin users can use them directly. @@ -23,12 +22,12 @@ class Connectivity { if (_singleton == null) { _singleton = Connectivity._(); } - return _singleton; + return _singleton!; } Connectivity._(); - static Connectivity _singleton; + static Connectivity? _singleton; static ConnectivityPlatform get _platform => ConnectivityPlatform.instance; @@ -46,125 +45,4 @@ class Connectivity { Future checkConnectivity() { return _platform.checkConnectivity(); } - - /// 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/connectivity/connectivity/macos/connectivity.podspec b/packages/connectivity/connectivity/macos/connectivity.podspec deleted file mode 100644 index ea544dfc15de..000000000000 --- a/packages/connectivity/connectivity/macos/connectivity.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' - s.version = '0.0.1' - s.summary = 'No-op implementation of the macos connectivity to avoid build issues on macos' - s.description = <<-DESC - No-op implementation of the connectivity plugin to avoid build issues on macos. - https://github.com/flutter/flutter/issues/46618 - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/connectivity' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - - s.platform = :osx - s.osx.deployment_target = '10.11' -end - diff --git a/packages/connectivity/connectivity/pubspec.yaml b/packages/connectivity/connectivity/pubspec.yaml index 691c683a84e0..16e179cfa085 100644 --- a/packages/connectivity/connectivity/pubspec.yaml +++ b/packages/connectivity/connectivity/pubspec.yaml @@ -1,11 +1,13 @@ name: connectivity description: Flutter plugin for discovering the state of the network (WiFi & mobile/cellular) connectivity on Android and iOS. -homepage: https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity -# 0.4.y+z is compatible with 1.0.0, if you land a breaking change bump -# the version to 2.0.0. -# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.4.9+4 +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: @@ -23,23 +25,16 @@ flutter: dependencies: flutter: sdk: flutter - meta: ^1.0.5 - connectivity_platform_interface: ^1.0.2 - connectivity_macos: ^0.1.0 - connectivity_for_web: ^0.3.0 + meta: ^1.3.0 + connectivity_platform_interface: ^2.0.0 + connectivity_macos: ^0.2.0 + connectivity_for_web: ^0.4.0 dev_dependencies: flutter_test: sdk: flutter flutter_driver: sdk: flutter - test: any - integration_test: - path: ../../integration_test - mockito: ^4.1.1 - plugin_platform_interface: ^1.0.0 - pedantic: ^1.8.0 - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + 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 index aca87a658027..e6c253e0d49a 100644 --- a/packages/connectivity/connectivity/test/connectivity_test.dart +++ b/packages/connectivity/connectivity/test/connectivity_test.dart @@ -1,4 +1,4 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -6,12 +6,9 @@ import 'package:connectivity/connectivity.dart'; import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:mockito/mockito.dart'; +import 'package:test/fake.dart'; const ConnectivityResult kCheckConnectivityResult = ConnectivityResult.wifi; -const 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 = @@ -19,8 +16,8 @@ const LocationAuthorizationStatus kGetLocationResult = void main() { group('Connectivity', () { - Connectivity connectivity; - MockConnectivityPlatform fakePlatform; + late Connectivity connectivity; + late MockConnectivityPlatform fakePlatform; setUp(() async { fakePlatform = MockConnectivityPlatform(); ConnectivityPlatform.instance = fakePlatform; @@ -31,62 +28,13 @@ void main() { ConnectivityResult result = await connectivity.checkConnectivity(); expect(result, kCheckConnectivityResult); }); - - test('getWifiName', () async { - String result = await connectivity.getWifiName(); - expect(result, kWifiNameResult); - }); - - test('getWifiBSSID', () async { - String result = await connectivity.getWifiBSSID(); - expect(result, kWifiBSSIDResult); - }); - - test('getWifiIP', () async { - String result = await connectivity.getWifiIP(); - expect(result, kWifiIpAddressResult); - }); - - test('requestLocationServiceAuthorization', () async { - LocationAuthorizationStatus result = - await connectivity.requestLocationServiceAuthorization(); - expect(result, kRequestLocationResult); - }); - - test('getLocationServiceAuthorization', () async { - LocationAuthorizationStatus result = - await connectivity.getLocationServiceAuthorization(); - expect(result, kRequestLocationResult); - }); }); } -class MockConnectivityPlatform extends Mock +class MockConnectivityPlatform extends Fake with MockPlatformInterfaceMixin implements ConnectivityPlatform { Future checkConnectivity() async { return kCheckConnectivityResult; } - - Future getWifiName() async { - return kWifiNameResult; - } - - Future getWifiBSSID() async { - return kWifiBSSIDResult; - } - - Future getWifiIP() async { - return kWifiIpAddressResult; - } - - Future requestLocationServiceAuthorization({ - bool requestAlwaysLocationUsage = false, - }) async { - return kRequestLocationResult; - } - - Future getLocationServiceAuthorization() async { - return kGetLocationResult; - } } diff --git a/packages/connectivity/connectivity_for_web/AUTHORS b/packages/connectivity/connectivity_for_web/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/connectivity/connectivity_for_web/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/connectivity/connectivity_for_web/CHANGELOG.md b/packages/connectivity/connectivity_for_web/CHANGELOG.md index c2bf6326f69e..ccd689760b84 100644 --- a/packages/connectivity/connectivity_for_web/CHANGELOG.md +++ b/packages/connectivity/connectivity_for_web/CHANGELOG.md @@ -1,3 +1,12 @@ +## 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`. diff --git a/packages/connectivity/connectivity_for_web/LICENSE b/packages/connectivity/connectivity_for_web/LICENSE index 26351460d9de..c6823b81eb84 100644 --- a/packages/connectivity/connectivity_for_web/LICENSE +++ b/packages/connectivity/connectivity_for_web/LICENSE @@ -1,4 +1,4 @@ -Copyright 2016, the Flutter project authors. All rights reserved. +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: diff --git a/packages/connectivity/connectivity_for_web/example/README.md b/packages/connectivity/connectivity_for_web/example/README.md new file mode 100644 index 000000000000..0ec01e025570 --- /dev/null +++ b/packages/connectivity/connectivity_for_web/example/README.md @@ -0,0 +1,21 @@ +# Testing + +This package utilizes the `integration_test` package to run its tests in a web browser. + +See [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) for more info. + +## Running the tests + +Make sure you have updated to the latest Flutter master. + +1. Check what version of Chrome is running on the machine you're running tests on. + +2. Download and install driver for that version from here: + * + +3. Start the driver using `chromedriver --port=4444` + +4. Run tests: `flutter drive -d web-server --browser-name=chrome --driver=test_driver/integration_driver.dart --target=integration_test/TEST_NAME.dart`, or (in Linux): + + * Single: `./run_test.sh integration_test/TEST_NAME.dart` + * All: `./run_test.sh` diff --git a/packages/connectivity/connectivity_for_web/example/integration_test/network_information_test.dart b/packages/connectivity/connectivity_for_web/example/integration_test/network_information_test.dart new file mode 100644 index 000000000000..e6faa30da4dc --- /dev/null +++ b/packages/connectivity/connectivity_for_web/example/integration_test/network_information_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: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 new file mode 100644 index 000000000000..556b6fe6fca0 --- /dev/null +++ b/packages/connectivity/connectivity_for_web/example/integration_test/src/connectivity_mocks.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 '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 new file mode 100644 index 000000000000..e1a38dcdcd46 --- /dev/null +++ b/packages/connectivity/connectivity_for_web/example/lib/main.dart @@ -0,0 +1,25 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +void main() { + runApp(MyApp()); +} + +/// App for testing +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State { + @override + Widget build(BuildContext context) { + return Directionality( + textDirection: TextDirection.ltr, + child: Text('Testing... Look at the console output for results!'), + ); + } +} diff --git a/packages/connectivity/connectivity_for_web/example/pubspec.yaml b/packages/connectivity/connectivity_for_web/example/pubspec.yaml new file mode 100644 index 000000000000..3b8e209e2486 --- /dev/null +++ b/packages/connectivity/connectivity_for_web/example/pubspec.yaml @@ -0,0 +1,21 @@ +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 new file mode 100755 index 000000000000..aa52974f310e --- /dev/null +++ b/packages/connectivity/connectivity_for_web/example/run_test.sh @@ -0,0 +1,22 @@ +#!/usr/bin/bash +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +if pgrep -lf chromedriver > /dev/null; then + echo "chromedriver is running." + + if [ $# -eq 0 ]; then + echo "No target specified, running all tests..." + find integration_test/ -iname *_test.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target='{}' + else + echo "Running test target: $1..." + set -x + flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target=$1 + fi + + else + echo "chromedriver is not running." + echo "Please, check the README.md for instructions on how to use run_test.sh" +fi + diff --git a/packages/connectivity/connectivity_for_web/example/test_driver/integration_test.dart b/packages/connectivity/connectivity_for_web/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/connectivity/connectivity_for_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_for_web/example/web/index.html b/packages/connectivity/connectivity_for_web/example/web/index.html new file mode 100644 index 000000000000..7fb138cc90fa --- /dev/null +++ b/packages/connectivity/connectivity_for_web/example/web/index.html @@ -0,0 +1,13 @@ + + + + + + example + + + + + diff --git a/packages/connectivity/connectivity_for_web/ios/connectivity_for_web.podspec b/packages/connectivity/connectivity_for_web/ios/connectivity_for_web.podspec deleted file mode 100644 index 75b891c56533..000000000000 --- a/packages/connectivity/connectivity_for_web/ios/connectivity_for_web.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint connectivity_web.podspec' to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'connectivity_for_web' - s.version = '0.1.0' - s.summary = 'No-op implementation of connectivity web plugin to avoid build issues on iOS' - s.description = <<-DESC -temp fake connectivity_web plugin - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity_for_web' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.dependency 'Flutter' - s.platform = :ios, '8.0' - - # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } - s.swift_version = '5.0' -end diff --git a/packages/connectivity/connectivity_for_web/lib/connectivity_for_web.dart b/packages/connectivity/connectivity_for_web/lib/connectivity_for_web.dart index fd061a878867..d1c6811f5349 100644 --- a/packages/connectivity/connectivity_for_web/lib/connectivity_for_web.dart +++ b/packages/connectivity/connectivity_for_web/lib/connectivity_for_web.dart @@ -1,3 +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 'dart:async'; import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; 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 index 5caa5679d6ad..475ec0d675b7 100644 --- 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 @@ -1,3 +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 'dart:async'; import 'dart:html' as html show window; @@ -9,26 +13,26 @@ class DartHtmlConnectivityPlugin extends ConnectivityPlugin { /// Checks the connection status of the device. @override Future checkConnectivity() async { - return html.window.navigator.onLine + return html.window.navigator.onLine ?? false ? ConnectivityResult.wifi : ConnectivityResult.none; } - StreamController _connectivityResult; + StreamController? _connectivityResult; /// Returns a Stream of ConnectivityResults changes. @override Stream get onConnectivityChanged { if (_connectivityResult == null) { - _connectivityResult = StreamController(); + _connectivityResult = StreamController.broadcast(); // Fallback to dart:html window.onOnline / window.onOffline html.window.onOnline.listen((event) { - _connectivityResult.add(ConnectivityResult.wifi); + _connectivityResult!.add(ConnectivityResult.wifi); }); html.window.onOffline.listen((event) { - _connectivityResult.add(ConnectivityResult.none); + _connectivityResult!.add(ConnectivityResult.none); }); } - return _connectivityResult.stream; + 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 index 99bac2ab8d30..6554f7a8c124 100644 --- 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 @@ -1,7 +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 'dart:async'; import 'dart:html' as html show window, NetworkInformation; -import 'dart:js'; -import 'dart:js_util'; +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'; @@ -18,7 +22,7 @@ class NetworkInformationApiConnectivityPlugin extends ConnectivityPlugin { /// The constructor of the plugin. NetworkInformationApiConnectivityPlugin() - : this.withConnection(html.window.navigator.connection); + : this.withConnection(html.window.navigator.connection!); /// Creates the plugin, with an override of the NetworkInformation object. @visibleForTesting @@ -32,8 +36,8 @@ class NetworkInformationApiConnectivityPlugin extends ConnectivityPlugin { return networkInformationToConnectivityResult(_networkInformation); } - StreamController _connectivityResultStreamController; - Stream _connectivityResultStream; + StreamController? _connectivityResultStreamController; + late Stream _connectivityResultStream; /// Returns a Stream of ConnectivityResults changes. @override @@ -41,8 +45,10 @@ class NetworkInformationApiConnectivityPlugin extends ConnectivityPlugin { if (_connectivityResultStreamController == null) { _connectivityResultStreamController = StreamController(); + + // Directly write the 'onchange' function on the networkInformation object. setProperty(_networkInformation, 'onchange', allowInterop((_) { - _connectivityResultStreamController + _connectivityResultStreamController! .add(networkInformationToConnectivityResult(_networkInformation)); })); // TODO: Implement the above with _networkInformation.onChange: @@ -54,7 +60,7 @@ class NetworkInformationApiConnectivityPlugin extends ConnectivityPlugin { // onChange Stream upon hot restart. // https://github.com/dart-lang/sdk/issues/42679 _connectivityResultStream = - _connectivityResultStreamController.stream.asBroadcastStream(); + _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 index efefd8d52440..691bd6da3bfb 100644 --- a/packages/connectivity/connectivity_for_web/lib/src/utils/connectivity_result.dart +++ b/packages/connectivity/connectivity_for_web/lib/src/utils/connectivity_result.dart @@ -1,9 +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 '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, + html.NetworkInformation? info, ) { if (info == null) { return ConnectivityResult.none; @@ -12,10 +16,10 @@ ConnectivityResult networkInformationToConnectivityResult( return ConnectivityResult.none; } if (info.effectiveType != null) { - return _effectiveTypeToConnectivityResult(info.effectiveType); + return _effectiveTypeToConnectivityResult(info.effectiveType!); } if (info.type != null) { - return _typeToConnectivityResult(info.type); + return _typeToConnectivityResult(info.type!); } return ConnectivityResult.none; } diff --git a/packages/connectivity/connectivity_for_web/pubspec.yaml b/packages/connectivity/connectivity_for_web/pubspec.yaml index 21fd80b4b56a..5b05dd80d088 100644 --- a/packages/connectivity/connectivity_for_web/pubspec.yaml +++ b/packages/connectivity/connectivity_for_web/pubspec.yaml @@ -1,7 +1,12 @@ 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. -version: 0.3.1+3 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 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" flutter: plugin: @@ -11,22 +16,12 @@ flutter: fileName: connectivity_for_web.dart dependencies: - connectivity_platform_interface: ^1.0.3 + connectivity_platform_interface: ^2.0.0 flutter_web_plugins: sdk: flutter flutter: sdk: flutter dev_dependencies: - test: any - flutter_driver: - sdk: flutter flutter_test: sdk: flutter - integration_test: - path: ../../integration_test - mockito: ^4.1.1 - -environment: - sdk: ">=2.6.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4" diff --git a/packages/connectivity/connectivity_for_web/test/.gitignore b/packages/connectivity/connectivity_for_web/test/.gitignore deleted file mode 100644 index d7dee828a6b9..000000000000 --- a/packages/connectivity/connectivity_for_web/test/.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/test/README.md b/packages/connectivity/connectivity_for_web/test/README.md new file mode 100644 index 000000000000..7c5b4ad682ba --- /dev/null +++ b/packages/connectivity/connectivity_for_web/test/README.md @@ -0,0 +1,5 @@ +## test + +This package uses integration tests for testing. + +See `example/README.md` for more info. diff --git a/packages/connectivity/connectivity_for_web/test/lib/main.dart b/packages/connectivity/connectivity_for_web/test/lib/main.dart deleted file mode 100644 index e3473532553e..000000000000 --- a/packages/connectivity/connectivity_for_web/test/lib/main.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:integration_test/integration_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; -import 'package:connectivity_for_web/src/network_information_api_connectivity_plugin.dart'; - -import 'package:mockito/mockito.dart'; - -import 'src/connectivity_mocks.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('checkConnectivity', () { - void testCheckConnectivity({ - String type, - String effectiveType, - num downlink = 10, - num rtt = 50, - ConnectivityResult expected, - }) { - final connection = MockNetworkInformation(); - when(connection.type).thenReturn(type); - when(connection.effectiveType).thenReturn(effectiveType); - when(connection.downlink).thenReturn(downlink); - when(connection.rtt).thenReturn(downlink); - - NetworkInformationApiConnectivityPlugin plugin = - NetworkInformationApiConnectivityPlugin.withConnection(connection); - expect(plugin.checkConnectivity(), completion(equals(expected))); - } - - test('0 downlink and rtt -> none', () { - testCheckConnectivity( - effectiveType: '4g', - downlink: 0, - rtt: 0, - expected: ConnectivityResult.none); - }); - test('slow-2g -> mobile', () { - testCheckConnectivity( - effectiveType: 'slow-2g', expected: ConnectivityResult.mobile); - }); - test('2g -> mobile', () { - testCheckConnectivity( - effectiveType: '2g', expected: ConnectivityResult.mobile); - }); - test('3g -> mobile', () { - testCheckConnectivity( - effectiveType: '3g', expected: ConnectivityResult.mobile); - }); - test('4g -> wifi', () { - testCheckConnectivity( - effectiveType: '4g', expected: ConnectivityResult.wifi); - }); - }); - - group('get onConnectivityChanged', () { - test('puts change events in a Stream', () async { - final connection = MockNetworkInformation(); - NetworkInformationApiConnectivityPlugin plugin = - NetworkInformationApiConnectivityPlugin.withConnection(connection); - - Stream results = plugin.onConnectivityChanged; - - // Fake a disconnect-reconnect - await connection.mockChangeValue(downlink: 0, rtt: 0); - await connection.mockChangeValue( - downlink: 10, rtt: 50, effectiveType: '4g'); - - // The stream of results is infinite, so we need to .take(2) for this test to complete. - expect( - results.take(2).toList(), - completion( - equals([ConnectivityResult.none, ConnectivityResult.wifi]))); - }); - }); -} diff --git a/packages/connectivity/connectivity_for_web/test/lib/src/connectivity_mocks.dart b/packages/connectivity/connectivity_for_web/test/lib/src/connectivity_mocks.dart deleted file mode 100644 index 7b82b512065b..000000000000 --- a/packages/connectivity/connectivity_for_web/test/lib/src/connectivity_mocks.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'dart:html'; - -import 'package:mockito/mockito.dart'; - -/// A Mock implementation of the NetworkInformation API that allows -/// for external modification of its values. -class MockNetworkInformation extends Mock implements NetworkInformation { - /// The callback that will fire after the network information values change. - Function onchange; - - /// Changes the desired values, and triggers the change event listener. - void mockChangeValue({ - String type, - String effectiveType, - num downlink, - num rtt, - }) async { - when(this.type).thenAnswer((_) => type); - when(this.effectiveType).thenAnswer((_) => effectiveType); - when(this.downlink).thenAnswer((_) => downlink); - when(this.rtt).thenAnswer((_) => rtt); - - onchange(Event('change')); - } -} diff --git a/packages/connectivity/connectivity_for_web/test/pubspec.yaml b/packages/connectivity/connectivity_for_web/test/pubspec.yaml deleted file mode 100644 index 512b7df8c26e..000000000000 --- a/packages/connectivity/connectivity_for_web/test/pubspec.yaml +++ /dev/null @@ -1,25 +0,0 @@ -name: connectivity_web_example -description: Example web app for the connectivity plugin -homepage: https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity_web - -dependencies: - connectivity_for_web: - path: ../ - js: ^0.6.1+1 - flutter_web_plugins: - sdk: flutter - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_driver: - sdk: flutter - integration_test: - path: ../../../integration_test - mockito: ^4.1.1 - -environment: - sdk: ">=2.6.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4" 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 new file mode 100644 index 000000000000..442c50144727 --- /dev/null +++ b/packages/connectivity/connectivity_for_web/test/tests_exist_elsewhere_test.dart @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '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_for_web/test/web/index.html b/packages/connectivity/connectivity_for_web/test/web/index.html deleted file mode 100644 index 6eff9a740d43..000000000000 --- a/packages/connectivity/connectivity_for_web/test/web/index.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - example - - - - - diff --git a/packages/connectivity/connectivity_macos/AUTHORS b/packages/connectivity/connectivity_macos/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/connectivity/connectivity_macos/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/connectivity/connectivity_macos/CHANGELOG.md b/packages/connectivity/connectivity_macos/CHANGELOG.md index e997123ecc89..c7bc5b4cf469 100644 --- a/packages/connectivity/connectivity_macos/CHANGELOG.md +++ b/packages/connectivity/connectivity_macos/CHANGELOG.md @@ -1,3 +1,29 @@ +## NEXT + +* Add Swift language version to podspec. + +## 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. diff --git a/packages/connectivity/connectivity_macos/LICENSE b/packages/connectivity/connectivity_macos/LICENSE index 507569823f1b..c6823b81eb84 100644 --- a/packages/connectivity/connectivity_macos/LICENSE +++ b/packages/connectivity/connectivity_macos/LICENSE @@ -1,4 +1,4 @@ -Copyright 2019 The Chromium Authors. All rights reserved. +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: diff --git a/packages/connectivity/connectivity_macos/README.md b/packages/connectivity/connectivity_macos/README.md index 464f7d79ccd1..6974fd1fcc7e 100644 --- a/packages/connectivity/connectivity_macos/README.md +++ b/packages/connectivity/connectivity_macos/README.md @@ -2,13 +2,6 @@ The macos implementation of [`connectivity`]. -**Please set your constraint to `connectivity_macos: '>=0.1.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.1.y+z`. -Please use `connectivity_macos: '>=0.1.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ## Usage ### Import the package @@ -29,4 +22,3 @@ dependencies: ``` Refer to the `connectivity` [documentation](https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity) for more details. - diff --git a/packages/connectivity/connectivity_macos/example/integration_test/connectivity_test.dart b/packages/connectivity/connectivity_macos/example/integration_test/connectivity_test.dart new file mode 100644 index 000000000000..3e2b1c008a84 --- /dev/null +++ b/packages/connectivity/connectivity_macos/example/integration_test/connectivity_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 '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/ios/Flutter/AppFrameworkInfo.plist b/packages/connectivity/connectivity_macos/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6c2de8086bcd..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - UIRequiredDeviceCapabilities - - arm64 - - MinimumOSVersion - 8.0 - - diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner.xcodeproj/project.pbxproj b/packages/connectivity/connectivity_macos/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index e497d093be56..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,490 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - EB0BA966000B5C35B13186D7 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C80D49AFD183103034E444C2 /* libPods-Runner.a */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 3173C764DD180BE02EB51E47 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; - 69D903F0A9A7C636EE803AF8 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C80D49AFD183103034E444C2 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - EB0BA966000B5C35B13186D7 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 89F516DEFCBF79E39D2885C2 /* Frameworks */ = { - isa = PBXGroup; - children = ( - C80D49AFD183103034E444C2 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; - 8ECC1C323F60D5498EEC2315 /* Pods */ = { - isa = PBXGroup; - children = ( - 69D903F0A9A7C636EE803AF8 /* Pods-Runner.debug.xcconfig */, - 3173C764DD180BE02EB51E47 /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B80C3931E831B6300D905FE /* App.framework */, - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 8ECC1C323F60D5498EEC2315 /* Pods */, - 89F516DEFCBF79E39D2885C2 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 3BAF367E8BACBC7576CEE653 /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 6A2F146AD353BE7A0C3E797E /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; - }; - 3BAF367E8BACBC7576CEE653 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 6A2F146AD353BE7A0C3E797E /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.connectivityExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.connectivityExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/connectivity/connectivity_macos/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a16ed0f..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/connectivity/connectivity_macos/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 3bb3697ef41c..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/AppDelegate.h b/packages/connectivity/connectivity_macos/example/ios/Runner/AppDelegate.h deleted file mode 100644 index d9e18e990f2e..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/AppDelegate.m b/packages/connectivity/connectivity_macos/example/ios/Runner/AppDelegate.m deleted file mode 100644 index f08675707182..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d22f10b2ab63..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/connectivity/connectivity_macos/example/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index ebf48f603974..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Info.plist b/packages/connectivity/connectivity_macos/example/ios/Runner/Info.plist deleted file mode 100644 index babbd80f1619..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Runner/Info.plist +++ /dev/null @@ -1,53 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - connectivity_example - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - NSLocationAlwaysAndWhenInUseUsageDescription - This app requires accessing your location information all the time to get wi-fi information. - NSLocationWhenInUseUsageDescription - This app requires accessing your location information when the app is in foreground to get wi-fi information. - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Runner.entitlements b/packages/connectivity/connectivity_macos/example/ios/Runner/Runner.entitlements deleted file mode 100644 index ba21fbdaf290..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Runner/Runner.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.developer.networking.wifi-info - - - diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/main.m b/packages/connectivity/connectivity_macos/example/ios/Runner/main.m deleted file mode 100644 index bec320c0bee0..000000000000 --- a/packages/connectivity/connectivity_macos/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/connectivity/connectivity_macos/example/lib/main.dart b/packages/connectivity/connectivity_macos/example/lib/main.dart index 4ad30972679a..d0e07341fe92 100644 --- a/packages/connectivity/connectivity_macos/example/lib/main.dart +++ b/packages/connectivity/connectivity_macos/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -7,7 +7,7 @@ import 'dart:async'; import 'dart:io'; -import 'package:connectivity/connectivity.dart'; +import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -40,9 +40,9 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); + MyHomePage({Key? key, this.title}) : super(key: key); - final String title; + final String? title; @override _MyHomePageState createState() => _MyHomePageState(); @@ -50,8 +50,8 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { String _connectionStatus = 'Unknown'; - final Connectivity _connectivity = Connectivity(); - StreamSubscription _connectivitySubscription; + final ConnectivityPlatform _connectivity = ConnectivityPlatform.instance; + late StreamSubscription _connectivitySubscription; @override void initState() { @@ -69,7 +69,7 @@ class _MyHomePageState extends State { // Platform messages are asynchronous, so we initialize in an async method. Future initConnectivity() async { - ConnectivityResult result; + late ConnectivityResult result; // Platform messages may fail, so we use a try/catch PlatformException. try { result = await _connectivity.checkConnectivity(); @@ -100,7 +100,7 @@ class _MyHomePageState extends State { Future _updateConnectionStatus(ConnectivityResult result) async { switch (result) { case ConnectivityResult.wifi: - String wifiName, wifiBSSID, wifiIP; + String? wifiName, wifiBSSID, wifiIP; try { if (Platform.isIOS) { diff --git a/packages/connectivity/connectivity_macos/example/macos/Podfile b/packages/connectivity/connectivity_macos/example/macos/Podfile new file mode 100644 index 000000000000..e9131e75c4d2 --- /dev/null +++ b/packages/connectivity/connectivity_macos/example/macos/Podfile @@ -0,0 +1,46 @@ +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/AppDelegate.swift b/packages/connectivity/connectivity_macos/example/macos/Runner/AppDelegate.swift index d53ef6437726..5cec4c48f620 100644 --- a/packages/connectivity/connectivity_macos/example/macos/Runner/AppDelegate.swift +++ b/packages/connectivity/connectivity_macos/example/macos/Runner/AppDelegate.swift @@ -1,3 +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 Cocoa import FlutterMacOS diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/connectivity/connectivity_macos/example/macos/Runner/Configs/AppInfo.xcconfig index a95148814518..db9bebac4b66 100644 --- a/packages/connectivity/connectivity_macos/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/packages/connectivity/connectivity_macos/example/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = connectivity_example // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.connectivityExample +PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.connectivityExample // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2019 io.flutter.plugins. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2019 The Flutter Authors. All rights reserved. diff --git a/packages/connectivity/connectivity_macos/example/macos/Runner/MainFlutterWindow.swift b/packages/connectivity/connectivity_macos/example/macos/Runner/MainFlutterWindow.swift index 2722837ec918..32aaeedceb1f 100644 --- a/packages/connectivity/connectivity_macos/example/macos/Runner/MainFlutterWindow.swift +++ b/packages/connectivity/connectivity_macos/example/macos/Runner/MainFlutterWindow.swift @@ -1,3 +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 Cocoa import FlutterMacOS diff --git a/packages/connectivity/connectivity_macos/example/pubspec.yaml b/packages/connectivity/connectivity_macos/example/pubspec.yaml index 7faf3e2f67e7..0af2e1587b00 100644 --- a/packages/connectivity/connectivity_macos/example/pubspec.yaml +++ b/packages/connectivity/connectivity_macos/example/pubspec.yaml @@ -1,20 +1,29 @@ 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: any + connectivity_platform_interface: ^2.0.0 connectivity_macos: + # When depending on this package from a real application you should use: + # connectivity_macos: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: flutter_driver: sdk: flutter - test: any integration_test: - path: ../../../integration_test - pedantic: ^1.8.0 + sdk: flutter + pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/connectivity/connectivity_macos/example/test_driver/integration_test.dart b/packages/connectivity/connectivity_macos/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/connectivity/connectivity_macos/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_macos/example/test_driver/integration_test/connectivity_test.dart b/packages/connectivity/connectivity_macos/example/test_driver/integration_test/connectivity_test.dart deleted file mode 100644 index 54a67337285a..000000000000 --- a/packages/connectivity/connectivity_macos/example/test_driver/integration_test/connectivity_test.dart +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io'; -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', () { - Connectivity _connectivity; - - setUpAll(() async { - _connectivity = Connectivity(); - }); - - 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/test_driver/test/integration_test.dart b/packages/connectivity/connectivity_macos/example/test_driver/test/integration_test.dart deleted file mode 100644 index c0cbdcab2fb6..000000000000 --- a/packages/connectivity/connectivity_macos/example/test_driver/test/integration_test.dart +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:convert'; -import 'dart:io'; -import 'package:flutter_driver/flutter_driver.dart'; - -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String data = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - await driver.close(); - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} diff --git a/packages/connectivity/connectivity_macos/ios/connectivity_macos.podspec b/packages/connectivity/connectivity_macos/ios/connectivity_macos.podspec deleted file mode 100644 index a941a16327f3..000000000000 --- a/packages/connectivity/connectivity_macos/ios/connectivity_macos.podspec +++ /dev/null @@ -1,21 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'connectivity_macos' - s.version = '0.0.1' - s.summary = 'No-op implementation of the connectivity desktop plugin to avoid build issues on iOS' - s.description = <<-DESC - No-op implementation of connectivity_macos to avoid build issues on iOS. - See https://github.com/flutter/flutter/issues/39659 - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity_macos' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.ios.deployment_target = '8.0' -end \ No newline at end of file diff --git a/packages/connectivity/connectivity_macos/lib/connectivity_macos.dart b/packages/connectivity/connectivity_macos/lib/connectivity_macos.dart deleted file mode 100644 index 7be7b143ca79..000000000000 --- a/packages/connectivity/connectivity_macos/lib/connectivity_macos.dart +++ /dev/null @@ -1,3 +0,0 @@ -// Analyze will fail if there is no main.dart file. This file should -// be removed once an example app has been added to connectivity_macos. -// https://github.com/flutter/flutter/issues/51007 diff --git a/packages/connectivity/connectivity_macos/macos/Classes/ConnectivityPlugin.swift b/packages/connectivity/connectivity_macos/macos/Classes/ConnectivityPlugin.swift index 8eafbc3188cf..69efe80df5ac 100644 --- a/packages/connectivity/connectivity_macos/macos/Classes/ConnectivityPlugin.swift +++ b/packages/connectivity/connectivity_macos/macos/Classes/ConnectivityPlugin.swift @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/connectivity/connectivity_macos/macos/Classes/IPHelper.h b/packages/connectivity/connectivity_macos/macos/Classes/IPHelper.h index e5a067b9069f..e5370fb349c3 100644 --- a/packages/connectivity/connectivity_macos/macos/Classes/IPHelper.h +++ b/packages/connectivity/connectivity_macos/macos/Classes/IPHelper.h @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/connectivity/connectivity_macos/macos/connectivity_macos.podspec b/packages/connectivity/connectivity_macos/macos/connectivity_macos.podspec index 57836f900b5c..51629084a23d 100644 --- a/packages/connectivity/connectivity_macos/macos/connectivity_macos.podspec +++ b/packages/connectivity/connectivity_macos/macos/connectivity_macos.podspec @@ -18,4 +18,5 @@ Pod::Spec.new do |s| s.platform = :osx s.osx.deployment_target = '10.11' -end \ No newline at end of file + s.swift_version = '5.0' +end diff --git a/packages/connectivity/connectivity_macos/pubspec.yaml b/packages/connectivity/connectivity_macos/pubspec.yaml index 99151ff7418d..1e8842c7417a 100644 --- a/packages/connectivity/connectivity_macos/pubspec.yaml +++ b/packages/connectivity/connectivity_macos/pubspec.yaml @@ -1,24 +1,30 @@ name: connectivity_macos description: macOS implementation of the connectivity plugin. -# 0.1.y+z is compatible with 1.0.0, if you land a breaking change bump -# the version to 2.0.0. -# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.1.0+6 -homepage: https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity_macos +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+1 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" flutter: plugin: + implements: connectivity_platform_interface platforms: macos: pluginClass: ConnectivityPlugin -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" - 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.8.0 + pedantic: ^1.10.0 diff --git a/packages/connectivity/connectivity_platform_interface/AUTHORS b/packages/connectivity/connectivity_platform_interface/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/connectivity/connectivity_platform_interface/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/connectivity/connectivity_platform_interface/CHANGELOG.md b/packages/connectivity/connectivity_platform_interface/CHANGELOG.md index e45f8f7e4d99..ee75d03ccc65 100644 --- a/packages/connectivity/connectivity_platform_interface/CHANGELOG.md +++ b/packages/connectivity/connectivity_platform_interface/CHANGELOG.md @@ -1,10 +1,22 @@ +## 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 +* Remove dart:io Platform checks from the MethodChannel implementation. This is tripping the analysis of other versions of the plugin. ## 1.0.4 diff --git a/packages/connectivity/connectivity_platform_interface/LICENSE b/packages/connectivity/connectivity_platform_interface/LICENSE index d7412e0a1e0c..c6823b81eb84 100644 --- a/packages/connectivity/connectivity_platform_interface/LICENSE +++ b/packages/connectivity/connectivity_platform_interface/LICENSE @@ -1,4 +1,4 @@ -Copyright 2020 The Chromium Authors. All rights reserved. +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: diff --git a/packages/connectivity/connectivity_platform_interface/lib/connectivity_platform_interface.dart b/packages/connectivity/connectivity_platform_interface/lib/connectivity_platform_interface.dart index cfd9cf648a9c..6667b353a4aa 100644 --- a/packages/connectivity/connectivity_platform_interface/lib/connectivity_platform_interface.dart +++ b/packages/connectivity/connectivity_platform_interface/lib/connectivity_platform_interface.dart @@ -1,4 +1,4 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -50,17 +50,17 @@ abstract class ConnectivityPlatform extends PlatformInterface { } /// Obtains the wifi name (SSID) of the connected network - Future getWifiName() { + Future getWifiName() { throw UnimplementedError('getWifiName() has not been implemented.'); } /// Obtains the wifi BSSID of the connected network. - Future getWifiBSSID() { + Future getWifiBSSID() { throw UnimplementedError('getWifiBSSID() has not been implemented.'); } /// Obtains the IP address of the connected wifi network - Future getWifiIP() { + Future getWifiIP() { throw UnimplementedError('getWifiIP() has not been implemented.'); } diff --git a/packages/connectivity/connectivity_platform_interface/lib/src/enums.dart b/packages/connectivity/connectivity_platform_interface/lib/src/enums.dart index 9d8cef9e1a66..b77f54cf60b2 100644 --- a/packages/connectivity/connectivity_platform_interface/lib/src/enums.dart +++ b/packages/connectivity/connectivity_platform_interface/lib/src/enums.dart @@ -1,3 +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. + /// Connection status check result. enum ConnectivityResult { /// WiFi: Device connected via Wi-Fi diff --git a/packages/connectivity/connectivity_platform_interface/lib/src/method_channel_connectivity.dart b/packages/connectivity/connectivity_platform_interface/lib/src/method_channel_connectivity.dart index 87deaa21ea3b..bdf820ac3ba7 100644 --- a/packages/connectivity/connectivity_platform_interface/lib/src/method_channel_connectivity.dart +++ b/packages/connectivity/connectivity_platform_interface/lib/src/method_channel_connectivity.dart @@ -1,4 +1,4 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -22,29 +22,29 @@ class MethodChannelConnectivity extends ConnectivityPlatform { EventChannel eventChannel = EventChannel('plugins.flutter.io/connectivity_status'); - Stream _onConnectivityChanged; + Stream? _onConnectivityChanged; /// Fires whenever the connectivity state changes. Stream get onConnectivityChanged { if (_onConnectivityChanged == null) { - _onConnectivityChanged = eventChannel - .receiveBroadcastStream() - .map((dynamic result) => result.toString()) - .map(parseConnectivityResult); + _onConnectivityChanged = + eventChannel.receiveBroadcastStream().map((dynamic result) { + return result != null ? result.toString() : ''; + }).map(parseConnectivityResult); } - return _onConnectivityChanged; + return _onConnectivityChanged!; } @override - Future checkConnectivity() { - return methodChannel - .invokeMethod('check') - .then(parseConnectivityResult); + Future checkConnectivity() async { + final String checkResult = + await methodChannel.invokeMethod('check') ?? ''; + return parseConnectivityResult(checkResult); } @override - Future getWifiName() async { - String wifiName = await methodChannel.invokeMethod('wifiName'); + Future getWifiName() async { + String? wifiName = await methodChannel.invokeMethod('wifiName'); // as Android might return , uniforming result // our iOS implementation will return null if (wifiName == '') { @@ -54,29 +54,31 @@ class MethodChannelConnectivity extends ConnectivityPlatform { } @override - Future getWifiBSSID() { + Future getWifiBSSID() { return methodChannel.invokeMethod('wifiBSSID'); } @override - Future getWifiIP() { + Future getWifiIP() { return methodChannel.invokeMethod('wifiIPAddress'); } @override Future requestLocationServiceAuthorization({ bool requestAlwaysLocationUsage = false, - }) { - return methodChannel.invokeMethod( - 'requestLocationServiceAuthorization', [ - requestAlwaysLocationUsage - ]).then(parseLocationAuthorizationStatus); + }) async { + final String requestLocationServiceResult = await methodChannel + .invokeMethod('requestLocationServiceAuthorization', + [requestAlwaysLocationUsage]) ?? + ''; + return parseLocationAuthorizationStatus(requestLocationServiceResult); } @override - Future getLocationServiceAuthorization() { - return methodChannel - .invokeMethod('getLocationServiceAuthorization') - .then(parseLocationAuthorizationStatus); + Future getLocationServiceAuthorization() async { + final String getLocationServiceResult = await methodChannel + .invokeMethod('getLocationServiceAuthorization') ?? + ''; + return parseLocationAuthorizationStatus(getLocationServiceResult); } } diff --git a/packages/connectivity/connectivity_platform_interface/lib/src/utils.dart b/packages/connectivity/connectivity_platform_interface/lib/src/utils.dart index 2ae22e1c9fc3..3b5b753f6c29 100644 --- a/packages/connectivity/connectivity_platform_interface/lib/src/utils.dart +++ b/packages/connectivity/connectivity_platform_interface/lib/src/utils.dart @@ -1,3 +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:connectivity_platform_interface/connectivity_platform_interface.dart'; /// Convert a String to a ConnectivityResult value. diff --git a/packages/connectivity/connectivity_platform_interface/pubspec.yaml b/packages/connectivity/connectivity_platform_interface/pubspec.yaml index 5bafcf9b806b..2003fdde6eeb 100644 --- a/packages/connectivity/connectivity_platform_interface/pubspec.yaml +++ b/packages/connectivity/connectivity_platform_interface/pubspec.yaml @@ -1,21 +1,22 @@ name: connectivity_platform_interface description: A common platform interface for the connectivity plugin. -homepage: https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity_platform_interface +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: 1.0.6 +version: 2.0.1 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.12.13+hotfix.5" dependencies: flutter: sdk: flutter - meta: ^1.0.5 - plugin_platform_interface: ^1.0.1 + meta: ^1.3.0 + plugin_platform_interface: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + 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 index 3d9c405c30ab..38f4ac38b156 100644 --- a/packages/connectivity/connectivity_platform_interface/test/method_channel_connectivity_test.dart +++ b/packages/connectivity/connectivity_platform_interface/test/method_channel_connectivity_test.dart @@ -1,4 +1,4 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -12,7 +12,7 @@ void main() { group('$MethodChannelConnectivity', () { final List log = []; - MethodChannelConnectivity methodChannelConnectivity; + late MethodChannelConnectivity methodChannelConnectivity; setUp(() async { methodChannelConnectivity = MethodChannelConnectivity(); @@ -42,7 +42,7 @@ void main() { .setMockMethodCallHandler((MethodCall methodCall) async { switch (methodCall.method) { case 'listen': - await ServicesBinding.instance.defaultBinaryMessenger + await ServicesBinding.instance!.defaultBinaryMessenger .handlePlatformMessage( methodChannelConnectivity.eventChannel.name, methodChannelConnectivity.eventChannel.codec @@ -64,7 +64,7 @@ void main() { }); test('getWifiName', () async { - final String result = await methodChannelConnectivity.getWifiName(); + final String? result = await methodChannelConnectivity.getWifiName(); expect(result, '1337wifi'); expect( log, @@ -78,7 +78,7 @@ void main() { }); test('getWifiBSSID', () async { - final String result = await methodChannelConnectivity.getWifiBSSID(); + final String? result = await methodChannelConnectivity.getWifiBSSID(); expect(result, 'c0:ff:33:c0:d3:55'); expect( log, @@ -92,7 +92,7 @@ void main() { }); test('getWifiIP', () async { - final String result = await methodChannelConnectivity.getWifiIP(); + final String? result = await methodChannelConnectivity.getWifiIP(); expect(result, '127.0.0.1'); expect( log, diff --git a/packages/cross_file/analysis_options.yaml b/packages/cross_file/analysis_options.yaml new file mode 100644 index 000000000000..cda4f6e153e6 --- /dev/null +++ b/packages/cross_file/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options_legacy.yaml diff --git a/packages/device_info/analysis_options.yaml b/packages/device_info/analysis_options.yaml new file mode 100644 index 000000000000..cda4f6e153e6 --- /dev/null +++ b/packages/device_info/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options_legacy.yaml diff --git a/packages/device_info/device_info/AUTHORS b/packages/device_info/device_info/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/device_info/device_info/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/device_info/device_info/CHANGELOG.md b/packages/device_info/device_info/CHANGELOG.md index 9e627f90b30f..a92cb8ce94b1 100644 --- a/packages/device_info/device_info/CHANGELOG.md +++ b/packages/device_info/device_info/CHANGELOG.md @@ -1,3 +1,32 @@ +## 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. diff --git a/packages/device_info/device_info/LICENSE b/packages/device_info/device_info/LICENSE index a6d6c0749818..c6823b81eb84 100644 --- a/packages/device_info/device_info/LICENSE +++ b/packages/device_info/device_info/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017 The Chromium Authors. All rights reserved. +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: diff --git a/packages/device_info/device_info/README.md b/packages/device_info/device_info/README.md index db846c39690c..34cf9bb2ac2b 100644 --- a/packages/device_info/device_info/README.md +++ b/packages/device_info/device_info/README.md @@ -1,13 +1,21 @@ # device_info -Get current device information from within the Flutter application. +--- + +## 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. -**Please set your constraint to `device_info: '>=0.4.y+x <2.0.0'`** +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. -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.4.y+z`. -Please use `device_info: '>=0.4.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 +--- + +Get current device information from within the Flutter application. # Usage @@ -28,11 +36,11 @@ IosDeviceInfo iosInfo = await deviceInfo.iosInfo; print('Running on ${iosInfo.utsname.machine}'); // e.g. "iPod7,1" ``` -You will find links to the API docs on the [pub page](https://pub.dartlang.org/packages/device_info). +You will find links to the API docs on the [pub page](https://pub.dev/packages/device_info). ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). -For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code). +For help on editing plugin code, view the [documentation](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin). diff --git a/packages/device_info/device_info/android/build.gradle b/packages/device_info/device_info/android/build.gradle index 58bdfd327631..9b1f6470a37d 100644 --- a/packages/device_info/device_info/android/build.gradle +++ b/packages/device_info/device_info/android/build.gradle @@ -4,7 +4,7 @@ version '1.0-SNAPSHOT' buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -15,14 +15,14 @@ buildscript { rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } apply plugin: 'com.android.library' android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { minSdkVersion 16 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 index 137daf508ac2..9b766d7f8381 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/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 index 800ca6dcddb7..531e5db0c237 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/device_info/device_info/example/README.md b/packages/device_info/device_info/example/README.md index 36ca6cc0600b..ea47551011d0 100644 --- a/packages/device_info/device_info/example/README.md +++ b/packages/device_info/device_info/example/README.md @@ -5,4 +5,4 @@ Demonstrates how to use the `device_info` plugin. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[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 index 43d6d0a1a8c5..eb0c628330be 100644 --- a/packages/device_info/device_info/example/android/app/build.gradle +++ b/packages/device_info/device_info/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 28 + compileSdkVersion 29 lintOptions { disable 'InvalidPackage' diff --git a/packages/device_info/device_info/example/android/app/src/main/java/io/flutter/plugins/deviceinfoexample/EmbeddingV1Activity.java b/packages/device_info/device_info/example/android/app/src/main/java/io/flutter/plugins/deviceinfoexample/EmbeddingV1Activity.java index 0256c6b8a1fb..86966cd137bb 100644 --- a/packages/device_info/device_info/example/android/app/src/main/java/io/flutter/plugins/deviceinfoexample/EmbeddingV1Activity.java +++ b/packages/device_info/device_info/example/android/app/src/main/java/io/flutter/plugins/deviceinfoexample/EmbeddingV1Activity.java @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/device_info/device_info/example/android/app/src/main/java/io/flutter/plugins/deviceinfoexample/EmbeddingV1ActivityTest.java b/packages/device_info/device_info/example/android/app/src/main/java/io/flutter/plugins/deviceinfoexample/EmbeddingV1ActivityTest.java index 8fa880842d51..a9babfe803ae 100644 --- a/packages/device_info/device_info/example/android/app/src/main/java/io/flutter/plugins/deviceinfoexample/EmbeddingV1ActivityTest.java +++ b/packages/device_info/device_info/example/android/app/src/main/java/io/flutter/plugins/deviceinfoexample/EmbeddingV1ActivityTest.java @@ -1,3 +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. + package io.flutter.plugins.deviceinfoexample; import androidx.test.rule.ActivityTestRule; diff --git a/packages/device_info/device_info/example/android/build.gradle b/packages/device_info/device_info/example/android/build.gradle index 83f114c21e31..3274eb601b9b 100644 --- a/packages/device_info/device_info/example/android/build.gradle +++ b/packages/device_info/device_info/example/android/build.gradle @@ -1,7 +1,7 @@ buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -12,7 +12,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/packages/device_info/device_info/example/integration_test/device_info_test.dart b/packages/device_info/device_info/example/integration_test/device_info_test.dart index 2fd1d9a9a491..953eb856d62a 100644 --- a/packages/device_info/device_info/example/integration_test/device_info_test.dart +++ b/packages/device_info/device_info/example/integration_test/device_info_test.dart @@ -1,6 +1,6 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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'; @@ -10,8 +10,8 @@ import 'package:integration_test/integration_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - IosDeviceInfo iosInfo; - AndroidDeviceInfo androidInfo; + late IosDeviceInfo iosInfo; + late AndroidDeviceInfo androidInfo; setUpAll(() async { final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); diff --git a/packages/device_info/device_info/example/ios/Podfile b/packages/device_info/device_info/example/ios/Podfile new file mode 100644 index 000000000000..f7d6a5e68c3a --- /dev/null +++ b/packages/device_info/device_info/example/ios/Podfile @@ -0,0 +1,38 @@ +# 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 index 8f80e80abb09..60f84800118f 100644 --- a/packages/device_info/device_info/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/device_info/device_info/example/ios/Runner.xcodeproj/project.pbxproj @@ -177,7 +177,7 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; + ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; @@ -437,7 +437,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.deviceInfoExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.deviceInfoExample; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -458,7 +458,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.deviceInfoExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.deviceInfoExample; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; diff --git a/packages/device_info/device_info/example/ios/Runner/AppDelegate.h b/packages/device_info/device_info/example/ios/Runner/AppDelegate.h index d9e18e990f2e..0681d288bb70 100644 --- a/packages/device_info/device_info/example/ios/Runner/AppDelegate.h +++ b/packages/device_info/device_info/example/ios/Runner/AppDelegate.h @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/device_info/device_info/example/ios/Runner/AppDelegate.m b/packages/device_info/device_info/example/ios/Runner/AppDelegate.m index f08675707182..30b87969f44a 100644 --- a/packages/device_info/device_info/example/ios/Runner/AppDelegate.m +++ b/packages/device_info/device_info/example/ios/Runner/AppDelegate.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/device_info/device_info/example/ios/Runner/main.m b/packages/device_info/device_info/example/ios/Runner/main.m index bec320c0bee0..f97b9ef5c8a1 100644 --- a/packages/device_info/device_info/example/ios/Runner/main.m +++ b/packages/device_info/device_info/example/ios/Runner/main.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/device_info/device_info/example/lib/main.dart b/packages/device_info/device_info/example/lib/main.dart index 1c1064aa09ee..44e3fb4ee2f7 100644 --- a/packages/device_info/device_info/example/lib/main.dart +++ b/packages/device_info/device_info/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -12,9 +12,9 @@ import 'package:flutter/services.dart'; import 'package:device_info/device_info.dart'; void main() { - runZoned(() { + runZonedGuarded(() { runApp(MyApp()); - }, onError: (dynamic error, dynamic stack) { + }, (dynamic error, dynamic stack) { print(error); print(stack); }); @@ -36,7 +36,7 @@ class _MyAppState extends State { } Future initPlatformState() async { - Map deviceData; + Map deviceData = {}; try { if (Platform.isAndroid) { diff --git a/packages/device_info/device_info/example/pubspec.yaml b/packages/device_info/device_info/example/pubspec.yaml index e22f6026ba69..c4e84f60de5e 100644 --- a/packages/device_info/device_info/example/pubspec.yaml +++ b/packages/device_info/device_info/example/pubspec.yaml @@ -1,18 +1,28 @@ 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: - path: ../../../integration_test - pedantic: ^1.8.0 + sdk: flutter + pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/device_info/device_info/example/test_driver/integration_test.dart b/packages/device_info/device_info/example/test_driver/integration_test.dart index 7a2c21338786..4f10f2a522f3 100644 --- a/packages/device_info/device_info/example/test_driver/integration_test.dart +++ b/packages/device_info/device_info/example/test_driver/integration_test.dart @@ -1,17 +1,7 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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'; +import 'package:integration_test/integration_test_driver.dart'; -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String data = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - await driver.close(); - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} +Future main() => integrationDriver(); diff --git a/packages/device_info/device_info/ios/Classes/FLTDeviceInfoPlugin.h b/packages/device_info/device_info/ios/Classes/FLTDeviceInfoPlugin.h index b5e95ed10e84..511b5b893fcb 100644 --- a/packages/device_info/device_info/ios/Classes/FLTDeviceInfoPlugin.h +++ b/packages/device_info/device_info/ios/Classes/FLTDeviceInfoPlugin.h @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/device_info/device_info/ios/Classes/FLTDeviceInfoPlugin.m b/packages/device_info/device_info/ios/Classes/FLTDeviceInfoPlugin.m index 423896061ac7..3d4d25ad9c6e 100644 --- a/packages/device_info/device_info/ios/Classes/FLTDeviceInfoPlugin.m +++ b/packages/device_info/device_info/ios/Classes/FLTDeviceInfoPlugin.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/device_info/device_info/lib/device_info.dart b/packages/device_info/device_info/lib/device_info.dart index f63730c4323f..1153ac6f7da6 100644 --- a/packages/device_info/device_info/lib/device_info.dart +++ b/packages/device_info/device_info/lib/device_info.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -15,7 +15,7 @@ class DeviceInfoPlugin { DeviceInfoPlugin(); /// This information does not change from call to call. Cache it. - AndroidDeviceInfo _cachedAndroidDeviceInfo; + AndroidDeviceInfo? _cachedAndroidDeviceInfo; /// Information derived from `android.os.Build`. /// @@ -25,7 +25,7 @@ class DeviceInfoPlugin { await DeviceInfoPlatform.instance.androidInfo(); /// This information does not change from call to call. Cache it. - IosDeviceInfo _cachedIosDeviceInfo; + IosDeviceInfo? _cachedIosDeviceInfo; /// Information derived from `UIDevice`. /// diff --git a/packages/device_info/device_info/pubspec.yaml b/packages/device_info/device_info/pubspec.yaml index 967a5bb0b585..c5830f401039 100644 --- a/packages/device_info/device_info/pubspec.yaml +++ b/packages/device_info/device_info/pubspec.yaml @@ -1,11 +1,13 @@ name: device_info description: Flutter plugin providing detailed information about the device (make, model, etc.), and Android or iOS version the app is running on. -homepage: https://github.com/flutter/plugins/tree/master/packages/device_info -# 0.4.y+z is compatible with 1.0.0, if you land a breaking change bump -# the version to 2.0.0. -# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.4.2+8 +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: @@ -19,14 +21,9 @@ flutter: dependencies: flutter: sdk: flutter - device_info_platform_interface: ^1.0.0 - + device_info_platform_interface: ^2.0.0 dev_dependencies: - test: ^1.3.0 + test: ^1.16.3 flutter_test: sdk: flutter - pedantic: ^1.8.0 - -environment: - sdk: ">=2.1.0<3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + pedantic: ^1.10.0 diff --git a/packages/device_info/device_info_platform_interface/AUTHORS b/packages/device_info/device_info_platform_interface/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/device_info/device_info_platform_interface/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/device_info/device_info_platform_interface/CHANGELOG.md b/packages/device_info/device_info_platform_interface/CHANGELOG.md index 8a7eb6c46be3..438d5bccad40 100644 --- a/packages/device_info/device_info_platform_interface/CHANGELOG.md +++ b/packages/device_info/device_info_platform_interface/CHANGELOG.md @@ -1,3 +1,17 @@ +## 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. diff --git a/packages/device_info/device_info_platform_interface/LICENSE b/packages/device_info/device_info_platform_interface/LICENSE index a6d6c0749818..c6823b81eb84 100644 --- a/packages/device_info/device_info_platform_interface/LICENSE +++ b/packages/device_info/device_info_platform_interface/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017 The Chromium Authors. All rights reserved. +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: diff --git a/packages/device_info/device_info_platform_interface/lib/device_info_platform_interface.dart b/packages/device_info/device_info_platform_interface/lib/device_info_platform_interface.dart index 808b7adf9dc7..a40363b2dcb6 100644 --- a/packages/device_info/device_info_platform_interface/lib/device_info_platform_interface.dart +++ b/packages/device_info/device_info_platform_interface/lib/device_info_platform_interface.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -7,10 +7,8 @@ import 'dart:async'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'method_channel/method_channel_device_info.dart'; - import 'model/android_device_info.dart'; import 'model/ios_device_info.dart'; - export 'model/android_device_info.dart'; export 'model/ios_device_info.dart'; diff --git a/packages/device_info/device_info_platform_interface/lib/method_channel/method_channel_device_info.dart b/packages/device_info/device_info_platform_interface/lib/method_channel/method_channel_device_info.dart index 7bd02e97436d..3c19e57f66a8 100644 --- a/packages/device_info/device_info_platform_interface/lib/method_channel/method_channel_device_info.dart +++ b/packages/device_info/device_info_platform_interface/lib/method_channel/method_channel_device_info.dart @@ -1,8 +1,11 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + import 'dart:async'; import 'package:flutter/services.dart'; import 'package:meta/meta.dart'; - import 'package:device_info_platform_interface/device_info_platform_interface.dart'; /// An implementation of [DeviceInfoPlatform] that uses method channels. @@ -13,16 +16,15 @@ class MethodChannelDeviceInfo extends DeviceInfoPlatform { // Method channel for Android devices Future androidInfo() async { - return AndroidDeviceInfo.fromMap( - (await channel.invokeMethod('getAndroidDeviceInfo')) - .cast(), - ); + return AndroidDeviceInfo.fromMap((await channel + .invokeMapMethod('getAndroidDeviceInfo')) ?? + {}); } // Method channel for iOS devices Future iosInfo() async { return IosDeviceInfo.fromMap( - (await channel.invokeMethod('getIosDeviceInfo')).cast(), - ); + (await channel.invokeMapMethod('getIosDeviceInfo')) ?? + {}); } } diff --git a/packages/device_info/device_info_platform_interface/lib/model/android_device_info.dart b/packages/device_info/device_info_platform_interface/lib/model/android_device_info.dart index 5b326cc5350a..b61dc14a0420 100644 --- a/packages/device_info/device_info_platform_interface/lib/model/android_device_info.dart +++ b/packages/device_info/device_info_platform_interface/lib/model/android_device_info.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -8,27 +8,27 @@ class AndroidDeviceInfo { /// Android device Info class. AndroidDeviceInfo({ - this.version, - this.board, - this.bootloader, - this.brand, - this.device, - this.display, - this.fingerprint, - this.hardware, - this.host, - this.id, - this.manufacturer, - this.model, - this.product, - List supported32BitAbis, - List supported64BitAbis, - List supportedAbis, - this.tags, - this.type, - this.isPhysicalDevice, - this.androidId, - List systemFeatures, + 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), @@ -38,39 +38,63 @@ class AndroidDeviceInfo { final AndroidBuildVersion version; /// The name of the underlying board, like "goldfish". + /// + /// The value is an empty String if it is not available. final String board; /// The system bootloader version number. + /// + /// The value is an empty String if it is not available. final String bootloader; /// The consumer-visible brand with which the product/hardware will be associated, if any. + /// + /// The value is an empty String if it is not available. final String brand; /// The name of the industrial design. + /// + /// The value is an empty String if it is not available. final String device; /// A build ID string meant for displaying to the user. + /// + /// The value is an empty String if it is not available. final String display; /// A string that uniquely identifies this build. + /// + /// The value is an empty String if it is not available. final String fingerprint; /// The name of the hardware (from the kernel command line or /proc). + /// + /// The value is an empty String if it is not available. final String hardware; /// Hostname. + /// + /// The value is an empty String if it is not available. final String host; /// Either a changelist number, or a label like "M4-rc20". + /// + /// The value is an empty String if it is not available. final String id; /// The manufacturer of the product/hardware. + /// + /// The value is an empty String if it is not available. final String manufacturer; /// The end-user-visible name for the end product. + /// + /// The value is an empty String if it is not available. final String model; /// The name of the overall product. + /// + /// The value is an empty String if it is not available. final String product; /// An ordered list of 32 bit ABIs supported by this device. @@ -83,15 +107,23 @@ class AndroidDeviceInfo { final List supportedAbis; /// Comma-separated tags describing the build, like "unsigned,debug". + /// + /// The value is an empty String if it is not available. final String tags; /// The type of build, like "user" or "eng". + /// + /// The value is an empty String if it is not available. final String type; - /// `false` if the application is running in an emulator, `true` otherwise. + /// The value is `true` if the application is running on a physical device. + /// + /// The value is `false` when the application is running on a emulator, or the value is unavailable. final bool isPhysicalDevice; /// The Android hardware device ID that is unique between the device + user and app signing. + /// + /// The value is an empty String if it is not available. final String androidId; /// Describes what features are available on the current device. @@ -113,35 +145,41 @@ class AndroidDeviceInfo { /// Deserializes from the message received from [_kChannel]. static AndroidDeviceInfo fromMap(Map map) { return AndroidDeviceInfo( - version: AndroidBuildVersion._fromMap( - map['version']?.cast() ?? {}), - board: map['board'], - bootloader: map['bootloader'], - brand: map['brand'], - device: map['device'], - display: map['display'], - fingerprint: map['fingerprint'], - hardware: map['hardware'], - host: map['host'], - id: map['id'], - manufacturer: map['manufacturer'], - model: map['model'], - product: map['product'], - supported32BitAbis: _fromList(map['supported32BitAbis'] ?? []), - supported64BitAbis: _fromList(map['supported64BitAbis'] ?? []), - supportedAbis: _fromList(map['supportedAbis'] ?? []), - tags: map['tags'], - type: map['type'], - isPhysicalDevice: map['isPhysicalDevice'], - androidId: map['androidId'], - systemFeatures: _fromList(map['systemFeatures'] ?? []), + version: AndroidBuildVersion._fromMap(map['version'] != null + ? map['version'].cast() + : {}), + board: map['board'] ?? '', + bootloader: map['bootloader'] ?? '', + brand: map['brand'] ?? '', + device: map['device'] ?? '', + display: map['display'] ?? '', + fingerprint: map['fingerprint'] ?? '', + hardware: map['hardware'] ?? '', + host: map['host'] ?? '', + id: map['id'] ?? '', + manufacturer: map['manufacturer'] ?? '', + model: map['model'] ?? '', + product: map['product'] ?? '', + supported32BitAbis: _fromList(map['supported32BitAbis']), + supported64BitAbis: _fromList(map['supported64BitAbis']), + supportedAbis: _fromList(map['supportedAbis']), + tags: map['tags'] ?? '', + type: map['type'] ?? '', + isPhysicalDevice: map['isPhysicalDevice'] ?? false, + androidId: map['androidId'] ?? '', + systemFeatures: _fromList(map['systemFeatures']), ); } /// Deserializes message as List static List _fromList(dynamic message) { - final List list = message; - return List.from(list); + if (message == null) { + return []; + } + assert(message is List); + final List list = List.from(message) + ..removeWhere((value) => value == null); + return list.cast(); } } @@ -152,47 +190,58 @@ class AndroidDeviceInfo { class AndroidBuildVersion { AndroidBuildVersion._({ this.baseOS, - this.codename, - this.incremental, this.previewSdkInt, - this.release, - this.sdkInt, this.securityPatch, + required this.codename, + required this.incremental, + required this.release, + required this.sdkInt, }); /// The base OS build the product is based on. - final String baseOS; + /// This is only available on Android 6.0 or above. + String? baseOS; + + /// The developer preview revision of a prerelease SDK. + /// This is only available on Android 6.0 or above. + int? previewSdkInt; + + /// The user-visible security patch level. + /// This is only available on Android 6.0 or above. + final String? securityPatch; /// The current development codename, or the string "REL" if this is a release build. + /// + /// The value is an empty String if it is not available. final String codename; /// The internal value used by the underlying source control to represent this build. + /// + /// The value is an empty String if it is not available. final String incremental; - /// The developer preview revision of a prerelease SDK. - final int previewSdkInt; - /// The user-visible version string. + /// + /// The value is an empty String if it is not available. final String release; /// The user-visible SDK version of the framework. /// /// Possible values are defined in: https://developer.android.com/reference/android/os/Build.VERSION_CODES.html + /// + /// The value is -1 if it is unavailable. final int sdkInt; - /// The user-visible security patch level. - final String securityPatch; - /// Deserializes from the map message received from [_kChannel]. static AndroidBuildVersion _fromMap(Map map) { return AndroidBuildVersion._( baseOS: map['baseOS'], - codename: map['codename'], - incremental: map['incremental'], previewSdkInt: map['previewSdkInt'], - release: map['release'], - sdkInt: map['sdkInt'], securityPatch: map['securityPatch'], + codename: map['codename'] ?? '', + incremental: map['incremental'] ?? '', + release: map['release'] ?? '', + sdkInt: map['sdkInt'] ?? -1, ); } } diff --git a/packages/device_info/device_info_platform_interface/lib/model/ios_device_info.dart b/packages/device_info/device_info_platform_interface/lib/model/ios_device_info.dart index d41202492101..17c96a3e250a 100644 --- a/packages/device_info/device_info_platform_interface/lib/model/ios_device_info.dart +++ b/packages/device_info/device_info_platform_interface/lib/model/ios_device_info.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -8,52 +8,71 @@ class IosDeviceInfo { /// IOS device info class. IosDeviceInfo({ - this.name, - this.systemName, - this.systemVersion, - this.model, - this.localizedModel, - this.identifierForVendor, - this.isPhysicalDevice, - this.utsname, + required this.name, + required this.systemName, + required this.systemVersion, + required this.model, + required this.localizedModel, + required this.identifierForVendor, + required this.isPhysicalDevice, + required this.utsname, }); /// Device name. + /// + /// The value is an empty String if it is not available. final String name; /// The name of the current operating system. + /// + /// The value is an empty String if it is not available. final String systemName; /// The current operating system version. + /// + /// The value is an empty String if it is not available. final String systemVersion; /// Device model. + /// + /// The value is an empty String if it is not available. final String model; /// Localized name of the device model. + /// + /// The value is an empty String if it is not available. final String localizedModel; /// Unique UUID value identifying the current device. + /// + /// The value is an empty String if it is not available. final String identifierForVendor; - /// `false` if the application is running in a simulator, `true` otherwise. + /// The value is `true` if the application is running on a physical device. + /// + /// The value is `false` when the application is running on a simulator, or the value is unavailable. final bool isPhysicalDevice; /// Operating system information derived from `sys/utsname.h`. + /// + /// The value is an empty String if it is not available. final IosUtsname utsname; /// Deserializes from the map message received from [_kChannel]. static IosDeviceInfo fromMap(Map map) { return IosDeviceInfo( - name: map['name'], - systemName: map['systemName'], - systemVersion: map['systemVersion'], - model: map['model'], - localizedModel: map['localizedModel'], - identifierForVendor: map['identifierForVendor'], - isPhysicalDevice: map['isPhysicalDevice'] == 'true', - utsname: - IosUtsname._fromMap(map['utsname']?.cast() ?? {}), + name: map['name'] ?? '', + systemName: map['systemName'] ?? '', + systemVersion: map['systemVersion'] ?? '', + model: map['model'] ?? '', + localizedModel: map['localizedModel'] ?? '', + identifierForVendor: map['identifierForVendor'] ?? '', + isPhysicalDevice: map['isPhysicalDevice'] != null + ? map['isPhysicalDevice'] == 'true' + : false, + utsname: IosUtsname._fromMap(map['utsname'] != null + ? map['utsname'].cast() + : {}), ); } } @@ -62,36 +81,46 @@ class IosDeviceInfo { /// See http://pubs.opengroup.org/onlinepubs/7908799/xsh/sysutsname.h.html for details. class IosUtsname { IosUtsname._({ - this.sysname, - this.nodename, - this.release, - this.version, - this.machine, + required this.sysname, + required this.nodename, + required this.release, + required this.version, + required this.machine, }); /// Operating system name. + /// + /// The value is an empty String if it is not available. final String sysname; /// Network node name. + /// + /// The value is an empty String if it is not available. final String nodename; /// Release level. + /// + /// The value is an empty String if it is not available. final String release; /// Version level. + /// + /// The value is an empty String if it is not available. final String version; /// Hardware type (e.g. 'iPhone7,1' for iPhone 6 Plus). + /// + /// The value is an empty String if it is not available. final String machine; /// Deserializes from the map message received from [_kChannel]. static IosUtsname _fromMap(Map map) { return IosUtsname._( - sysname: map['sysname'], - nodename: map['nodename'], - release: map['release'], - version: map['version'], - machine: map['machine'], + sysname: map['sysname'] ?? '', + nodename: map['nodename'] ?? '', + release: map['release'] ?? '', + version: map['version'] ?? '', + machine: map['machine'] ?? '', ); } } diff --git a/packages/device_info/device_info_platform_interface/pubspec.yaml b/packages/device_info/device_info_platform_interface/pubspec.yaml index 656e5b24c373..cf3e50f98422 100644 --- a/packages/device_info/device_info_platform_interface/pubspec.yaml +++ b/packages/device_info/device_info_platform_interface/pubspec.yaml @@ -1,22 +1,23 @@ name: device_info_platform_interface description: A common platform interface for the device_info plugin. -homepage: https://github.com/flutter/plugins/tree/master/packages/device_info +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: 1.0.1 +version: 2.0.1 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.9.1+hotfix.4" dependencies: flutter: sdk: flutter - meta: ^1.1.8 - plugin_platform_interface: ^1.0.2 + meta: ^1.3.0 + plugin_platform_interface: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - mockito: ^4.1.1 - pedantic: ^1.8.0 - -environment: - sdk: ">=2.7.0 <3.0.0" - flutter: ">=1.9.1+hotfix.4 <2.0.0" + 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 index 1da52e2cf39f..14ed7c0aefb4 100644 --- a/packages/device_info/device_info_platform_interface/test/method_channel_device_info_test.dart +++ b/packages/device_info/device_info_platform_interface/test/method_channel_device_info_test.dart @@ -1,19 +1,17 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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", () { - MethodChannelDeviceInfo methodChannelDeviceInfo; + late MethodChannelDeviceInfo methodChannelDeviceInfo; setUp(() async { methodChannelDeviceInfo = MethodChannelDeviceInfo(); @@ -23,11 +21,68 @@ void main() { switch (methodCall.method) { case 'getAndroidDeviceInfo': return ({ - "brand": "Google", + "version": { + "securityPatch": "2018-09-05", + "sdkInt": 28, + "release": "9", + "previewSdkInt": 0, + "incremental": "5124027", + "codename": "REL", + "baseOS": "", + }, + "board": "goldfish_x86_64", + "bootloader": "unknown", + "brand": "google", + "device": "generic_x86_64", + "display": "PSR1.180720.075", + "fingerprint": + "google/sdk_gphone_x86_64/generic_x86_64:9/PSR1.180720.075/5124027:user/release-keys", + "hardware": "ranchu", + "host": "abfarm730", + "id": "PSR1.180720.075", + "manufacturer": "Google", + "model": "Android SDK built for x86_64", + "product": "sdk_gphone_x86_64", + "supported32BitAbis": [ + "x86", + ], + "supported64BitAbis": [ + "x86_64", + ], + "supportedAbis": [ + "x86_64", + "x86", + ], + "tags": "release-keys", + "type": "user", + "isPhysicalDevice": false, + "androidId": "f47571f3b4648f45", + "systemFeatures": [ + "android.hardware.sensor.proximity", + "android.software.adoptable_storage", + "android.hardware.sensor.accelerometer", + "android.hardware.faketouch", + "android.software.backup", + "android.hardware.touchscreen", + ], }); case 'getIosDeviceInfo': return ({ - "name": "iPhone 10", + "name": "iPhone 13", + "systemName": "iOS", + "systemVersion": "13.0", + "model": "iPhone", + "localizedModel": "iPhone", + "identifierForVendor": "88F59280-55AD-402C-B922-3203B4794C06", + "isPhysicalDevice": false, + "utsname": { + "sysname": "Darwin", + "nodename": "host", + "release": "19.6.0", + "version": + "Darwin Kernel Version 19.6.0: Thu Jun 18 20:49:00 PDT 2020; root:xnu-6153.141.1~1/RELEASE_X86_64", + "machine": "x86_64", + } }); default: return null; @@ -38,12 +93,281 @@ void main() { test("androidInfo", () async { final AndroidDeviceInfo result = await methodChannelDeviceInfo.androidInfo(); - expect(result.brand, "Google"); + + expect(result.version.securityPatch, "2018-09-05"); + expect(result.version.sdkInt, 28); + expect(result.version.release, "9"); + expect(result.version.previewSdkInt, 0); + expect(result.version.incremental, "5124027"); + expect(result.version.codename, "REL"); + expect(result.board, "goldfish_x86_64"); + expect(result.bootloader, "unknown"); + expect(result.brand, "google"); + expect(result.device, "generic_x86_64"); + expect(result.display, "PSR1.180720.075"); + expect(result.fingerprint, + "google/sdk_gphone_x86_64/generic_x86_64:9/PSR1.180720.075/5124027:user/release-keys"); + expect(result.hardware, "ranchu"); + expect(result.host, "abfarm730"); + expect(result.id, "PSR1.180720.075"); + expect(result.manufacturer, "Google"); + expect(result.model, "Android SDK built for x86_64"); + expect(result.product, "sdk_gphone_x86_64"); + expect(result.supported32BitAbis, [ + "x86", + ]); + expect(result.supported64BitAbis, [ + "x86_64", + ]); + expect(result.supportedAbis, [ + "x86_64", + "x86", + ]); + expect(result.tags, "release-keys"); + expect(result.type, "user"); + expect(result.isPhysicalDevice, false); + expect(result.androidId, "f47571f3b4648f45"); + expect(result.systemFeatures, [ + "android.hardware.sensor.proximity", + "android.software.adoptable_storage", + "android.hardware.sensor.accelerometer", + "android.hardware.faketouch", + "android.software.backup", + "android.hardware.touchscreen", + ]); }); test("iosInfo", () async { final IosDeviceInfo result = await methodChannelDeviceInfo.iosInfo(); - expect(result.name, "iPhone 10"); + expect(result.name, "iPhone 13"); + expect(result.systemName, "iOS"); + expect(result.systemVersion, "13.0"); + expect(result.model, "iPhone"); + expect(result.localizedModel, "iPhone"); + expect( + result.identifierForVendor, "88F59280-55AD-402C-B922-3203B4794C06"); + expect(result.isPhysicalDevice, false); + expect(result.utsname.sysname, "Darwin"); + expect(result.utsname.nodename, "host"); + expect(result.utsname.release, "19.6.0"); + expect(result.utsname.version, + "Darwin Kernel Version 19.6.0: Thu Jun 18 20:49:00 PDT 2020; root:xnu-6153.141.1~1/RELEASE_X86_64"); + expect(result.utsname.machine, "x86_64"); + }); + }); + + group( + "$MethodChannelDeviceInfo handles null value in the map returned from method channel", + () { + 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/analysis_options.yaml b/packages/e2e/analysis_options.yaml new file mode 100644 index 000000000000..cda4f6e153e6 --- /dev/null +++ b/packages/e2e/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options_legacy.yaml diff --git a/packages/espresso/AUTHORS b/packages/espresso/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/espresso/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/espresso/CHANGELOG.md b/packages/espresso/CHANGELOG.md index edc6bd6d2ba1..4699db18c579 100644 --- a/packages/espresso/CHANGELOG.md +++ b/packages/espresso/CHANGELOG.md @@ -1,3 +1,28 @@ +## 0.1.0+2 + +* Migrate maven repo from jcenter to mavenCentral + +## 0.1.0+1 + +* Minor code cleanup +* Package metadata updates + +## 0.1.0 + +* Update SDK requirement for null-safety compatibility. + +## 0.0.1+9 + +* Update Flutter SDK constraint. + +## 0.0.1+8 + +* Android: Handle deprecation & unchecked warning as error. + +## 0.0.1+7 + +* Update android compileSdkVersion to 29. + ## 0.0.1+6 * Keep handling deprecated Android v1 classes for backward compatibility. diff --git a/packages/espresso/LICENSE b/packages/espresso/LICENSE index 507569823f1b..c6823b81eb84 100644 --- a/packages/espresso/LICENSE +++ b/packages/espresso/LICENSE @@ -1,4 +1,4 @@ -Copyright 2019 The Chromium Authors. All rights reserved. +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: diff --git a/packages/espresso/android/build.gradle b/packages/espresso/android/build.gradle index 4af1d3e8b67f..74988a50a3b9 100644 --- a/packages/espresso/android/build.gradle +++ b/packages/espresso/android/build.gradle @@ -4,7 +4,7 @@ version '1.0' buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -15,14 +15,14 @@ buildscript { rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } apply plugin: 'com.android.library' android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { minSdkVersion 16 diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/EspressoFlutter.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/EspressoFlutter.java index 106436f2b9ce..3ba1762117c3 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/EspressoFlutter.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/EspressoFlutter.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -130,6 +130,7 @@ public WidgetInteraction check(@Nonnull WidgetAssertion assertion) { return this; } + @SuppressWarnings("unchecked") private T performInternal(FlutterAction flutterAction) { checkNotNull( flutterAction, diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/ActionUtil.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/ActionUtil.java index 7dcb05b41724..73f8c111b6cf 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/ActionUtil.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/ActionUtil.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/action/ClickAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/ClickAction.java index 5da56fd402ad..d2e251e887e3 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/ClickAction.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/ClickAction.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterActions.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterActions.java index 258daf67a66e..2f0c171e780d 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterActions.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterActions.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterScrollToAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterScrollToAction.java index b97252a2306e..04692155fc80 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterScrollToAction.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterScrollToAction.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterTypeTextAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterTypeTextAction.java index bb62250eefcb..3de8aec56622 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterTypeTextAction.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterTypeTextAction.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterViewAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterViewAction.java index 7864b43d9ec0..7031915f1ca1 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterViewAction.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/FlutterViewAction.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -32,7 +32,7 @@ import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.SettableFuture; import io.flutter.embedding.android.FlutterView; -import io.flutter.view.FlutterNativeView; +import io.flutter.embedding.engine.FlutterJNI; import java.net.URI; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -105,7 +105,7 @@ public void perform(UiController uiController, View flutterView) { // The url {@code FlutterNativeView} returns is the http url that the Dart VM Observatory http // server serves at. Need to convert to the one that the WebSocket uses. URI dartVmServiceProtocolUrl = - DartVmServiceUtil.getServiceProtocolUri(FlutterNativeView.getObservatoryUri()); + DartVmServiceUtil.getServiceProtocolUri(FlutterJNI.getObservatoryUri()); String isolateId = DartVmServiceUtil.getDartIsolateId(flutterView); final FlutterTestingProtocol flutterTestingProtocol = new DartVmService( @@ -199,6 +199,7 @@ public String getName() { return FlutterViewRenderedIdlingResource.class.getSimpleName(); } + @SuppressWarnings("deprecation") @Override public boolean isIdleNow() { boolean isIdle = false; diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/SyntheticClickAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/SyntheticClickAction.java index 5036be1fd290..fa238cbe76c0 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/SyntheticClickAction.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/SyntheticClickAction.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/action/WaitUntilIdleAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/WaitUntilIdleAction.java index b83e29b7e582..a4c2c95bade4 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/WaitUntilIdleAction.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/WaitUntilIdleAction.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/action/WidgetCoordinatesCalculator.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/WidgetCoordinatesCalculator.java index 8d541ae823ee..13de56e5a616 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/WidgetCoordinatesCalculator.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/WidgetCoordinatesCalculator.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/action/WidgetInfoFetcher.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/WidgetInfoFetcher.java index 90d494e0b8ea..d922b1fb33ae 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/WidgetInfoFetcher.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/action/WidgetInfoFetcher.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/api/FlutterAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/FlutterAction.java index 24b264c00a27..71e851d2a959 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/FlutterAction.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/FlutterAction.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/api/FlutterTestingProtocol.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/FlutterTestingProtocol.java index d01aaf5fdc09..68429bfbdcd0 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/FlutterTestingProtocol.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/FlutterTestingProtocol.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/api/SyntheticAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/SyntheticAction.java index aed0c4bc7570..41af3e99dfda 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/SyntheticAction.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/SyntheticAction.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/api/WidgetAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/WidgetAction.java index e49d3ef2bb0f..012066cc3f80 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/WidgetAction.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/WidgetAction.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/api/WidgetAssertion.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/WidgetAssertion.java index 313dd2672336..9cd36f1df363 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/WidgetAssertion.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/WidgetAssertion.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/api/WidgetMatcher.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/WidgetMatcher.java index 9f47e0bbeee6..5c983c118ede 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/WidgetMatcher.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/api/WidgetMatcher.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/assertion/FlutterAssertions.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/assertion/FlutterAssertions.java index 63ec0f6f6fdc..0a6b2b791545 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/assertion/FlutterAssertions.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/assertion/FlutterAssertions.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/assertion/FlutterViewAssertion.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/assertion/FlutterViewAssertion.java index 5f4697de947a..1233e9f35edf 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/assertion/FlutterViewAssertion.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/assertion/FlutterViewAssertion.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/common/Constants.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/common/Constants.java index c47f8df1e34d..359d50ae4fba 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/common/Constants.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/common/Constants.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/common/Duration.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/common/Duration.java index d620153fc2f5..086ee47ad52c 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/common/Duration.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/common/Duration.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/exception/AmbiguousWidgetMatcherException.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/exception/AmbiguousWidgetMatcherException.java index 24d495f74945..c0f1a06f5733 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/exception/AmbiguousWidgetMatcherException.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/exception/AmbiguousWidgetMatcherException.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/exception/InvalidFlutterViewException.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/exception/InvalidFlutterViewException.java index ca69e39802d0..d2d32869dd66 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/exception/InvalidFlutterViewException.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/exception/InvalidFlutterViewException.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/exception/NoMatchingWidgetException.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/exception/NoMatchingWidgetException.java index 49c949a07c8a..756710f790c5 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/exception/NoMatchingWidgetException.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/exception/NoMatchingWidgetException.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/idgenerator/IdException.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/idgenerator/IdException.java index 1a3666ec24e1..94c2d86db922 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/idgenerator/IdException.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/idgenerator/IdException.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/idgenerator/IdGenerator.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/idgenerator/IdGenerator.java index b69d8f61aa4f..23d02373e856 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/idgenerator/IdGenerator.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/idgenerator/IdGenerator.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/idgenerator/IdGenerators.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/idgenerator/IdGenerators.java index f8f72dc2a37d..d14d8c50eaac 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/idgenerator/IdGenerators.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/idgenerator/IdGenerators.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/JsonRpcClient.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/JsonRpcClient.java index 028a78028406..743c138fbf09 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/JsonRpcClient.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/JsonRpcClient.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/ErrorObject.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/ErrorObject.java index af5c68e574aa..877dffbe9ade 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/ErrorObject.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/ErrorObject.java @@ -1,3 +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. + package androidx.test.espresso.flutter.internal.jsonrpc.message; import com.google.gson.JsonObject; diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcRequest.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcRequest.java index fa033407eabf..09bc7bbfe770 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcRequest.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcRequest.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcResponse.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcResponse.java index f845765a98e5..460aaa48a17c 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcResponse.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/jsonrpc/message/JsonRpcResponse.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmService.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmService.java index da11fcc8c8b6..a1cdd977066c 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -30,7 +30,6 @@ import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; import com.google.gson.JsonObject; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; @@ -360,10 +359,9 @@ boolean isTestingApiRegistered(JsonRpcResponse isolateInfoResp) { isolateId, isolateInfoResp.getError())); return false; } - Iterator extensions = - isolateInfoResp.getResult().get(EXTENSION_RPCS_TAG).getAsJsonArray().iterator(); - while (extensions.hasNext()) { - String extensionApi = extensions.next().getAsString(); + for (JsonElement jsonElement : + isolateInfoResp.getResult().get(EXTENSION_RPCS_TAG).getAsJsonArray()) { + String extensionApi = jsonElement.getAsString(); if (TESTING_EXTENSION_METHOD.equals(extensionApi)) { Log.d( TAG, diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmServiceUtil.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmServiceUtil.java index 2cf41f1f87a7..63c62c4f5046 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmServiceUtil.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/DartVmServiceUtil.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -71,6 +71,7 @@ public static String getDartIsolateId(View flutterView) { } /** Gets the Dart executor for the given {@code flutterView}. */ + @SuppressWarnings("deprecation") public static DartExecutor getDartExecutor(View flutterView) { checkNotNull(flutterView, "The Flutter View instance cannot be null."); // Flutter's embedding is in the phase of rewriting/refactoring. Let's be compatible with both diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/FlutterProtocolException.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/FlutterProtocolException.java index 71cdb26ebf5c..26865a31098f 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/FlutterProtocolException.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/FlutterProtocolException.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetOffsetAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetOffsetAction.java index 9b92f672f356..d668d4a303f7 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetOffsetAction.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetOffsetAction.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetOffsetResponse.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetOffsetResponse.java index 52fcd4ce45ab..a86cccbf1b6d 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetOffsetResponse.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetOffsetResponse.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/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 2fe0d44bfcda..94cac364ddc7 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetWidgetDiagnosticsAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetWidgetDiagnosticsAction.java index 5982ee481ed8..6aa030a1d669 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetWidgetDiagnosticsAction.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetWidgetDiagnosticsAction.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetWidgetDiagnosticsResponse.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetWidgetDiagnosticsResponse.java index 65a456c0939a..b0c8b4246b8a 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetWidgetDiagnosticsResponse.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/GetWidgetDiagnosticsResponse.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/NoPendingFrameCondition.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/NoPendingFrameCondition.java index 7e7739b6a1a0..2051f947f619 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/NoPendingFrameCondition.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/NoPendingFrameCondition.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/NoPendingPlatformMessagesCondition.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/NoPendingPlatformMessagesCondition.java index 8430ee23f92d..9145e5cd4aac 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/NoPendingPlatformMessagesCondition.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/NoPendingPlatformMessagesCondition.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/NoTransientCallbacksCondition.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/NoTransientCallbacksCondition.java index 4548b28b66bd..35aa5385fba9 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/NoTransientCallbacksCondition.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/NoTransientCallbacksCondition.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/WaitCondition.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/WaitCondition.java index 7017e88765f3..868a877bbb1c 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/WaitCondition.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/WaitCondition.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/WaitForConditionAction.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/WaitForConditionAction.java index efbe588828c3..b8ca1846c8c3 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/WaitForConditionAction.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/WaitForConditionAction.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/WidgetInfoFactory.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/WidgetInfoFactory.java index 2353577e5f4b..46269678b97b 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/WidgetInfoFactory.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/internal/protocol/impl/WidgetInfoFactory.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/FlutterMatchers.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/FlutterMatchers.java index 5a272f24bdc0..9db88665f8e7 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/FlutterMatchers.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/FlutterMatchers.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -96,6 +96,7 @@ public void describeTo(Description description) { description.appendText("is a FlutterView"); } + @SuppressWarnings("deprecation") @Override public boolean matchesSafely(View flutterView) { return flutterView instanceof FlutterView diff --git a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/IsDescendantOfMatcher.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/IsDescendantOfMatcher.java index 24a441549624..81c33d9b2fdb 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/IsDescendantOfMatcher.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/IsDescendantOfMatcher.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/IsExistingMatcher.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/IsExistingMatcher.java index 3380d2146b87..f077254be8f6 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/IsExistingMatcher.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/IsExistingMatcher.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithTextMatcher.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithTextMatcher.java index 4b86aed03216..99d630bbb1c4 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithTextMatcher.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithTextMatcher.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithTooltipMatcher.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithTooltipMatcher.java index 27d4314b3039..78c14673a55d 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithTooltipMatcher.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithTooltipMatcher.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithTypeMatcher.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithTypeMatcher.java index 84cf0e03feae..cea0572ed1b6 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithTypeMatcher.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithTypeMatcher.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithValueKeyMatcher.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithValueKeyMatcher.java index 0e3df39be9b8..fba9ec5dc5ac 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithValueKeyMatcher.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/matcher/WithValueKeyMatcher.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/model/WidgetInfo.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/model/WidgetInfo.java index d6394d2052f3..9d8671fbcf2e 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/model/WidgetInfo.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/model/WidgetInfo.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/androidx/test/espresso/flutter/model/WidgetInfoBuilder.java b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/model/WidgetInfoBuilder.java index 53ea8a27cddc..029111a6cb9b 100644 --- a/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/model/WidgetInfoBuilder.java +++ b/packages/espresso/android/src/main/java/androidx/test/espresso/flutter/model/WidgetInfoBuilder.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/android/src/main/java/com/example/espresso/EspressoPlugin.java b/packages/espresso/android/src/main/java/com/example/espresso/EspressoPlugin.java index e1b1e3ac86db..6c8620b3ca14 100644 --- a/packages/espresso/android/src/main/java/com/example/espresso/EspressoPlugin.java +++ b/packages/espresso/android/src/main/java/com/example/espresso/EspressoPlugin.java @@ -1,3 +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. + package com.example.espresso; import androidx.annotation.NonNull; diff --git a/packages/espresso/example/android/app/build.gradle b/packages/espresso/example/android/app/build.gradle index 0be415652fdc..6def13f65898 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 28 + compileSdkVersion 29 lintOptions { disable 'InvalidPackage' @@ -35,7 +35,7 @@ android { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.example.espresso_example" minSdkVersion 16 - targetSdkVersion 28 + targetSdkVersion 29 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/packages/espresso/example/android/app/src/androidTest/java/com/example/MainActivityTest.java b/packages/espresso/example/android/app/src/androidTest/java/com/example/MainActivityTest.java index aaedd6cbd7cb..739d49c1f9b0 100644 --- a/packages/espresso/example/android/app/src/androidTest/java/com/example/MainActivityTest.java +++ b/packages/espresso/example/android/app/src/androidTest/java/com/example/MainActivityTest.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/espresso/example/android/app/src/main/java/com/example/espresso_example/MainActivity.java b/packages/espresso/example/android/app/src/main/java/com/example/espresso_example/MainActivity.java index 413ef9e50448..7b2675e21399 100644 --- a/packages/espresso/example/android/app/src/main/java/com/example/espresso_example/MainActivity.java +++ b/packages/espresso/example/android/app/src/main/java/com/example/espresso_example/MainActivity.java @@ -1,3 +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. + package com.example.espresso_example; import androidx.annotation.NonNull; diff --git a/packages/espresso/example/android/build.gradle b/packages/espresso/example/android/build.gradle index e0d7ae2c11af..456d020f6e2c 100644 --- a/packages/espresso/example/android/build.gradle +++ b/packages/espresso/example/android/build.gradle @@ -1,7 +1,7 @@ buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -12,7 +12,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/packages/espresso/example/ios/Flutter/AppFrameworkInfo.plist b/packages/espresso/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6b4c0f78a785..000000000000 --- a/packages/espresso/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 8.0 - - diff --git a/packages/espresso/example/ios/Flutter/Debug.xcconfig b/packages/espresso/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index e8efba114687..000000000000 --- a/packages/espresso/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/espresso/example/ios/Flutter/Release.xcconfig b/packages/espresso/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index 399e9340e6f6..000000000000 --- a/packages/espresso/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/espresso/example/ios/Runner.xcodeproj/project.pbxproj b/packages/espresso/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 2209e01dfcd6..000000000000 --- a/packages/espresso/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,584 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - B4A70C1E3465B7A2E7ECD8F8 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AE5F32230E1B4F4C17EDB557 /* Pods_Runner.framework */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 02691CEFCB33C0B1CABE7A23 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 09442C04D3DC0049E7725D93 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; - 3EF237100A0BFC444DE6BC97 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - AE5F32230E1B4F4C17EDB557 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - B4A70C1E3465B7A2E7ECD8F8 /* Pods_Runner.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 301432828879F7BDE0943C41 /* Frameworks */ = { - isa = PBXGroup; - children = ( - AE5F32230E1B4F4C17EDB557 /* Pods_Runner.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B80C3931E831B6300D905FE /* App.framework */, - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - E9E5CC94EC52B9D261A44A5E /* Pods */, - 301432828879F7BDE0943C41 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - ); - name = "Supporting Files"; - sourceTree = ""; - }; - E9E5CC94EC52B9D261A44A5E /* Pods */ = { - isa = PBXGroup; - children = ( - 02691CEFCB33C0B1CABE7A23 /* Pods-Runner.debug.xcconfig */, - 3EF237100A0BFC444DE6BC97 /* Pods-Runner.release.xcconfig */, - 09442C04D3DC0049E7725D93 /* Pods-Runner.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 5D7E711796DC6F61E7F1A6AE /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - DC7821945A6EDE472DDF686F /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1020; - ORGANIZATIONNAME = "The Chromium Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - LastSwiftMigration = 1100; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; - }; - 5D7E711796DC6F61E7F1A6AE /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; - DC7821945A6EDE472DDF686F /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.espressoExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.espressoExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.espressoExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - 249021D3217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - 249021D4217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/espresso/example/ios/Runner/AppDelegate.swift b/packages/espresso/example/ios/Runner/AppDelegate.swift deleted file mode 100644 index 70693e4a8c12..000000000000 --- a/packages/espresso/example/ios/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -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/espresso/example/ios/Runner/Info.plist b/packages/espresso/example/ios/Runner/Info.plist deleted file mode 100644 index 96cc992ec974..000000000000 --- a/packages/espresso/example/ios/Runner/Info.plist +++ /dev/null @@ -1,45 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - espresso_example - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/espresso/example/ios/Runner/Runner-Bridging-Header.h b/packages/espresso/example/ios/Runner/Runner-Bridging-Header.h deleted file mode 100644 index 7335fdf9000c..000000000000 --- a/packages/espresso/example/ios/Runner/Runner-Bridging-Header.h +++ /dev/null @@ -1 +0,0 @@ -#import "GeneratedPluginRegistrant.h" \ No newline at end of file diff --git a/packages/espresso/example/lib/main.dart b/packages/espresso/example/lib/main.dart index c74423f507e8..14f94abb28c8 100644 --- a/packages/espresso/example/lib/main.dart +++ b/packages/espresso/example/lib/main.dart @@ -1,3 +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:flutter/material.dart'; void main() => runApp(MyApp()); @@ -21,13 +25,13 @@ class MyApp extends StatelessWidget { // is not restarted. primarySwatch: Colors.blue, ), - home: _MyHomePage(title: 'Flutter Demo Home Page'), + home: const _MyHomePage(title: 'Flutter Demo Home Page'), ); } } class _MyHomePage extends StatefulWidget { - _MyHomePage({Key key, this.title}) : super(key: key); + const _MyHomePage({Key? key, required this.title}) : super(key: key); // This widget is the home page of your application. It is stateful, meaning // that it has a State object (defined below) that contains fields that affect @@ -94,7 +98,7 @@ class _MyHomePageState extends State<_MyHomePage> { children: [ Text( 'Button tapped $_counter time${_counter == 1 ? '' : 's'}.', - key: ValueKey('CountText'), + key: const ValueKey('CountText'), ), ], ), @@ -102,7 +106,7 @@ class _MyHomePageState extends State<_MyHomePage> { floatingActionButton: FloatingActionButton( onPressed: _incrementCounter, tooltip: 'Increment', - child: Icon(Icons.add), + child: const Icon(Icons.add), ), // This trailing comma makes auto-formatting nicer for build methods. ); } diff --git a/packages/espresso/example/pubspec.yaml b/packages/espresso/example/pubspec.yaml index d2d73da3c0ae..6a5fcdd466fe 100644 --- a/packages/espresso/example/pubspec.yaml +++ b/packages/espresso/example/pubspec.yaml @@ -1,66 +1,28 @@ name: espresso_example description: Demonstrates how to use the espresso plugin. -publish_to: 'none' +publish_to: none environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: sdk: flutter - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^0.1.2 - dev_dependencies: - flutter_test: - sdk: flutter - flutter_driver: - sdk: flutter - pedantic: ^1.8.0 - espresso: + # When depending on this package from a real application you should use: + # espresso: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + pedantic: ^1.10.0 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/espresso/example/test_driver/example.dart b/packages/espresso/example/test_driver/example.dart index ab74ff550930..2dda52acc729 100644 --- a/packages/espresso/example/test_driver/example.dart +++ b/packages/espresso/example/test_driver/example.dart @@ -1,6 +1,9 @@ -import 'package:flutter_driver/driver_extension.dart'; +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. import 'package:espresso_example/main.dart' as app; +import 'package:flutter_driver/driver_extension.dart'; void main() { enableFlutterDriverExtension(); diff --git a/packages/espresso/ios/.gitignore b/packages/espresso/ios/.gitignore deleted file mode 100644 index aa479fd3ce8a..000000000000 --- a/packages/espresso/ios/.gitignore +++ /dev/null @@ -1,37 +0,0 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -.generated/ - -*.pbxuser -*.mode1v3 -*.mode2v3 -*.perspectivev3 - -!default.pbxuser -!default.mode1v3 -!default.mode2v3 -!default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/Generated.xcconfig -/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/packages/espresso/ios/Classes/EspressoPlugin.h b/packages/espresso/ios/Classes/EspressoPlugin.h deleted file mode 100644 index 5f9761591f72..000000000000 --- a/packages/espresso/ios/Classes/EspressoPlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface EspressoPlugin : NSObject -@end diff --git a/packages/espresso/ios/Classes/EspressoPlugin.m b/packages/espresso/ios/Classes/EspressoPlugin.m deleted file mode 100644 index cb4ef8072cae..000000000000 --- a/packages/espresso/ios/Classes/EspressoPlugin.m +++ /dev/null @@ -1,15 +0,0 @@ -#import "EspressoPlugin.h" - -@implementation EspressoPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - FlutterMethodChannel* channel = - [FlutterMethodChannel methodChannelWithName:@"espresso" - binaryMessenger:[registrar messenger]]; - EspressoPlugin* instance = [[EspressoPlugin alloc] init]; - [registrar addMethodCallDelegate:instance channel:channel]; -} - -- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - result(FlutterMethodNotImplemented); -} -@end diff --git a/packages/espresso/ios/espresso.podspec b/packages/espresso/ios/espresso.podspec deleted file mode 100644 index c9b2d106fd92..000000000000 --- a/packages/espresso/ios/espresso.podspec +++ /dev/null @@ -1,25 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint espresso.podspec' to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'espresso' - s.version = '0.0.1' - s.summary = 'Flutter Espresso' - s.description = <<-DESC -Provides bindings for Espresso tests of Flutter apps. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/espresso' } - s.documentation_url = 'https://pub.dev/packages/espresso' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.platform = :ios, '8.0' - - # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } -end diff --git a/packages/espresso/pubspec.yaml b/packages/espresso/pubspec.yaml index 6eb371337e01..62095df29e78 100644 --- a/packages/espresso/pubspec.yaml +++ b/packages/espresso/pubspec.yaml @@ -1,11 +1,19 @@ name: espresso description: Java classes for testing Flutter apps using Espresso. -version: 0.0.1+6 -homepage: https://github.com/flutter/plugins/espresso +repository: https://github.com/flutter/plugins/tree/master/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+2 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +flutter: + plugin: + platforms: + android: + package: com.example.espresso + pluginClass: EspressoPlugin dependencies: flutter: @@ -14,14 +22,4 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 - -# The following section is specific to Flutter. -flutter: - plugin: - platforms: - android: - package: com.example.espresso - pluginClass: EspressoPlugin - ios: - pluginClass: EspressoPlugin + pedantic: ^1.10.0 diff --git a/packages/file_selector/analysis_options.yaml b/packages/file_selector/analysis_options.yaml new file mode 100644 index 000000000000..cda4f6e153e6 --- /dev/null +++ b/packages/file_selector/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options_legacy.yaml diff --git a/packages/file_selector/file_selector/AUTHORS b/packages/file_selector/file_selector/AUTHORS new file mode 100644 index 000000000000..dbf9d190931b --- /dev/null +++ b/packages/file_selector/file_selector/AUTHORS @@ -0,0 +1,65 @@ +# 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. +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/file_selector/file_selector/CHANGELOG.md b/packages/file_selector/file_selector/CHANGELOG.md new file mode 100644 index 000000000000..2f8a4d0754f1 --- /dev/null +++ b/packages/file_selector/file_selector/CHANGELOG.md @@ -0,0 +1,23 @@ +## 0.8.2 + +* Update platform_plugin_interface version requirement. + +## 0.8.1 + +Endorse the web implementation. + +## 0.8.0 + +Migrate to null safety. + +## 0.7.0+2 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 0.7.0+1 + +* Update Flutter SDK constraint. + +## 0.7.0 + +* Initial Open Source release. diff --git a/packages/file_selector/file_selector/LICENSE b/packages/file_selector/file_selector/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/file_selector/file_selector/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/file_selector/file_selector/README.md b/packages/file_selector/file_selector/README.md new file mode 100644 index 000000000000..22ae7073ca2d --- /dev/null +++ b/packages/file_selector/file_selector/README.md @@ -0,0 +1,36 @@ +# file_selector + +[![pub package](https://img.shields.io/pub/v/file_selector.svg)](https://pub.dartlang.org/packages/file_selector) + +A Flutter plugin that manages files and interactions with file dialogs. + +## Usage +To use this plugin, add `file_selector` as a [dependency in your pubspec.yaml file](https://flutter.dev/platform-plugins/). + +### Examples +Here are small examples that show you how to use the API. +Please also take a look at our [example][example] app. + +#### Open a single file +``` dart +final typeGroup = XTypeGroup(label: 'images', extensions: ['jpg', 'png']); +final file = await openFile(acceptedTypeGroups: [typeGroup]); +``` + +#### Open multiple files at once +``` dart +final typeGroup = XTypeGroup(label: 'images', extensions: ['jpg', 'png']); +final files = await openFiles(acceptedTypeGroups: [typeGroup]); +``` + +#### Saving a file +```dart +final path = await getSavePath(); +final name = "hello_file_selector.txt"; +final data = Uint8List.fromList("Hello World!".codeUnits); +final mimeType = "text/plain"; +final file = XFile.fromData(data, name: name, mimeType: mimeType); +await file.saveTo(path); +``` + +[example]:./example diff --git a/packages/file_selector/file_selector/example/.gitignore b/packages/file_selector/file_selector/example/.gitignore new file mode 100644 index 000000000000..7abd0753cfc3 --- /dev/null +++ b/packages/file_selector/file_selector/example/.gitignore @@ -0,0 +1,48 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Currently only web supported +android/ +ios/ + +# Exceptions to above rules. +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/packages/file_selector/file_selector/example/.metadata b/packages/file_selector/file_selector/example/.metadata new file mode 100644 index 000000000000..897381f2373f --- /dev/null +++ b/packages/file_selector/file_selector/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 7736f3bc90270dcb0480db2ccffbf1d13c28db85 + channel: dev + +project_type: app diff --git a/packages/file_selector/file_selector/example/README.md b/packages/file_selector/file_selector/example/README.md new file mode 100644 index 000000000000..93260dc716b2 --- /dev/null +++ b/packages/file_selector/file_selector/example/README.md @@ -0,0 +1,8 @@ +# file_selector_example + +Demonstrates how to use the file_selector plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.dev/). diff --git a/packages/file_selector/file_selector/example/lib/get_directory_page.dart b/packages/file_selector/file_selector/example/lib/get_directory_page.dart new file mode 100644 index 000000000000..6022559ce40e --- /dev/null +++ b/packages/file_selector/file_selector/example/lib/get_directory_page.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:file_selector/file_selector.dart'; +import 'package:flutter/material.dart'; + +/// Screen that shows an example of getDirectoryPath +class GetDirectoryPage extends StatelessWidget { + void _getDirectoryPath(BuildContext context) async { + final String confirmButtonText = 'Choose'; + final String? directoryPath = await getDirectoryPath( + confirmButtonText: confirmButtonText, + ); + if (directoryPath == null) { + // Operation was canceled by the user. + return; + } + await showDialog( + context: context, + builder: (context) => TextDisplay(directoryPath), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Open a text file"), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: Colors.blue, + onPrimary: Colors.white, + ), + child: Text('Press to ask user to choose a directory'), + onPressed: () => _getDirectoryPath(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog +class TextDisplay extends StatelessWidget { + /// Directory path + final String directoryPath; + + /// Default Constructor + TextDisplay(this.directoryPath); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text('Selected Directory'), + content: Scrollbar( + child: SingleChildScrollView( + child: Text(directoryPath), + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () => Navigator.pop(context), + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector/example/lib/home_page.dart b/packages/file_selector/file_selector/example/lib/home_page.dart new file mode 100644 index 000000000000..ab0b5c32187c --- /dev/null +++ b/packages/file_selector/file_selector/example/lib/home_page.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:flutter/material.dart'; + +/// Home Page of the application +class HomePage extends StatelessWidget { + @override + Widget build(BuildContext context) { + final ButtonStyle style = ElevatedButton.styleFrom( + primary: Colors.blue, + onPrimary: Colors.white, + ); + return Scaffold( + appBar: AppBar( + title: Text('File Selector Demo Home Page'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: style, + child: Text('Open a text file'), + onPressed: () => Navigator.pushNamed(context, '/open/text'), + ), + SizedBox(height: 10), + ElevatedButton( + style: style, + child: Text('Open an image'), + onPressed: () => Navigator.pushNamed(context, '/open/image'), + ), + SizedBox(height: 10), + ElevatedButton( + style: style, + child: Text('Open multiple images'), + onPressed: () => Navigator.pushNamed(context, '/open/images'), + ), + SizedBox(height: 10), + ElevatedButton( + style: style, + child: Text('Save a file'), + onPressed: () => Navigator.pushNamed(context, '/save/text'), + ), + SizedBox(height: 10), + ElevatedButton( + style: style, + child: Text('Open a get directory dialog'), + onPressed: () => Navigator.pushNamed(context, '/directory'), + ), + ], + ), + ), + ); + } +} diff --git a/packages/file_selector/file_selector/example/lib/main.dart b/packages/file_selector/file_selector/example/lib/main.dart new file mode 100644 index 000000000000..bd4c9b7ab853 --- /dev/null +++ b/packages/file_selector/file_selector/example/lib/main.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/material.dart'; +import 'package:example/home_page.dart'; +import 'package:example/get_directory_page.dart'; +import 'package:example/open_text_page.dart'; +import 'package:example/open_image_page.dart'; +import 'package:example/open_multiple_images_page.dart'; +import 'package:example/save_text_page.dart'; + +void main() { + runApp(MyApp()); +} + +/// MyApp is the Main Application +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'File Selector Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + home: HomePage(), + routes: { + '/open/image': (context) => OpenImagePage(), + '/open/images': (context) => OpenMultipleImagesPage(), + '/open/text': (context) => OpenTextPage(), + '/save/text': (context) => SaveTextPage(), + '/directory': (context) => GetDirectoryPage(), + }, + ); + } +} diff --git a/packages/file_selector/file_selector/example/lib/open_image_page.dart b/packages/file_selector/file_selector/example/lib/open_image_page.dart new file mode 100644 index 000000000000..ae0d5ad4eefb --- /dev/null +++ b/packages/file_selector/file_selector/example/lib/open_image_page.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 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:file_selector/file_selector.dart'; +import 'package:flutter/material.dart'; + +/// Screen that shows an example of openFiles +class OpenImagePage extends StatelessWidget { + void _openImageFile(BuildContext context) async { + final XTypeGroup typeGroup = XTypeGroup( + label: 'images', + extensions: ['jpg', 'png'], + ); + final List files = await openFiles(acceptedTypeGroups: [typeGroup]); + if (files.isEmpty) { + // Operation was canceled by the user. + return; + } + final XFile file = files[0]; + final String fileName = file.name; + final String filePath = file.path; + + await showDialog( + context: context, + builder: (context) => ImageDisplay(fileName, filePath), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Open an image"), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: Colors.blue, + onPrimary: Colors.white, + ), + child: Text('Press to open an image file(png, jpg)'), + onPressed: () => _openImageFile(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog +class ImageDisplay extends StatelessWidget { + /// Image's name + final String fileName; + + /// Image's path + final String filePath; + + /// Default Constructor + ImageDisplay(this.fileName, this.filePath); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(fileName), + // On web the filePath is a blob url + // while on other platforms it is a system path. + content: kIsWeb ? Image.network(filePath) : Image.file(File(filePath)), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector/example/lib/open_multiple_images_page.dart b/packages/file_selector/file_selector/example/lib/open_multiple_images_page.dart new file mode 100644 index 000000000000..5bf080eff450 --- /dev/null +++ b/packages/file_selector/file_selector/example/lib/open_multiple_images_page.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 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:file_selector/file_selector.dart'; +import 'package:flutter/material.dart'; + +/// Screen that shows an example of openFiles +class OpenMultipleImagesPage extends StatelessWidget { + void _openImageFile(BuildContext context) async { + final XTypeGroup jpgsTypeGroup = XTypeGroup( + label: 'JPEGs', + extensions: ['jpg', 'jpeg'], + ); + final XTypeGroup pngTypeGroup = XTypeGroup( + label: 'PNGs', + extensions: ['png'], + ); + final List files = await openFiles(acceptedTypeGroups: [ + jpgsTypeGroup, + pngTypeGroup, + ]); + if (files.isEmpty) { + // Operation was canceled by the user. + return; + } + await showDialog( + context: context, + builder: (context) => MultipleImagesDisplay(files), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Open multiple images"), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: Colors.blue, + onPrimary: Colors.white, + ), + child: Text('Press to open multiple images (png, jpg)'), + onPressed: () => _openImageFile(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog +class MultipleImagesDisplay extends StatelessWidget { + /// The files containing the images + final List files; + + /// Default Constructor + MultipleImagesDisplay(this.files); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text('Gallery'), + // On web the filePath is a blob url + // while on other platforms it is a system path. + content: Center( + child: Row( + children: [ + ...files.map( + (file) => Flexible( + child: kIsWeb + ? Image.network(file.path) + : Image.file(File(file.path))), + ) + ], + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector/example/lib/open_text_page.dart b/packages/file_selector/file_selector/example/lib/open_text_page.dart new file mode 100644 index 000000000000..8451378f7baa --- /dev/null +++ b/packages/file_selector/file_selector/example/lib/open_text_page.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:file_selector/file_selector.dart'; +import 'package:flutter/material.dart'; + +/// Screen that shows an example of openFile +class OpenTextPage extends StatelessWidget { + void _openTextFile(BuildContext context) async { + final XTypeGroup typeGroup = XTypeGroup( + label: 'text', + extensions: ['txt', 'json'], + ); + final XFile? file = await openFile(acceptedTypeGroups: [typeGroup]); + if (file == null) { + // Operation was canceled by the user. + return; + } + final String fileName = file.name; + final String fileContent = await file.readAsString(); + + await showDialog( + context: context, + builder: (context) => TextDisplay(fileName, fileContent), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Open a text file"), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: Colors.blue, + onPrimary: Colors.white, + ), + child: Text('Press to open a text file (json, txt)'), + onPressed: () => _openTextFile(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog +class TextDisplay extends StatelessWidget { + /// File's name + final String fileName; + + /// File to display + final String fileContent; + + /// Default Constructor + TextDisplay(this.fileName, this.fileContent); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(fileName), + content: Scrollbar( + child: SingleChildScrollView( + child: Text(fileContent), + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () => Navigator.pop(context), + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector/example/lib/save_text_page.dart b/packages/file_selector/file_selector/example/lib/save_text_page.dart new file mode 100644 index 000000000000..1610fc05164d --- /dev/null +++ b/packages/file_selector/file_selector/example/lib/save_text_page.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 'dart:typed_data'; +import 'package:file_selector/file_selector.dart'; +import 'package:flutter/material.dart'; + +/// Page for showing an example of saving with file_selector +class SaveTextPage extends StatelessWidget { + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _contentController = TextEditingController(); + + void _saveFile() async { + String? path = await getSavePath(); + if (path == null) { + // Operation was canceled by the user. + return; + } + final String text = _contentController.text; + final String fileName = _nameController.text; + final Uint8List fileData = Uint8List.fromList(text.codeUnits); + final String fileMimeType = 'text/plain'; + final XFile textFile = + XFile.fromData(fileData, mimeType: fileMimeType, name: fileName); + await textFile.saveTo(path); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text("Save text into a file"), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 300, + child: TextField( + minLines: 1, + maxLines: 12, + controller: _nameController, + decoration: InputDecoration( + hintText: '(Optional) Suggest File Name', + ), + ), + ), + Container( + width: 300, + child: TextField( + minLines: 1, + maxLines: 12, + controller: _contentController, + decoration: InputDecoration( + hintText: 'Enter File Contents', + ), + ), + ), + SizedBox(height: 10), + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: Colors.blue, + onPrimary: Colors.white, + ), + child: Text('Press to save a text file'), + onPressed: () => _saveFile(), + ), + ], + ), + ), + ); + } +} diff --git a/packages/file_selector/file_selector/example/pubspec.yaml b/packages/file_selector/file_selector/example/pubspec.yaml new file mode 100644 index 000000000000..4987c836ad63 --- /dev/null +++ b/packages/file_selector/file_selector/example/pubspec.yaml @@ -0,0 +1,27 @@ +name: example +description: A new Flutter project. +publish_to: none + +version: 1.0.0+1 + +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 + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/integration_test/example/web/favicon.png b/packages/file_selector/file_selector/example/web/favicon.png similarity index 100% rename from packages/integration_test/example/web/favicon.png rename to packages/file_selector/file_selector/example/web/favicon.png diff --git a/packages/integration_test/example/web/icons/Icon-192.png b/packages/file_selector/file_selector/example/web/icons/Icon-192.png similarity index 100% rename from packages/integration_test/example/web/icons/Icon-192.png rename to packages/file_selector/file_selector/example/web/icons/Icon-192.png diff --git a/packages/integration_test/example/web/icons/Icon-512.png b/packages/file_selector/file_selector/example/web/icons/Icon-512.png similarity index 100% rename from packages/integration_test/example/web/icons/Icon-512.png rename to packages/file_selector/file_selector/example/web/icons/Icon-512.png diff --git a/packages/file_selector/file_selector/example/web/index.html b/packages/file_selector/file_selector/example/web/index.html new file mode 100644 index 000000000000..c6fa1623be95 --- /dev/null +++ b/packages/file_selector/file_selector/example/web/index.html @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + example + + + + + + + + diff --git a/packages/file_selector/file_selector/example/web/manifest.json b/packages/file_selector/file_selector/example/web/manifest.json new file mode 100644 index 000000000000..8c012917dab7 --- /dev/null +++ b/packages/file_selector/file_selector/example/web/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/packages/file_selector/file_selector/lib/file_selector.dart b/packages/file_selector/file_selector/lib/file_selector.dart new file mode 100644 index 000000000000..c2803d60c972 --- /dev/null +++ b/packages/file_selector/file_selector/lib/file_selector.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 'dart:async'; + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; + +export 'package:file_selector_platform_interface/file_selector_platform_interface.dart' + show XFile, XTypeGroup; + +/// Open file dialog for loading files and return a file path +Future openFile({ + List acceptedTypeGroups = const [], + String? initialDirectory, + String? confirmButtonText, +}) { + return FileSelectorPlatform.instance.openFile( + acceptedTypeGroups: acceptedTypeGroups, + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText); +} + +/// Open file dialog for loading files and return a list of file paths +Future> openFiles({ + List acceptedTypeGroups = const [], + String? initialDirectory, + String? confirmButtonText, +}) { + return FileSelectorPlatform.instance.openFiles( + acceptedTypeGroups: acceptedTypeGroups, + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText); +} + +/// Saves File to user's file system +Future getSavePath({ + List acceptedTypeGroups = const [], + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, +}) async { + return FileSelectorPlatform.instance.getSavePath( + acceptedTypeGroups: acceptedTypeGroups, + initialDirectory: initialDirectory, + suggestedName: suggestedName, + confirmButtonText: confirmButtonText); +} + +/// Gets a directory path from a user's file system +Future getDirectoryPath({ + String? initialDirectory, + String? confirmButtonText, +}) async { + return FileSelectorPlatform.instance.getDirectoryPath( + initialDirectory: initialDirectory, confirmButtonText: confirmButtonText); +} diff --git a/packages/file_selector/file_selector/pubspec.yaml b/packages/file_selector/file_selector/pubspec.yaml new file mode 100644 index 000000000000..e1725ef05b93 --- /dev/null +++ b/packages/file_selector/file_selector/pubspec.yaml @@ -0,0 +1,28 @@ +name: file_selector +description: Flutter plugin for opening and saving files. +repository: https://github.com/flutter/plugins/tree/master/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 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +flutter: + plugin: + platforms: + web: + default_package: file_selector_web + +dependencies: + file_selector_platform_interface: ^2.0.0 + file_selector_web: ^0.8.1 + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + pedantic: ^1.10.0 + plugin_platform_interface: ^2.0.0 + test: ^1.16.3 diff --git a/packages/file_selector/file_selector/test/file_selector_test.dart b/packages/file_selector/file_selector/test/file_selector_test.dart new file mode 100644 index 000000000000..6d5e215eeb01 --- /dev/null +++ b/packages/file_selector/file_selector/test/file_selector_test.dart @@ -0,0 +1,337 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:file_selector/file_selector.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:test/fake.dart'; + +void main() { + late FakeFileSelector fakePlatformImplementation; + final initialDirectory = '/home/flutteruser'; + final confirmButtonText = 'Use this profile picture'; + final suggestedName = 'suggested_name'; + final acceptedTypeGroups = [ + XTypeGroup(label: 'documents', mimeTypes: [ + 'application/msword', + 'application/vnd.openxmlformats-officedocument.wordprocessing', + ]), + XTypeGroup(label: 'images', extensions: [ + 'jpg', + 'png', + ]), + ]; + + setUp(() { + fakePlatformImplementation = FakeFileSelector(); + FileSelectorPlatform.instance = fakePlatformImplementation; + }); + + group('openFile', () { + final expectedFile = XFile('path'); + + test('works', () async { + fakePlatformImplementation + ..setExpectations( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + acceptedTypeGroups: acceptedTypeGroups) + ..setFileResponse([expectedFile]); + + final file = await openFile( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + acceptedTypeGroups: acceptedTypeGroups, + ); + + expect(file, expectedFile); + }); + + test('works with no arguments', () async { + fakePlatformImplementation.setFileResponse([expectedFile]); + + final file = await openFile(); + + expect(file, expectedFile); + }); + + test('sets the initial directory', () async { + fakePlatformImplementation + ..setExpectations(initialDirectory: initialDirectory) + ..setFileResponse([expectedFile]); + + final file = await openFile(initialDirectory: initialDirectory); + expect(file, expectedFile); + }); + + test('sets the button confirmation label', () async { + fakePlatformImplementation + ..setExpectations(confirmButtonText: confirmButtonText) + ..setFileResponse([expectedFile]); + + final file = await openFile(confirmButtonText: confirmButtonText); + expect(file, expectedFile); + }); + + test('sets the accepted type groups', () async { + fakePlatformImplementation + ..setExpectations(acceptedTypeGroups: acceptedTypeGroups) + ..setFileResponse([expectedFile]); + + final file = await openFile(acceptedTypeGroups: acceptedTypeGroups); + expect(file, expectedFile); + }); + }); + + group('openFiles', () { + final expectedFiles = [XFile('path')]; + + test('works', () async { + fakePlatformImplementation + ..setExpectations( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + acceptedTypeGroups: acceptedTypeGroups) + ..setFileResponse(expectedFiles); + + final file = await openFiles( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + acceptedTypeGroups: acceptedTypeGroups, + ); + + expect(file, expectedFiles); + }); + + test('works with no arguments', () async { + fakePlatformImplementation.setFileResponse(expectedFiles); + + final files = await openFiles(); + + expect(files, expectedFiles); + }); + + test('sets the initial directory', () async { + fakePlatformImplementation + ..setExpectations(initialDirectory: initialDirectory) + ..setFileResponse(expectedFiles); + + final files = await openFiles(initialDirectory: initialDirectory); + expect(files, expectedFiles); + }); + + test('sets the button confirmation label', () async { + fakePlatformImplementation + ..setExpectations(confirmButtonText: confirmButtonText) + ..setFileResponse(expectedFiles); + + final files = await openFiles(confirmButtonText: confirmButtonText); + expect(files, expectedFiles); + }); + + test('sets the accepted type groups', () async { + fakePlatformImplementation + ..setExpectations(acceptedTypeGroups: acceptedTypeGroups) + ..setFileResponse(expectedFiles); + + final files = await openFiles(acceptedTypeGroups: acceptedTypeGroups); + expect(files, expectedFiles); + }); + }); + + group('getSavePath', () { + final expectedSavePath = '/example/path'; + + test('works', () async { + fakePlatformImplementation + ..setExpectations( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + acceptedTypeGroups: acceptedTypeGroups, + suggestedName: suggestedName) + ..setPathResponse(expectedSavePath); + + final savePath = await getSavePath( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + acceptedTypeGroups: acceptedTypeGroups, + suggestedName: suggestedName, + ); + + expect(savePath, expectedSavePath); + }); + + test('works with no arguments', () async { + fakePlatformImplementation.setPathResponse(expectedSavePath); + + final savePath = await getSavePath(); + expect(savePath, expectedSavePath); + }); + + test('sets the initial directory', () async { + fakePlatformImplementation + ..setExpectations(initialDirectory: initialDirectory) + ..setPathResponse(expectedSavePath); + + final savePath = await getSavePath(initialDirectory: initialDirectory); + expect(savePath, expectedSavePath); + }); + + test('sets the button confirmation label', () async { + fakePlatformImplementation + ..setExpectations(confirmButtonText: confirmButtonText) + ..setPathResponse(expectedSavePath); + + final savePath = await getSavePath(confirmButtonText: confirmButtonText); + expect(savePath, expectedSavePath); + }); + + test('sets the accepted type groups', () async { + fakePlatformImplementation + ..setExpectations(acceptedTypeGroups: acceptedTypeGroups) + ..setPathResponse(expectedSavePath); + + final savePath = + await getSavePath(acceptedTypeGroups: acceptedTypeGroups); + expect(savePath, expectedSavePath); + }); + + test('sets the suggested name', () async { + fakePlatformImplementation + ..setExpectations(suggestedName: suggestedName) + ..setPathResponse(expectedSavePath); + + final savePath = await getSavePath(suggestedName: suggestedName); + expect(savePath, expectedSavePath); + }); + }); + + group('getDirectoryPath', () { + final expectedDirectoryPath = '/example/path'; + + test('works', () async { + fakePlatformImplementation + ..setExpectations( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText) + ..setPathResponse(expectedDirectoryPath); + + final directoryPath = await getDirectoryPath( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + ); + + expect(directoryPath, expectedDirectoryPath); + }); + + test('works with no arguments', () async { + fakePlatformImplementation.setPathResponse(expectedDirectoryPath); + + final directoryPath = await getDirectoryPath(); + expect(directoryPath, expectedDirectoryPath); + }); + + test('sets the initial directory', () async { + fakePlatformImplementation + ..setExpectations(initialDirectory: initialDirectory) + ..setPathResponse(expectedDirectoryPath); + + final directoryPath = + await getDirectoryPath(initialDirectory: initialDirectory); + expect(directoryPath, expectedDirectoryPath); + }); + + test('sets the button confirmation label', () async { + fakePlatformImplementation + ..setExpectations(confirmButtonText: confirmButtonText) + ..setPathResponse(expectedDirectoryPath); + + final directoryPath = + await getDirectoryPath(confirmButtonText: confirmButtonText); + expect(directoryPath, expectedDirectoryPath); + }); + }); +} + +class FakeFileSelector extends Fake + with MockPlatformInterfaceMixin + implements FileSelectorPlatform { + // Expectations. + List? acceptedTypeGroups = const []; + String? initialDirectory; + String? confirmButtonText; + String? suggestedName; + // Return values. + List? files; + String? path; + + void setExpectations({ + List acceptedTypeGroups = const [], + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, + }) { + this.acceptedTypeGroups = acceptedTypeGroups; + this.initialDirectory = initialDirectory; + this.suggestedName = suggestedName; + this.confirmButtonText = confirmButtonText; + } + + void setFileResponse(List files) { + this.files = files; + } + + void setPathResponse(String path) { + this.path = path; + } + + @override + Future openFile({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) async { + expect(acceptedTypeGroups, this.acceptedTypeGroups); + expect(initialDirectory, this.initialDirectory); + expect(suggestedName, this.suggestedName); + return files?[0]; + } + + @override + Future> openFiles({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) async { + expect(acceptedTypeGroups, this.acceptedTypeGroups); + expect(initialDirectory, this.initialDirectory); + expect(suggestedName, this.suggestedName); + return files!; + } + + @override + Future getSavePath({ + List? acceptedTypeGroups, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, + }) async { + expect(acceptedTypeGroups, this.acceptedTypeGroups); + expect(initialDirectory, this.initialDirectory); + expect(suggestedName, this.suggestedName); + expect(confirmButtonText, this.confirmButtonText); + return path; + } + + @override + Future getDirectoryPath({ + String? initialDirectory, + String? confirmButtonText, + }) async { + expect(initialDirectory, this.initialDirectory); + expect(confirmButtonText, this.confirmButtonText); + return path; + } +} diff --git a/packages/file_selector/file_selector_platform_interface/AUTHORS b/packages/file_selector/file_selector_platform_interface/AUTHORS new file mode 100644 index 000000000000..dbf9d190931b --- /dev/null +++ b/packages/file_selector/file_selector_platform_interface/AUTHORS @@ -0,0 +1,65 @@ +# 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. +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/file_selector/file_selector_platform_interface/CHANGELOG.md b/packages/file_selector/file_selector_platform_interface/CHANGELOG.md index 0d8803f93540..ba595addfcaf 100644 --- a/packages/file_selector/file_selector_platform_interface/CHANGELOG.md +++ b/packages/file_selector/file_selector_platform_interface/CHANGELOG.md @@ -1,3 +1,31 @@ +## 2.0.2 + +* Update platform_plugin_interface version requirement. + +## 2.0.1 + +* Replace extensions with leading dots. + +## 2.0.0 + +* Migration to null-safety + +## 1.0.3+1 + +* Bump the [cross_file](https://pub.dev/packages/cross_file) package version. + +## 1.0.3 + +* Update Flutter SDK constraint. + +## 1.0.2 + +* Replace locally defined `XFile` types with the versions from the [cross_file](https://pub.dev/packages/cross_file) package. + +## 1.0.1 + +* Allow type groups that allow any file. + ## 1.0.0 * Initial release. diff --git a/packages/file_selector/file_selector_platform_interface/LICENSE b/packages/file_selector/file_selector_platform_interface/LICENSE index 2c91f1438173..c6823b81eb84 100644 --- a/packages/file_selector/file_selector_platform_interface/LICENSE +++ b/packages/file_selector/file_selector_platform_interface/LICENSE @@ -1,4 +1,4 @@ -Copyright 2020 The Flutter Authors. All rights reserved. +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: @@ -22,4 +22,4 @@ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/file_selector/file_selector_platform_interface/lib/file_selector_platform_interface.dart b/packages/file_selector/file_selector_platform_interface/lib/file_selector_platform_interface.dart index 69e3064150b5..5e9a9fefa0bc 100644 --- a/packages/file_selector/file_selector_platform_interface/lib/file_selector_platform_interface.dart +++ b/packages/file_selector/file_selector_platform_interface/lib/file_selector_platform_interface.dart @@ -1,2 +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. + export 'src/platform_interface/file_selector_interface.dart'; export 'src/types/types.dart'; diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/method_channel/method_channel_file_selector.dart b/packages/file_selector/file_selector_platform_interface/lib/src/method_channel/method_channel_file_selector.dart index 8681a1dbffa6..34017acc90e0 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 @@ -1,7 +1,8 @@ -// Copyright 2020 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:cross_file/cross_file.dart'; import 'package:flutter/services.dart'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; @@ -18,57 +19,57 @@ class MethodChannelFileSelector extends FileSelectorPlatform { /// Load a file from user's computer and return it as an XFile @override - Future openFile({ - List acceptedTypeGroups, - String initialDirectory, - String confirmButtonText, + Future openFile({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, }) async { - final List path = await _channel.invokeListMethod( + final List? path = await _channel.invokeListMethod( 'openFile', { 'acceptedTypeGroups': - acceptedTypeGroups?.map((group) => group.toJSON())?.toList(), + acceptedTypeGroups?.map((group) => group.toJSON()).toList(), 'initialDirectory': initialDirectory, 'confirmButtonText': confirmButtonText, 'multiple': false, }, ); - return path == null ? null : XFile(path?.first); + return path == null ? null : XFile(path.first); } /// Load multiple files from user's computer and return it as an XFile @override Future> openFiles({ - List acceptedTypeGroups, - String initialDirectory, - String confirmButtonText, + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, }) async { - final List pathList = await _channel.invokeListMethod( + final List? pathList = await _channel.invokeListMethod( 'openFile', { 'acceptedTypeGroups': - acceptedTypeGroups?.map((group) => group.toJSON())?.toList(), + acceptedTypeGroups?.map((group) => group.toJSON()).toList(), 'initialDirectory': initialDirectory, 'confirmButtonText': confirmButtonText, 'multiple': true, }, ); - return pathList?.map((path) => XFile(path))?.toList() ?? []; + return pathList?.map((path) => XFile(path)).toList() ?? []; } /// Gets the path from a save dialog @override - Future getSavePath({ - List acceptedTypeGroups, - String initialDirectory, - String suggestedName, - String confirmButtonText, + Future getSavePath({ + List? acceptedTypeGroups, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, }) async { return _channel.invokeMethod( 'getSavePath', { 'acceptedTypeGroups': - acceptedTypeGroups?.map((group) => group.toJSON())?.toList(), + acceptedTypeGroups?.map((group) => group.toJSON()).toList(), 'initialDirectory': initialDirectory, 'suggestedName': suggestedName, 'confirmButtonText': confirmButtonText, @@ -78,9 +79,9 @@ class MethodChannelFileSelector extends FileSelectorPlatform { /// Gets a directory path from a dialog @override - Future getDirectoryPath({ - String initialDirectory, - String confirmButtonText, + Future getDirectoryPath({ + String? initialDirectory, + String? confirmButtonText, }) async { return _channel.invokeMethod( 'getDirectoryPath', diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/platform_interface/file_selector_interface.dart b/packages/file_selector/file_selector_platform_interface/lib/src/platform_interface/file_selector_interface.dart index cf23d5f01eab..54a6557c4428 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 @@ -1,9 +1,10 @@ -// Copyright 2020 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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:cross_file/cross_file.dart'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; @@ -37,37 +38,40 @@ abstract class FileSelectorPlatform extends PlatformInterface { } /// Open file dialog for loading files and return a file path - Future openFile({ - List acceptedTypeGroups, - String initialDirectory, - String confirmButtonText, + /// Returns `null` if user cancels the operation. + Future openFile({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, }) { throw UnimplementedError('openFile() has not been implemented.'); } /// Open file dialog for loading files and return a list of file paths Future> openFiles({ - List acceptedTypeGroups, - String initialDirectory, - String confirmButtonText, + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, }) { throw UnimplementedError('openFiles() has not been implemented.'); } /// Open file dialog for saving files and return a file path at which to save - Future getSavePath({ - List acceptedTypeGroups, - String initialDirectory, - String suggestedName, - String confirmButtonText, + /// Returns `null` if user cancels the operation. + Future getSavePath({ + List? acceptedTypeGroups, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, }) { throw UnimplementedError('getSavePath() has not been implemented.'); } /// Open file dialog for loading directories and return a directory path - Future getDirectoryPath({ - String initialDirectory, - String confirmButtonText, + /// Returns `null` if user cancels the operation. + Future getDirectoryPath({ + String? initialDirectory, + String? confirmButtonText, }) { throw UnimplementedError('getDirectoryPath() has not been implemented.'); } diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/types/types.dart b/packages/file_selector/file_selector_platform_interface/lib/src/types/types.dart index 8848c6751ba3..9caee27c3e35 100644 --- a/packages/file_selector/file_selector_platform_interface/lib/src/types/types.dart +++ b/packages/file_selector/file_selector_platform_interface/lib/src/types/types.dart @@ -1,3 +1,6 @@ -export 'x_file/x_file.dart'; +// Copyright 2013 The Flutter 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:cross_file/cross_file.dart'; export 'x_type_group/x_type_group.dart'; diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/base.dart b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/base.dart deleted file mode 100644 index 7ea050ff28db..000000000000 --- a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/base.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -/// The interface for a XFile. -/// -/// A XFile is a container that wraps the path of a selected -/// file by the user and (in some platforms, like web) the bytes -/// with the contents of the file. -/// -/// This class is a very limited subset of dart:io [File], so all -/// the methods should seem familiar. -abstract class XFileBase { - /// Construct a XFile - XFileBase(String path); - - /// Save the XFile at the indicated file path. - void saveTo(String path) async { - throw UnimplementedError('saveTo has not been implemented.'); - } - - /// Get the path of the picked file. - /// - /// This should only be used as a backwards-compatibility clutch - /// for mobile apps, or cosmetic reasons only (to show the user - /// the path they've picked). - /// - /// Accessing the data contained in the picked file by its path - /// is platform-dependant (and won't work on web), so use the - /// byte getters in the XFile instance instead. - String get path { - throw UnimplementedError('.path has not been implemented.'); - } - - /// The name of the file as it was selected by the user in their device. - /// - /// Use only for cosmetic reasons, do not try to use this as a path. - String get name { - throw UnimplementedError('.name has not been implemented.'); - } - - /// For web, it may be necessary for a file to know its MIME type. - String get mimeType { - throw UnimplementedError('.mimeType has not been implemented.'); - } - - /// Get the length of the file. Returns a `Future` that completes with the length in bytes. - Future length() { - throw UnimplementedError('.length() has not been implemented.'); - } - - /// Synchronously read the entire file contents as a string using the given [Encoding]. - /// - /// By default, `encoding` is [utf8]. - /// - /// Throws Exception if the operation fails. - Future readAsString({Encoding encoding = utf8}) { - throw UnimplementedError('readAsString() has not been implemented.'); - } - - /// Synchronously read the entire file contents as a list of bytes. - /// - /// Throws Exception if the operation fails. - Future readAsBytes() { - throw UnimplementedError('readAsBytes() has not been implemented.'); - } - - /// Create a new independent [Stream] for the contents of this file. - /// - /// If `start` is present, the file will be read from byte-offset `start`. Otherwise from the beginning (index 0). - /// - /// If `end` is present, only up to byte-index `end` will be read. Otherwise, until end of file. - /// - /// In order to make sure that system resources are freed, the stream must be read to completion or the subscription on the stream must be cancelled. - Stream openRead([int start, int end]) { - throw UnimplementedError('openRead() has not been implemented.'); - } - - /// Get the last-modified time for the XFile - Future lastModified() { - throw UnimplementedError('openRead() has not been implemented.'); - } -} diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/html.dart b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/html.dart deleted file mode 100644 index fe898eb4ca62..000000000000 --- a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/html.dart +++ /dev/null @@ -1,132 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:http/http.dart' as http show readBytes; -import 'package:meta/meta.dart'; -import 'dart:html'; - -import '../../web_helpers/web_helpers.dart'; -import './base.dart'; - -/// A XFile that works on web. -/// -/// It wraps the bytes of a selected file. -class XFile extends XFileBase { - String path; - - final String mimeType; - final Uint8List _data; - final int _length; - final String name; - final DateTime _lastModified; - Element _target; - - final XFileTestOverrides _overrides; - - bool get _hasTestOverrides => _overrides != null; - - /// Construct a XFile object from its ObjectUrl. - /// - /// Optionally, this can be initialized with `bytes` and `length` - /// so no http requests are performed to retrieve files later. - /// - /// `name` needs to be passed from the outside, since we only have - /// access to it while we create the ObjectUrl. - XFile( - this.path, { - this.mimeType, - this.name, - int length, - Uint8List bytes, - DateTime lastModified, - @visibleForTesting XFileTestOverrides overrides, - }) : _data = bytes, - _length = length, - _overrides = overrides, - _lastModified = lastModified, - super(path); - - /// Construct an XFile from its data - XFile.fromData( - Uint8List bytes, { - this.mimeType, - this.name, - int length, - DateTime lastModified, - this.path, - @visibleForTesting XFileTestOverrides overrides, - }) : _data = bytes, - _length = length, - _overrides = overrides, - _lastModified = lastModified, - super(path) { - if (path == null) { - final blob = (mimeType == null) ? Blob([bytes]) : Blob([bytes], mimeType); - this.path = Url.createObjectUrl(blob); - } - } - - @override - Future lastModified() async { - if (_lastModified != null) { - return Future.value(_lastModified); - } - return null; - } - - Future get _bytes async { - if (_data != null) { - return Future.value(UnmodifiableUint8ListView(_data)); - } - return http.readBytes(path); - } - - @override - Future length() async { - return _length ?? (await _bytes).length; - } - - @override - Future readAsString({Encoding encoding = utf8}) async { - return encoding.decode(await _bytes); - } - - @override - Future readAsBytes() async { - return Future.value(await _bytes); - } - - @override - Stream openRead([int start, int end]) async* { - final bytes = await _bytes; - yield bytes.sublist(start ?? 0, end ?? bytes.length); - } - - /// Saves the data of this XFile at the location indicated by path. - /// For the web implementation, the path variable is ignored. - void saveTo(String path) async { - // Create a DOM container where we can host the anchor. - _target = ensureInitialized('__x_file_dom_element'); - - // Create an tag with the appropriate download attributes and click it - // May be overridden with XFileTestOverrides - final AnchorElement element = - (_hasTestOverrides && _overrides.createAnchorElement != null) - ? _overrides.createAnchorElement(this.path, this.name) - : createAnchorElement(this.path, this.name); - - // Clear the children in our container so we can add an element to click - _target.children.clear(); - addElementToContainerAndClick(_target, element); - } -} - -/// Overrides some functions to allow testing -@visibleForTesting -class XFileTestOverrides { - /// For overriding the creation of the file input element. - Element Function(String href, String suggestedName) createAnchorElement; - - /// Default constructor for overrides - XFileTestOverrides({this.createAnchorElement}); -} diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/interface.dart b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/interface.dart deleted file mode 100644 index f5fe388e0899..000000000000 --- a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/interface.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'dart:typed_data'; -import 'package:meta/meta.dart'; - -import './base.dart'; - -/// A XFile is a cross-platform, simplified File abstraction. -/// -/// It wraps the bytes of a selected file, and its (platform-dependant) path. -class XFile extends XFileBase { - /// Construct a XFile object from its path. - /// - /// Optionally, this can be initialized with `bytes` and `length` - /// so no http requests are performed to retrieve data later. - /// - /// `name` may be passed from the outside, for those cases where the effective - /// `path` of the file doesn't match what the user sees when selecting it - /// (like in web) - XFile( - String path, { - String mimeType, - String name, - int length, - Uint8List bytes, - DateTime lastModified, - @visibleForTesting XFileTestOverrides overrides, - }) : super(path) { - throw UnimplementedError( - 'XFile is not available in your current platform.'); - } - - /// Construct a XFile object from its data - XFile.fromData( - Uint8List bytes, { - String mimeType, - String name, - int length, - DateTime lastModified, - String path, - @visibleForTesting XFileTestOverrides overrides, - }) : super(path) { - throw UnimplementedError( - 'XFile is not available in your current platform.'); - } -} - -/// Overrides some functions of XFile for testing purposes -@visibleForTesting -class XFileTestOverrides { - /// For overriding the creation of the file input element. - dynamic Function(String href, String suggestedName) createAnchorElement; - - /// Default constructor for overrides - XFileTestOverrides({this.createAnchorElement}); -} diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/io.dart b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/io.dart deleted file mode 100644 index 753732df2811..000000000000 --- a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/io.dart +++ /dev/null @@ -1,111 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; - -import './base.dart'; - -/// A XFile backed by a dart:io File. -class XFile extends XFileBase { - final File _file; - final String mimeType; - final DateTime _lastModified; - int _length; - - final Uint8List _bytes; - - /// Construct a XFile object backed by a dart:io File. - XFile( - String path, { - this.mimeType, - String name, - int length, - Uint8List bytes, - DateTime lastModified, - }) : _file = File(path), - _bytes = null, - _lastModified = lastModified, - super(path); - - /// Construct an XFile from its data - XFile.fromData( - Uint8List bytes, { - this.mimeType, - String path, - String name, - int length, - DateTime lastModified, - }) : _bytes = bytes, - _file = File(path ?? ''), - _length = length, - _lastModified = lastModified, - super(path) { - if (length == null) { - _length = bytes.length; - } - } - - @override - Future lastModified() { - if (_lastModified != null) { - return Future.value(_lastModified); - } - return _file.lastModified(); - } - - @override - void saveTo(String path) async { - File fileToSave = File(path); - await fileToSave.writeAsBytes(_bytes ?? (await readAsBytes())); - await fileToSave.create(); - } - - @override - String get path { - return _file.path; - } - - @override - String get name { - return _file.path.split(Platform.pathSeparator).last; - } - - @override - Future length() { - if (_length != null) { - return Future.value(_length); - } - return _file.length(); - } - - @override - Future readAsString({Encoding encoding = utf8}) { - if (_bytes != null) { - return Future.value(String.fromCharCodes(_bytes)); - } - return _file.readAsString(encoding: encoding); - } - - @override - Future readAsBytes() { - if (_bytes != null) { - return Future.value(_bytes); - } - return _file.readAsBytes(); - } - - Stream _getBytes(int start, int end) async* { - final bytes = _bytes; - yield bytes.sublist(start ?? 0, end ?? bytes.length); - } - - @override - Stream openRead([int start, int end]) { - if (_bytes != null) { - return _getBytes(start, end); - } else { - return _file - .openRead(start ?? 0, end) - .map((chunk) => Uint8List.fromList(chunk)); - } - } -} diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/x_file.dart b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/x_file.dart deleted file mode 100644 index 4545c605875a..000000000000 --- a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_file/x_file.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'interface.dart' - if (dart.library.html) 'html.dart' - if (dart.library.io) 'io.dart'; diff --git a/packages/file_selector/file_selector_platform_interface/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 a7d52a766498..3e3326379610 100644 --- a/packages/file_selector/file_selector_platform_interface/lib/src/types/x_type_group/x_type_group.dart +++ b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_type_group/x_type_group.dart @@ -1,37 +1,35 @@ -// Copyright 2020 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter 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 set of allowed XTypes class XTypeGroup { /// Creates a new group with the given label and file extensions. + /// + /// A group with none of the type options provided indicates that any type is + /// allowed. XTypeGroup({ this.label, - this.extensions, + List? extensions, this.mimeTypes, this.macUTIs, this.webWildCards, - }) : assert( - !((extensions == null || extensions.isEmpty) && - (mimeTypes == null || mimeTypes.isEmpty) && - (macUTIs == null || macUTIs.isEmpty) && - (webWildCards == null || webWildCards.isEmpty)), - "At least one type must be provided for an XTypeGroup."); + }) : this.extensions = _removeLeadingDots(extensions); /// The 'name' or reference to this group of types - final String label; + final String? label; /// The extensions for this group - final List extensions; + final List? extensions; /// The MIME types for this group - final List mimeTypes; + final List? mimeTypes; /// The UTIs for this group - final List macUTIs; + final List? macUTIs; /// The web wild cards for this group (ex: image/*, video/*) - final List webWildCards; + final List? webWildCards; /// Converts this object into a JSON formatted object Map toJSON() { @@ -43,4 +41,7 @@ class XTypeGroup { 'webWildCards': webWildCards, }; } + + static List? _removeLeadingDots(List? exts) => + exts?.map((ext) => ext.startsWith('.') ? ext.substring(1) : ext).toList(); } diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/web_helpers/web_helpers.dart b/packages/file_selector/file_selector_platform_interface/lib/src/web_helpers/web_helpers.dart index 9e40e562bc9a..0f157ed0be5a 100644 --- a/packages/file_selector/file_selector_platform_interface/lib/src/web_helpers/web_helpers.dart +++ b/packages/file_selector/file_selector_platform_interface/lib/src/web_helpers/web_helpers.dart @@ -1,7 +1,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 'dart:html'; /// Create anchor element with download attribute -AnchorElement createAnchorElement(String href, String suggestedName) { +AnchorElement createAnchorElement(String href, String? suggestedName) { final element = AnchorElement(href: href); if (suggestedName == null) { @@ -27,7 +31,7 @@ Element ensureInitialized(String id) { if (target == null) { final Element targetElement = Element.tag('flt-x-file')..id = id; - querySelector('body').children.add(targetElement); + querySelector('body')!.children.add(targetElement); target = targetElement; } return target; diff --git a/packages/file_selector/file_selector_platform_interface/pubspec.yaml b/packages/file_selector/file_selector_platform_interface/pubspec.yaml index 015762a8f5a1..ed0780537a80 100644 --- a/packages/file_selector/file_selector_platform_interface/pubspec.yaml +++ b/packages/file_selector/file_selector_platform_interface/pubspec.yaml @@ -1,24 +1,25 @@ name: file_selector_platform_interface description: A common platform interface for the file_selector plugin. -homepage: https://github.com/flutter/plugins/tree/master/packages/file_selector/file_selector_platform_interface +repository: https://github.com/flutter/plugins/tree/master/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: 1.0.0 +version: 2.0.2 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" dependencies: + cross_file: ^0.3.0 flutter: sdk: flutter - meta: ^1.0.5 - http: ^0.12.0+1 - plugin_platform_interface: ^1.0.1 + http: ^0.13.0 + meta: ^1.3.0 + plugin_platform_interface: ^2.0.0 dev_dependencies: - test: ^1.15.0 flutter_test: sdk: flutter - mockito: ^4.1.1 - pedantic: ^1.8.0 - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.9.1+hotfix.4 <2.0.0" + pedantic: ^1.10.0 + test: ^1.16.3 diff --git a/packages/file_selector/file_selector_platform_interface/test/assets/hello.txt b/packages/file_selector/file_selector_platform_interface/test/assets/hello.txt deleted file mode 100644 index 5dd01c177f5d..000000000000 --- a/packages/file_selector/file_selector_platform_interface/test/assets/hello.txt +++ /dev/null @@ -1 +0,0 @@ -Hello, world! \ No newline at end of file diff --git a/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart b/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart index 6809cee66963..f5b609c93ed3 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 @@ -1,10 +1,8 @@ -// Copyright 2020 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:mockito/mockito.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; import 'package:file_selector_platform_interface/src/method_channel/method_channel_file_selector.dart'; @@ -16,28 +14,10 @@ void main() { isInstanceOf()); }); - test('Cannot be implemented with `implements`', () { - expect(() { - FileSelectorPlatform.instance = ImplementsFileSelectorPlatform(); - }, throwsA(isInstanceOf())); - }); - - test('Can be mocked with `implements`', () { - final FileSelectorPlatformMock mock = FileSelectorPlatformMock(); - FileSelectorPlatform.instance = mock; - }); - test('Can be extended', () { FileSelectorPlatform.instance = ExtendsFileSelectorPlatform(); }); }); } -class FileSelectorPlatformMock extends Mock - with MockPlatformInterfaceMixin - implements FileSelectorPlatform {} - -class ImplementsFileSelectorPlatform extends Mock - implements FileSelectorPlatform {} - class ExtendsFileSelectorPlatform extends FileSelectorPlatform {} diff --git a/packages/file_selector/file_selector_platform_interface/test/method_channel_file_selector_test.dart b/packages/file_selector/file_selector_platform_interface/test/method_channel_file_selector_test.dart index 99f9fe0f0e3b..96a3c2d4f4c9 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 @@ -1,4 +1,4 @@ -// Copyright 2020 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -29,14 +29,14 @@ void main() { test('passes the accepted type groups correctly', () async { final group = XTypeGroup( label: 'text', - extensions: ['.txt'], + extensions: ['txt'], mimeTypes: ['text/plain'], macUTIs: ['public.text'], ); final groupTwo = XTypeGroup( label: 'image', - extensions: ['.jpg'], + extensions: ['jpg'], mimeTypes: ['image/jpg'], macUTIs: ['public.image'], webWildCards: ['image/*']); @@ -90,14 +90,14 @@ void main() { test('passes the accepted type groups correctly', () async { final group = XTypeGroup( label: 'text', - extensions: ['.txt'], + extensions: ['txt'], mimeTypes: ['text/plain'], macUTIs: ['public.text'], ); final groupTwo = XTypeGroup( label: 'image', - extensions: ['.jpg'], + extensions: ['jpg'], mimeTypes: ['image/jpg'], macUTIs: ['public.image'], webWildCards: ['image/*']); @@ -152,14 +152,14 @@ void main() { test('passes the accepted type groups correctly', () async { final group = XTypeGroup( label: 'text', - extensions: ['.txt'], + extensions: ['txt'], mimeTypes: ['text/plain'], macUTIs: ['public.text'], ); final groupTwo = XTypeGroup( label: 'image', - extensions: ['.jpg'], + extensions: ['jpg'], mimeTypes: ['image/jpg'], macUTIs: ['public.image'], webWildCards: ['image/*']); diff --git a/packages/file_selector/file_selector_platform_interface/test/x_file_html_test.dart b/packages/file_selector/file_selector_platform_interface/test/x_file_html_test.dart deleted file mode 100644 index f888a0486ca7..000000000000 --- a/packages/file_selector/file_selector_platform_interface/test/x_file_html_test.dart +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2020 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -@TestOn('chrome') // Uses web-only Flutter SDK - -import 'dart:convert'; -import 'dart:html' as html; -import 'dart:typed_data'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; - -import 'dart:html'; - -final String expectedStringContents = 'Hello, world!'; -final Uint8List bytes = utf8.encode(expectedStringContents); -final html.File textFile = html.File([bytes], 'hello.txt'); -final String textFileUrl = html.Url.createObjectUrl(textFile); - -void main() { - group('Create with an objectUrl', () { - final file = XFile(textFileUrl); - - test('Can be read as a string', () async { - expect(await file.readAsString(), equals(expectedStringContents)); - }); - test('Can be read as bytes', () async { - expect(await file.readAsBytes(), equals(bytes)); - }); - - test('Can be read as a stream', () async { - expect(await file.openRead().first, equals(bytes)); - }); - - test('Stream can be sliced', () async { - expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5))); - }); - }); - - group('Create from data', () { - final file = XFile.fromData(bytes); - - test('Can be read as a string', () async { - expect(await file.readAsString(), equals(expectedStringContents)); - }); - test('Can be read as bytes', () async { - expect(await file.readAsBytes(), equals(bytes)); - }); - - test('Can be read as a stream', () async { - expect(await file.openRead().first, equals(bytes)); - }); - - test('Stream can be sliced', () async { - expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5))); - }); - }); - - group('saveTo(..)', () { - final String xFileDomElementId = '__x_file_dom_element'; - - group('XFile saveTo(..)', () { - test('creates a DOM container', () async { - XFile file = XFile.fromData(bytes); - - await file.saveTo(''); - - final container = querySelector('#${xFileDomElementId}'); - - expect(container, isNotNull); - }); - - test('create anchor element', () async { - XFile file = XFile.fromData(bytes, name: textFile.name); - - await file.saveTo('path'); - - final container = querySelector('#${xFileDomElementId}'); - final AnchorElement element = container?.children?.firstWhere( - (element) => element.tagName == 'A', - orElse: () => null); - - expect(element, isNotNull); - expect(element.href, file.path); - expect(element.download, file.name); - }); - - test('anchor element is clicked', () async { - final mockAnchor = AnchorElement(); - - XFileTestOverrides overrides = XFileTestOverrides( - createAnchorElement: (_, __) => mockAnchor, - ); - - XFile file = - XFile.fromData(bytes, name: textFile.name, overrides: overrides); - - bool clicked = false; - mockAnchor.onClick.listen((event) => clicked = true); - - await file.saveTo('path'); - - expect(clicked, true); - }); - }); - }); -} diff --git a/packages/file_selector/file_selector_platform_interface/test/x_file_io_test.dart b/packages/file_selector/file_selector_platform_interface/test/x_file_io_test.dart deleted file mode 100644 index b669324462b2..000000000000 --- a/packages/file_selector/file_selector_platform_interface/test/x_file_io_test.dart +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright 2020 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -@TestOn('vm') // Uses dart:io - -import 'dart:convert'; -import 'dart:io'; -import 'dart:typed_data'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; - -// Please note that executing this test with command -// `flutter test test/x_file_io_test.dart` will set the directory -// to ./file_selector_platform_interface. -// -// This will cause our hello.txt file to be not be found. Please -// execute this test with `flutter test` or change the path prefix -// to ./test/assets/ -// -// https://github.com/flutter/flutter/issues/20907 - -final pathPrefix = './assets/'; -final path = pathPrefix + 'hello.txt'; -final String expectedStringContents = 'Hello, world!'; -final Uint8List bytes = utf8.encode(expectedStringContents); -final File textFile = File(path); -final String textFilePath = textFile.path; - -void main() { - group('Create with a path', () { - final file = XFile(textFilePath); - - test('Can be read as a string', () async { - expect(await file.readAsString(), equals(expectedStringContents)); - }); - test('Can be read as bytes', () async { - expect(await file.readAsBytes(), equals(bytes)); - }); - - test('Can be read as a stream', () async { - expect(await file.openRead().first, equals(bytes)); - }); - - test('Stream can be sliced', () async { - expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5))); - }); - - test('saveTo(..) creates file', () async { - File removeBeforeTest = File(pathPrefix + 'newFilePath.txt'); - if (removeBeforeTest.existsSync()) { - await removeBeforeTest.delete(); - } - - await file.saveTo(pathPrefix + 'newFilePath.txt'); - File newFile = File(pathPrefix + 'newFilePath.txt'); - - expect(newFile.existsSync(), isTrue); - expect(newFile.readAsStringSync(), 'Hello, world!'); - - await newFile.delete(); - }); - }); - - group('Create with data', () { - final file = XFile.fromData(bytes); - - test('Can be read as a string', () async { - expect(await file.readAsString(), equals(expectedStringContents)); - }); - test('Can be read as bytes', () async { - expect(await file.readAsBytes(), equals(bytes)); - }); - - test('Can be read as a stream', () async { - expect(await file.openRead().first, equals(bytes)); - }); - - test('Stream can be sliced', () async { - expect(await file.openRead(2, 5).first, equals(bytes.sublist(2, 5))); - }); - - test('Function saveTo(..) creates file', () async { - File removeBeforeTest = File(pathPrefix + 'newFileData.txt'); - if (removeBeforeTest.existsSync()) { - await removeBeforeTest.delete(); - } - - await file.saveTo(pathPrefix + 'newFileData.txt'); - File newFile = File(pathPrefix + 'newFileData.txt'); - - expect(newFile.existsSync(), isTrue); - expect(newFile.readAsStringSync(), 'Hello, world!'); - - await newFile.delete(); - }); - }); -} diff --git a/packages/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 877e530cb342..e85a4929e411 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 @@ -1,4 +1,4 @@ -// Copyright 2020 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -7,13 +7,9 @@ import 'package:file_selector_platform_interface/file_selector_platform_interfac void main() { group('XTypeGroup', () { - test('fails assertion with no parameters set', () { - expect(() => XTypeGroup(), throwsAssertionError); - }); - test('toJSON() creates correct map', () { final label = 'test group'; - final extensions = ['.txt', '.jpg']; + final extensions = ['txt', 'jpg']; final mimeTypes = ['text/plain']; final macUTIs = ['public.plain-text']; final webWildCards = ['image/*']; @@ -33,5 +29,24 @@ void main() { expect(jsonMap['macUTIs'], macUTIs); expect(jsonMap['webWildCards'], webWildCards); }); + + test('A wildcard group can be created', () { + final group = XTypeGroup( + label: 'Any', + ); + + final jsonMap = group.toJSON(); + expect(jsonMap['extensions'], null); + expect(jsonMap['mimeTypes'], null); + expect(jsonMap['macUTIs'], null); + expect(jsonMap['webWildCards'], null); + }); + + test('Leading dots are removed from extensions', () { + final extensions = ['.txt', '.jpg']; + final group = XTypeGroup(extensions: extensions); + + expect(group.extensions, ['txt', 'jpg']); + }); }); } diff --git a/packages/file_selector/file_selector_web/AUTHORS b/packages/file_selector/file_selector_web/AUTHORS new file mode 100644 index 000000000000..dbf9d190931b --- /dev/null +++ b/packages/file_selector/file_selector_web/AUTHORS @@ -0,0 +1,65 @@ +# 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. +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/file_selector/file_selector_web/CHANGELOG.md b/packages/file_selector/file_selector_web/CHANGELOG.md new file mode 100644 index 000000000000..3eb7c3b94494 --- /dev/null +++ b/packages/file_selector/file_selector_web/CHANGELOG.md @@ -0,0 +1,16 @@ +# 0.8.1 + +- Return a non-null value from `getSavePath` for consistency with + API expectations that null indicates canceling. + +# 0.8.0 + +- Migrated to null-safety + +# 0.7.0+1 + +- Add dummy `ios` dir, so flutter sdk can be lower than 1.20 + +# 0.7.0 + +- Initial open-source release. diff --git a/packages/file_selector/file_selector_web/LICENSE b/packages/file_selector/file_selector_web/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/file_selector/file_selector_web/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/file_selector/file_selector_web/README.md b/packages/file_selector/file_selector_web/README.md new file mode 100644 index 000000000000..24d48f48586f --- /dev/null +++ b/packages/file_selector/file_selector_web/README.md @@ -0,0 +1,30 @@ +# file_selector_web + +The web implementation of [`file_selector`][1]. + +## Usage + +### Import the package +To use this plugin in your Flutter Web app, simply add it as a dependency in +your pubspec alongside the base `file_selector` plugin. + +_(This is only temporary: in the future we hope to make this package an +"endorsed" implementation of `file_selector`, so that it is automatically +included in your Flutter Web app when you depend on `package:file_selector`.)_ + +This is what the above means to your `pubspec.yaml`: + +```yaml +... +dependencies: + ... + file_selector: ^0.7.0 + file_selector_web: ^0.7.0 + ... +``` + +### Use the plugin +Once you have the `file_selector_web` dependency in your pubspec, you should +be able to use `package:file_selector` as normal. + +[1]: https://pub.dev/packages/file_selector diff --git a/packages/file_selector/file_selector_web/example/README.md b/packages/file_selector/file_selector_web/example/README.md new file mode 100644 index 000000000000..6187e55841c9 --- /dev/null +++ b/packages/file_selector/file_selector_web/example/README.md @@ -0,0 +1,21 @@ +# Testing + +This package utilizes the `integration_test` package to run its tests in a web browser. + +See [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) for more info. + +## Running the tests + +Make sure you have updated to the latest Flutter master. + +1. Check what version of Chrome is running on the machine you're running tests on. + +2. Download and install driver for that version from here: + * + +3. Start the driver using `chromedriver --port=4444` + +4. Run tests: `flutter drive -d web-server --browser-name=chrome --driver=test_driver/integration_test.dart --target=integration_test/TEST_NAME.dart`, or (in Linux): + + * Single: `./run_test.sh integration_test/TEST_NAME.dart` + * All: `./run_test.sh` diff --git a/packages/file_selector/file_selector_web/example/integration_test/dom_helper_test.dart b/packages/file_selector/file_selector_web/example/integration_test/dom_helper_test.dart new file mode 100644 index 000000000000..f000574861ab --- /dev/null +++ b/packages/file_selector/file_selector_web/example/integration_test/dom_helper_test.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:html'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:file_selector_web/src/dom_helper.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; + +void main() { + group('dom_helper', () { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + late DomHelper domHelper; + late FileUploadInputElement input; + + FileList? createFileList(List files) { + final dataTransfer = DataTransfer(); + files.forEach(dataTransfer.items!.add); + return dataTransfer.files as FileList?; + } + + void setFilesAndTriggerChange(List files) { + input.files = createFileList(files); + input.dispatchEvent(Event('change')); + } + + setUp(() { + domHelper = DomHelper(); + input = FileUploadInputElement(); + }); + + group('getFiles', () { + final mockFile1 = File(['123456'], 'file1.txt'); + final mockFile2 = File([], 'file2.txt'); + + testWidgets('works', (_) async { + final Future> futureFiles = domHelper.getFiles( + input: input, + ); + + setFilesAndTriggerChange([mockFile1, mockFile2]); + + final List files = await futureFiles; + + expect(files.length, 2); + + expect(files[0].name, 'file1.txt'); + expect(await files[0].length(), 6); + expect(await files[0].readAsString(), '123456'); + expect(await files[0].lastModified(), isNotNull); + + expect(files[1].name, 'file2.txt'); + expect(await files[1].length(), 0); + expect(await files[1].readAsString(), ''); + expect(await files[1].lastModified(), isNotNull); + }); + + testWidgets('works multiple times', (_) async { + Future> futureFiles; + List files; + + // It should work the first time + futureFiles = domHelper.getFiles(input: input); + setFilesAndTriggerChange([mockFile1]); + + files = await futureFiles; + + expect(files.length, 1); + expect(files.first.name, mockFile1.name); + + // The same input should work more than once + futureFiles = domHelper.getFiles(input: input); + setFilesAndTriggerChange([mockFile2]); + + files = await futureFiles; + + expect(files.length, 1); + expect(files.first.name, mockFile2.name); + }); + + testWidgets('sets the attributes and clicks it', (_) async { + final accept = '.jpg,.png'; + final multiple = true; + bool wasClicked = false; + + //ignore: unawaited_futures + input.onClick.first.then((_) => wasClicked = true); + + final futureFile = domHelper.getFiles( + accept: accept, + multiple: multiple, + input: input, + ); + + expect(input.matchesWithAncestors('body'), true); + expect(input.accept, accept); + expect(input.multiple, multiple); + expect( + wasClicked, + true, + reason: + 'The should be clicked otherwise no dialog will be shown', + ); + + setFilesAndTriggerChange([]); + await futureFile; + + // It should be already removed from the DOM after the file is resolved. + expect(input.parent, isNull); + }); + }); + }); +} diff --git a/packages/file_selector/file_selector_web/example/integration_test/file_selector_web_test.dart b/packages/file_selector/file_selector_web/example/integration_test/file_selector_web_test.dart new file mode 100644 index 000000000000..c16aa1cf454e --- /dev/null +++ b/packages/file_selector/file_selector_web/example/integration_test/file_selector_web_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 'dart:html'; +import 'dart:typed_data'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:file_selector_web/file_selector_web.dart'; +import 'package:file_selector_web/src/dom_helper.dart'; + +void main() { + group('FileSelectorWeb', () { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('openFile', () { + testWidgets('works', (WidgetTester _) async { + final mockFile = createXFile('1001', 'identity.png'); + + final mockDomHelper = MockDomHelper() + ..setFiles([mockFile]) + ..expectAccept('.jpg,.jpeg,image/png,image/*') + ..expectMultiple(false); + + final plugin = FileSelectorWeb(domHelper: mockDomHelper); + + final typeGroup = XTypeGroup( + label: 'images', + extensions: ['jpg', 'jpeg'], + mimeTypes: ['image/png'], + webWildCards: ['image/*'], + ); + + final file = await plugin.openFile(acceptedTypeGroups: [typeGroup]); + + expect(file.name, mockFile.name); + expect(await file.length(), 4); + expect(await file.readAsString(), '1001'); + expect(await file.lastModified(), isNotNull); + }); + }); + + group('openFiles', () { + testWidgets('works', (WidgetTester _) async { + final mockFile1 = createXFile('123456', 'file1.txt'); + final mockFile2 = createXFile('', 'file2.txt'); + + final mockDomHelper = MockDomHelper() + ..setFiles([mockFile1, mockFile2]) + ..expectAccept('.txt') + ..expectMultiple(true); + + final plugin = FileSelectorWeb(domHelper: mockDomHelper); + + final typeGroup = XTypeGroup( + label: 'files', + extensions: ['.txt'], + ); + + final files = await plugin.openFiles(acceptedTypeGroups: [typeGroup]); + + expect(files.length, 2); + + expect(files[0].name, mockFile1.name); + expect(await files[0].length(), 6); + expect(await files[0].readAsString(), '123456'); + expect(await files[0].lastModified(), isNotNull); + + expect(files[1].name, mockFile2.name); + expect(await files[1].length(), 0); + expect(await files[1].readAsString(), ''); + expect(await files[1].lastModified(), isNotNull); + }); + }); + + group('getSavePath', () { + testWidgets('returns non-null', (WidgetTester _) async { + final plugin = FileSelectorWeb(); + final savePath = plugin.getSavePath(); + expect(await savePath, isNotNull); + }); + }); + }); +} + +class MockDomHelper implements DomHelper { + List _files = []; + String _expectedAccept = ''; + bool _expectedMultiple = false; + + @override + Future> getFiles({ + String accept = '', + bool multiple = false, + FileUploadInputElement? input, + }) { + expect(accept, _expectedAccept, + reason: 'Expected "accept" value does not match.'); + expect(multiple, _expectedMultiple, + reason: 'Expected "multiple" value does not match.'); + return Future.value(_files); + } + + void setFiles(List files) { + _files = files; + } + + void expectAccept(String accept) { + _expectedAccept = accept; + } + + void expectMultiple(bool multiple) { + _expectedMultiple = multiple; + } +} + +XFile createXFile(String content, String name) { + final data = Uint8List.fromList(content.codeUnits); + return XFile.fromData(data, name: name, lastModified: DateTime.now()); +} diff --git a/packages/file_selector/file_selector_web/example/lib/main.dart b/packages/file_selector/file_selector_web/example/lib/main.dart new file mode 100644 index 000000000000..e1a38dcdcd46 --- /dev/null +++ b/packages/file_selector/file_selector_web/example/lib/main.dart @@ -0,0 +1,25 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +void main() { + runApp(MyApp()); +} + +/// App for testing +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State { + @override + Widget build(BuildContext context) { + return Directionality( + textDirection: TextDirection.ltr, + child: Text('Testing... Look at the console output for results!'), + ); + } +} diff --git a/packages/file_selector/file_selector_web/example/pubspec.yaml b/packages/file_selector/file_selector_web/example/pubspec.yaml new file mode 100644 index 000000000000..dd98c28d1a99 --- /dev/null +++ b/packages/file_selector/file_selector_web/example/pubspec.yaml @@ -0,0 +1,21 @@ +name: file_selector_web_integration_tests +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.2.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + build_runner: ^1.10.0 + file_selector_web: + path: ../ + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter diff --git a/packages/file_selector/file_selector_web/example/run_test.sh b/packages/file_selector/file_selector_web/example/run_test.sh new file mode 100755 index 000000000000..0542b53cd6c9 --- /dev/null +++ b/packages/file_selector/file_selector_web/example/run_test.sh @@ -0,0 +1,20 @@ +#!/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." +fi diff --git a/packages/file_selector/file_selector_web/example/test_driver/integration_test.dart b/packages/file_selector/file_selector_web/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/file_selector/file_selector_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/file_selector/file_selector_web/example/web/index.html b/packages/file_selector/file_selector_web/example/web/index.html new file mode 100644 index 000000000000..dc9f89762aec --- /dev/null +++ b/packages/file_selector/file_selector_web/example/web/index.html @@ -0,0 +1,12 @@ + + + + + Browser Tests + + + + + diff --git a/packages/file_selector/file_selector_web/lib/file_selector_web.dart b/packages/file_selector/file_selector_web/lib/file_selector_web.dart new file mode 100644 index 000000000000..f7c10b36a186 --- /dev/null +++ b/packages/file_selector/file_selector_web/lib/file_selector_web.dart @@ -0,0 +1,77 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'package:meta/meta.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:file_selector_web/src/dom_helper.dart'; +import 'package:file_selector_web/src/utils.dart'; + +/// The web implementation of [FileSelectorPlatform]. +/// +/// This class implements the `package:file_selector` functionality for the web. +class FileSelectorWeb extends FileSelectorPlatform { + final DomHelper _domHelper; + + /// Registers this class as the default instance of [FileSelectorPlatform]. + static void registerWith(Registrar registrar) { + FileSelectorPlatform.instance = FileSelectorWeb(); + } + + /// Default constructor, initializes _domHelper that we can use + /// to interact with the DOM. + /// overrides parameter allows for testing to override functions + FileSelectorWeb({@visibleForTesting DomHelper? domHelper}) + : _domHelper = domHelper ?? DomHelper(); + + @override + Future openFile({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) async { + final files = await _openFiles(acceptedTypeGroups: acceptedTypeGroups); + return files.first; + } + + @override + Future> openFiles({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) async { + return _openFiles(acceptedTypeGroups: acceptedTypeGroups, multiple: true); + } + + // This is intended to be passed to XFile, which ignores the path, but 'null' + // indicates a canceled save on other platforms, so provide a non-null dummy + // value. + @override + Future getSavePath({ + List? acceptedTypeGroups, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, + }) async => + ''; + + @override + Future getDirectoryPath({ + String? initialDirectory, + String? confirmButtonText, + }) async => + null; + + Future> _openFiles({ + List? acceptedTypeGroups, + bool multiple = false, + }) async { + final accept = acceptedTypesToString(acceptedTypeGroups); + return _domHelper.getFiles( + accept: accept, + multiple: multiple, + ); + } +} diff --git a/packages/file_selector/file_selector_web/lib/src/dom_helper.dart b/packages/file_selector/file_selector_web/lib/src/dom_helper.dart new file mode 100644 index 000000000000..0d251af9cc7f --- /dev/null +++ b/packages/file_selector/file_selector_web/lib/src/dom_helper.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 'dart:async'; +import 'dart:html'; +import 'package:meta/meta.dart'; +import 'package:flutter/services.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; + +/// Class to manipulate the DOM with the intention of reading files from it. +class DomHelper { + final _container = Element.tag('file-selector'); + + /// Default constructor, initializes the container DOM element. + DomHelper() { + final body = querySelector('body')!; + body.children.add(_container); + } + + /// Sets the attributes and waits for a file to be selected. + Future> getFiles({ + String accept = '', + bool multiple = false, + @visibleForTesting FileUploadInputElement? input, + }) { + final Completer> completer = Completer(); + final FileUploadInputElement inputElement = + input ?? FileUploadInputElement(); + + _container.children.add( + inputElement + ..accept = accept + ..multiple = multiple, + ); + + inputElement.onChange.first.then((_) { + final List files = + inputElement.files!.map(_convertFileToXFile).toList(); + inputElement.remove(); + completer.complete(files); + }); + + inputElement.onError.first.then((event) { + final ErrorEvent error = event as ErrorEvent; + final platformException = PlatformException( + code: error.type, + message: error.message, + ); + inputElement.remove(); + completer.completeError(platformException); + }); + + inputElement.click(); + + return completer.future; + } + + XFile _convertFileToXFile(File file) => XFile( + Url.createObjectUrl(file), + name: file.name, + length: file.size, + lastModified: DateTime.fromMillisecondsSinceEpoch( + file.lastModified ?? DateTime.now().millisecondsSinceEpoch), + ); +} diff --git a/packages/file_selector/file_selector_web/lib/src/utils.dart b/packages/file_selector/file_selector_web/lib/src/utils.dart new file mode 100644 index 000000000000..e52c00d1c223 --- /dev/null +++ b/packages/file_selector/file_selector_web/lib/src/utils.dart @@ -0,0 +1,38 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; + +/// Convert list of XTypeGroups to a comma-separated string +String acceptedTypesToString(List? acceptedTypes) { + if (acceptedTypes == null) return ''; + final List allTypes = []; + for (final group in acceptedTypes) { + _assertTypeGroupIsValid(group); + if (group.extensions != null) { + allTypes.addAll(group.extensions!.map(_normalizeExtension)); + } + if (group.mimeTypes != null) { + allTypes.addAll(group.mimeTypes!); + } + if (group.webWildCards != null) { + allTypes.addAll(group.webWildCards!); + } + } + return allTypes.join(','); +} + +/// Make sure that at least one of its fields is populated. +void _assertTypeGroupIsValid(XTypeGroup group) { + assert( + !((group.extensions == null || group.extensions!.isEmpty) && + (group.mimeTypes == null || group.mimeTypes!.isEmpty) && + (group.webWildCards == null || group.webWildCards!.isEmpty)), + 'At least one of extensions / mimeTypes / webWildCards is required for web.'); +} + +/// Append a dot at the beggining if it is not there png -> .png +String _normalizeExtension(String ext) { + return ext.isNotEmpty && ext[0] != '.' ? '.' + ext : ext; +} diff --git a/packages/file_selector/file_selector_web/pubspec.yaml b/packages/file_selector/file_selector_web/pubspec.yaml new file mode 100644 index 000000000000..ebbdfdbbd4da --- /dev/null +++ b/packages/file_selector/file_selector_web/pubspec.yaml @@ -0,0 +1,29 @@ +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 +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 +version: 0.8.1 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +flutter: + plugin: + platforms: + web: + pluginClass: FileSelectorWeb + fileName: file_selector_web.dart + +dependencies: + file_selector_platform_interface: ^2.0.0 + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + meta: ^1.3.0 + +dev_dependencies: + flutter_test: + sdk: flutter + pedantic: ^1.10.0 diff --git a/packages/file_selector/file_selector_web/test/more_tests_exist_elsewhere_test.dart b/packages/file_selector/file_selector_web/test/more_tests_exist_elsewhere_test.dart new file mode 100644 index 000000000000..37c6eb644c9b --- /dev/null +++ b/packages/file_selector/file_selector_web/test/more_tests_exist_elsewhere_test.dart @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('Tell the user where to find the real tests', () { + print('---'); + print('This package also uses integration_test to run additional tests.'); + print('See `example/README.md` for more info.'); + print('---'); + }); +} diff --git a/packages/file_selector/file_selector_web/test/utils_test.dart b/packages/file_selector/file_selector_web/test/utils_test.dart new file mode 100644 index 000000000000..2951af24dff9 --- /dev/null +++ b/packages/file_selector/file_selector_web/test/utils_test.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:flutter_test/flutter_test.dart'; +import 'package:file_selector_web/src/utils.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; + +void main() { + group('FileSelectorWeb utils', () { + group('acceptedTypesToString', () { + test('works', () { + final List acceptedTypes = [ + XTypeGroup(label: 'images', webWildCards: ['images/*']), + XTypeGroup(label: 'jpgs', extensions: ['jpg', 'jpeg']), + XTypeGroup(label: 'pngs', mimeTypes: ['image/png']), + ]; + final accepts = acceptedTypesToString(acceptedTypes); + expect(accepts, 'images/*,.jpg,.jpeg,image/png'); + }); + + test('works with an empty list', () { + final List acceptedTypes = []; + final accepts = acceptedTypesToString(acceptedTypes); + expect(accepts, ''); + }); + + test('works with extensions', () { + final List acceptedTypes = [ + XTypeGroup(label: 'jpgs', extensions: ['jpeg', 'jpg']), + XTypeGroup(label: 'pngs', extensions: ['png']), + ]; + final accepts = acceptedTypesToString(acceptedTypes); + expect(accepts, '.jpeg,.jpg,.png'); + }); + + test('works with mime types', () { + final List acceptedTypes = [ + XTypeGroup(label: 'jpgs', mimeTypes: ['image/jpeg', 'image/jpg']), + XTypeGroup(label: 'pngs', mimeTypes: ['image/png']), + ]; + final accepts = acceptedTypesToString(acceptedTypes); + expect(accepts, 'image/jpeg,image/jpg,image/png'); + }); + + test('works with web wild cards', () { + final List acceptedTypes = [ + XTypeGroup(label: 'images', webWildCards: ['image/*']), + XTypeGroup(label: 'audios', webWildCards: ['audio/*']), + XTypeGroup(label: 'videos', webWildCards: ['video/*']), + ]; + final accepts = acceptedTypesToString(acceptedTypes); + expect(accepts, 'image/*,audio/*,video/*'); + }); + }); + }); +} diff --git a/packages/flutter_plugin_android_lifecycle/AUTHORS b/packages/flutter_plugin_android_lifecycle/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/flutter_plugin_android_lifecycle/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md index 401be5f5278a..f24a22332eaa 100644 --- a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md +++ b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md @@ -1,3 +1,19 @@ +## 2.0.2 +* Migrate maven repo from jcenter to mavenCentral + +## 2.0.1 +* Make sure androidx.lifecycle.DefaultLifecycleObservable doesn't get shrunk + away. + +## 2.0.0 + +* Bump Dart SDK for null-safety compatibility. +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 1.0.12 + +* Update Flutter SDK constraint. + ## 1.0.11 * Keep handling deprecated Android v1 classes for backward compatibility. diff --git a/packages/flutter_plugin_android_lifecycle/LICENSE b/packages/flutter_plugin_android_lifecycle/LICENSE index 507569823f1b..c6823b81eb84 100644 --- a/packages/flutter_plugin_android_lifecycle/LICENSE +++ b/packages/flutter_plugin_android_lifecycle/LICENSE @@ -1,4 +1,4 @@ -Copyright 2019 The Chromium Authors. All rights reserved. +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: diff --git a/packages/flutter_plugin_android_lifecycle/README.md b/packages/flutter_plugin_android_lifecycle/README.md index 25f4d9efd056..3290140f4e5e 100644 --- a/packages/flutter_plugin_android_lifecycle/README.md +++ b/packages/flutter_plugin_android_lifecycle/README.md @@ -1,6 +1,6 @@ # Flutter Android Lifecycle Plugin -[![pub package](https://img.shields.io/pub/v/flutter_plugin_android_lifecycle.svg)](https://pub.dartlang.org/packages/flutter_plugin_android_lifecycle) +[![pub package](https://img.shields.io/pub/v/flutter_plugin_android_lifecycle.svg)](https://pub.dev/packages/flutter_plugin_android_lifecycle) A Flutter plugin for Android to allow other Flutter plugins to access Android `Lifecycle` objects in the plugin's binding. @@ -11,7 +11,7 @@ major version of the Android `Lifecycle` API they expect. ## Installation -Add `flutter_plugin_android_lifecycle` as a [dependency in your pubspec.yaml file](https://flutter.io/using-packages/). +Add `flutter_plugin_android_lifecycle` as a [dependency in your pubspec.yaml file](https://flutter.dev/using-packages/). ## Example diff --git a/packages/flutter_plugin_android_lifecycle/analysis_options.yaml b/packages/flutter_plugin_android_lifecycle/analysis_options.yaml new file mode 100644 index 000000000000..cda4f6e153e6 --- /dev/null +++ b/packages/flutter_plugin_android_lifecycle/analysis_options.yaml @@ -0,0 +1 @@ +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 ac042bf144ab..cf34c98aaf3b 100644 --- a/packages/flutter_plugin_android_lifecycle/android/build.gradle +++ b/packages/flutter_plugin_android_lifecycle/android/build.gradle @@ -4,7 +4,7 @@ version '1.0' buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -15,7 +15,7 @@ buildscript { rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } @@ -27,6 +27,7 @@ android { defaultConfig { minSdkVersion 16 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'proguard.txt' } lintOptions { disable 'InvalidPackage' diff --git a/packages/flutter_plugin_android_lifecycle/android/proguard.txt b/packages/flutter_plugin_android_lifecycle/android/proguard.txt new file mode 100644 index 000000000000..d3a6df0eefd2 --- /dev/null +++ b/packages/flutter_plugin_android_lifecycle/android/proguard.txt @@ -0,0 +1,9 @@ +# The point of this package is to specify that a dependent plugin intends to +# use the AndroidX lifecycle classes. Make sure no R8 heuristics shrink classes +# brought in by the embedding's pom. +# +# This isn't strictly needed since by definition, plugins using Android +# lifecycles should implement DefaultLifecycleObserver and therefore keep it +# from being shrunk. But there seems to be an R8 bug so this needs to stay +# https://issuetracker.google.com/issues/142778206. +-keep class androidx.lifecycle.DefaultLifecycleObserver diff --git a/packages/flutter_plugin_android_lifecycle/android/src/main/java/io/flutter/embedding/engine/plugins/lifecycle/FlutterLifecycleAdapter.java b/packages/flutter_plugin_android_lifecycle/android/src/main/java/io/flutter/embedding/engine/plugins/lifecycle/FlutterLifecycleAdapter.java index 91ac6e0fd15f..05490eb93e46 100644 --- a/packages/flutter_plugin_android_lifecycle/android/src/main/java/io/flutter/embedding/engine/plugins/lifecycle/FlutterLifecycleAdapter.java +++ b/packages/flutter_plugin_android_lifecycle/android/src/main/java/io/flutter/embedding/engine/plugins/lifecycle/FlutterLifecycleAdapter.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/flutter_plugin_android_lifecycle/android/src/main/java/io/flutter/plugins/flutter_plugin_android_lifecycle/FlutterAndroidLifecyclePlugin.java b/packages/flutter_plugin_android_lifecycle/android/src/main/java/io/flutter/plugins/flutter_plugin_android_lifecycle/FlutterAndroidLifecyclePlugin.java index bd77c1545ac7..e3b8ea2a6318 100644 --- a/packages/flutter_plugin_android_lifecycle/android/src/main/java/io/flutter/plugins/flutter_plugin_android_lifecycle/FlutterAndroidLifecyclePlugin.java +++ b/packages/flutter_plugin_android_lifecycle/android/src/main/java/io/flutter/plugins/flutter_plugin_android_lifecycle/FlutterAndroidLifecyclePlugin.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/flutter_plugin_android_lifecycle/android/src/test/java/io/flutter/embedding/engine/plugins/lifecycle/FlutterLifecycleAdapterTest.java b/packages/flutter_plugin_android_lifecycle/android/src/test/java/io/flutter/embedding/engine/plugins/lifecycle/FlutterLifecycleAdapterTest.java index 2a5a91d02f60..08bb3d7266e8 100644 --- a/packages/flutter_plugin_android_lifecycle/android/src/test/java/io/flutter/embedding/engine/plugins/lifecycle/FlutterLifecycleAdapterTest.java +++ b/packages/flutter_plugin_android_lifecycle/android/src/test/java/io/flutter/embedding/engine/plugins/lifecycle/FlutterLifecycleAdapterTest.java @@ -1,3 +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. + package io.flutter.embedding.engine.plugins.lifecycle; import static org.junit.Assert.assertEquals; diff --git a/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/flutter_plugin_android_lifecycle/EmbeddingV1ActivityTest.java b/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/flutter_plugin_android_lifecycle/EmbeddingV1ActivityTest.java index e61cadd3a9d3..84173f4a9c0f 100644 --- a/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/flutter_plugin_android_lifecycle/EmbeddingV1ActivityTest.java +++ b/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/flutter_plugin_android_lifecycle/EmbeddingV1ActivityTest.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/flutter_plugin_android_lifecycle/MainActivityTest.java b/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/flutter_plugin_android_lifecycle/MainActivityTest.java index 4c3c9a247883..66a606ca00a9 100644 --- a/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/flutter_plugin_android_lifecycle/MainActivityTest.java +++ b/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/flutter_plugin_android_lifecycle/MainActivityTest.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/flutter_plugin_android_lifecycle/example/android/app/src/main/java/io/flutter/plugins/flutter_plugin_android_lifecycle_example/EmbeddingV1Activity.java b/packages/flutter_plugin_android_lifecycle/example/android/app/src/main/java/io/flutter/plugins/flutter_plugin_android_lifecycle_example/EmbeddingV1Activity.java index f303f4c61d20..e6ab004fccf6 100644 --- a/packages/flutter_plugin_android_lifecycle/example/android/app/src/main/java/io/flutter/plugins/flutter_plugin_android_lifecycle_example/EmbeddingV1Activity.java +++ b/packages/flutter_plugin_android_lifecycle/example/android/app/src/main/java/io/flutter/plugins/flutter_plugin_android_lifecycle_example/EmbeddingV1Activity.java @@ -1,3 +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. + package io.flutter.plugins.flutter_plugin_android_lifecycle_example; import android.os.Bundle; diff --git a/packages/flutter_plugin_android_lifecycle/example/android/app/src/main/java/io/flutter/plugins/flutter_plugin_android_lifecycle_example/MainActivity.java b/packages/flutter_plugin_android_lifecycle/example/android/app/src/main/java/io/flutter/plugins/flutter_plugin_android_lifecycle_example/MainActivity.java index e2d2560e9105..1726aecbeddb 100644 --- a/packages/flutter_plugin_android_lifecycle/example/android/app/src/main/java/io/flutter/plugins/flutter_plugin_android_lifecycle_example/MainActivity.java +++ b/packages/flutter_plugin_android_lifecycle/example/android/app/src/main/java/io/flutter/plugins/flutter_plugin_android_lifecycle_example/MainActivity.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/flutter_plugin_android_lifecycle/example/android/build.gradle b/packages/flutter_plugin_android_lifecycle/example/android/build.gradle index e0d7ae2c11af..456d020f6e2c 100644 --- a/packages/flutter_plugin_android_lifecycle/example/android/build.gradle +++ b/packages/flutter_plugin_android_lifecycle/example/android/build.gradle @@ -1,7 +1,7 @@ buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -12,7 +12,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/packages/flutter_plugin_android_lifecycle/example/integration_test/flutter_plugin_android_lifecycle_test.dart b/packages/flutter_plugin_android_lifecycle/example/integration_test/flutter_plugin_android_lifecycle_test.dart index 1405ab69153c..51f9a2537ffc 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 @@ -1,6 +1,6 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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'; diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/.gitignore b/packages/flutter_plugin_android_lifecycle/example/ios/.gitignore deleted file mode 100644 index f78c1480b6dd..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/.gitignore +++ /dev/null @@ -1,31 +0,0 @@ -*.mode1v3 -*.mode2v3 -*.moved-aside -*.pbxuser -*.perspectivev3 -**/*sync/ -.sconsign.dblite -.tags* -**/.vagrant/ -**/DerivedData/ -Icon? -**/Pods/ -**/.symlinks/ -profile -xcuserdata -**/.generated/ -Flutter/App.framework -Flutter/Flutter.framework -Flutter/Generated.xcconfig -Flutter/app.flx -Flutter/app.zip -Flutter/flutter_assets/ -Flutter/flutter_export_environment.sh -ServiceDefinitions.json -Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!default.mode1v3 -!default.mode2v3 -!default.pbxuser -!default.perspectivev3 diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Flutter/AppFrameworkInfo.plist b/packages/flutter_plugin_android_lifecycle/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6b4c0f78a785..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 8.0 - - diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Flutter/Debug.xcconfig b/packages/flutter_plugin_android_lifecycle/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index e8efba114687..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Flutter/Release.xcconfig b/packages/flutter_plugin_android_lifecycle/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index 399e9340e6f6..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner.xcodeproj/project.pbxproj b/packages/flutter_plugin_android_lifecycle/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index f42fc07b1c19..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,576 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 68BFFC9A2252F6377926CCB6 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D97B2D435F77384E1832544A /* libPods-Runner.a */; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; - 48B2B2D61E102CB7FCA66327 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 8A495AA36DFBF39C3BD5D917 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 9E27BB0D8AE008E9718C1EC3 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - D97B2D435F77384E1832544A /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - 68BFFC9A2252F6377926CCB6 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 29946A38AEAEDCD95716766D /* Pods */ = { - isa = PBXGroup; - children = ( - 8A495AA36DFBF39C3BD5D917 /* Pods-Runner.debug.xcconfig */, - 9E27BB0D8AE008E9718C1EC3 /* Pods-Runner.release.xcconfig */, - 48B2B2D61E102CB7FCA66327 /* Pods-Runner.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B80C3931E831B6300D905FE /* App.framework */, - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 29946A38AEAEDCD95716766D /* Pods */, - C6B60E52AC0C0C398A9D6E3E /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - C6B60E52AC0C0C398A9D6E3E /* Frameworks */ = { - isa = PBXGroup; - children = ( - D97B2D435F77384E1832544A /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - B0349D7BFB658C43C3407041 /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 2D345E120F865FCD8BCE231E /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1020; - ORGANIZATIONNAME = "The Chromium Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 2D345E120F865FCD8BCE231E /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; - B0349D7BFB658C43C3407041 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.flutterAndroidLifecycleExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.flutterAndroidLifecycleExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.flutterAndroidLifecycleExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - 249021D3217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - 249021D4217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/flutter_plugin_android_lifecycle/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index a28140cfdb3f..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,91 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/AppDelegate.h b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/AppDelegate.h deleted file mode 100644 index 36e21bbf9cf4..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,6 +0,0 @@ -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/AppDelegate.m b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/AppDelegate.m deleted file mode 100644 index 59a72e90be12..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,13 +0,0 @@ -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf03016f..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca8..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cde12118dda..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb8..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index dcdc2306c285..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a9..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 6a84f41e14e2..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index d0e1f5853602..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Base.lproj/Main.storyboard b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516fb38..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Info.plist b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Info.plist deleted file mode 100644 index 8526e1f7226c..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Info.plist +++ /dev/null @@ -1,45 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - flutter_plugin_android_lifecycle_example - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/main.m b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/main.m deleted file mode 100644 index dff6597e4513..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/main.m +++ /dev/null @@ -1,9 +0,0 @@ -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/flutter_plugin_android_lifecycle/example/lib/main.dart b/packages/flutter_plugin_android_lifecycle/example/lib/main.dart index 6dfe523a0ae1..c019590b2a7c 100644 --- a/packages/flutter_plugin_android_lifecycle/example/lib/main.dart +++ b/packages/flutter_plugin_android_lifecycle/example/lib/main.dart @@ -1,3 +1,7 @@ +// Copyright 2013 The Flutter 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'; diff --git a/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml b/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml index a040dbe95d92..6dc8d366e82b 100644 --- a/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml +++ b/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml @@ -1,23 +1,27 @@ name: flutter_plugin_android_lifecycle_example description: Demonstrates how to use the flutter_plugin_android_lifecycle plugin. -publish_to: 'none' +publish_to: none environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.12.0 <3.0.0" dependencies: flutter: sdk: flutter - integration_test: - path: ../../integration_test + flutter_plugin_android_lifecycle: + # When depending on this package from a real application you should use: + # flutter_plugin_android_lifecycle: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ dev_dependencies: + integration_test: + sdk: flutter flutter_test: sdk: flutter pedantic: ^1.8.0 - flutter_plugin_android_lifecycle: - path: ../ - flutter: uses-material-design: true diff --git a/packages/flutter_plugin_android_lifecycle/ios/.gitignore b/packages/flutter_plugin_android_lifecycle/ios/.gitignore deleted file mode 100644 index aa479fd3ce8a..000000000000 --- a/packages/flutter_plugin_android_lifecycle/ios/.gitignore +++ /dev/null @@ -1,37 +0,0 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -.generated/ - -*.pbxuser -*.mode1v3 -*.mode2v3 -*.perspectivev3 - -!default.pbxuser -!default.mode1v3 -!default.mode2v3 -!default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/Generated.xcconfig -/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/packages/flutter_plugin_android_lifecycle/ios/Assets/.gitkeep b/packages/flutter_plugin_android_lifecycle/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/flutter_plugin_android_lifecycle/ios/Classes/FlutterAndroidLifecyclePlugin.h b/packages/flutter_plugin_android_lifecycle/ios/Classes/FlutterAndroidLifecyclePlugin.h deleted file mode 100644 index a554ce0500c6..000000000000 --- a/packages/flutter_plugin_android_lifecycle/ios/Classes/FlutterAndroidLifecyclePlugin.h +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import - -@interface FlutterAndroidLifecyclePlugin : NSObject -@end diff --git a/packages/flutter_plugin_android_lifecycle/ios/Classes/FlutterAndroidLifecyclePlugin.m b/packages/flutter_plugin_android_lifecycle/ios/Classes/FlutterAndroidLifecyclePlugin.m deleted file mode 100644 index 38cffd362da7..000000000000 --- a/packages/flutter_plugin_android_lifecycle/ios/Classes/FlutterAndroidLifecyclePlugin.m +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "FlutterAndroidLifecyclePlugin.h" - -@implementation FlutterAndroidLifecyclePlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { -} -@end diff --git a/packages/flutter_plugin_android_lifecycle/ios/flutter_plugin_android_lifecycle.podspec b/packages/flutter_plugin_android_lifecycle/ios/flutter_plugin_android_lifecycle.podspec deleted file mode 100644 index 0c802a3101ba..000000000000 --- a/packages/flutter_plugin_android_lifecycle/ios/flutter_plugin_android_lifecycle.podspec +++ /dev/null @@ -1,26 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint flutter_plugin_android_lifecycle.podspec' to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'flutter_plugin_android_lifecycle' - s.version = '0.0.1' - s.summary = 'Flutter Android Lifecycle Plugin' - s.description = <<-DESC -A Flutter plugin for Android to allow other Flutter plugins to access Android Lifecycle objects in the plugin's binding. -This plugin a no-op on iOS. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/flutter_plugin_android_lifecycle' } - s.documentation_url = 'https://pub.dev/packages/flutter_plugin_android_lifecycle' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.platform = :ios, '8.0' - - # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } -end diff --git a/packages/flutter_plugin_android_lifecycle/lib/flutter_plugin_android_lifecycle.dart b/packages/flutter_plugin_android_lifecycle/lib/flutter_plugin_android_lifecycle.dart index 4352552e3eda..340b06832f19 100644 --- a/packages/flutter_plugin_android_lifecycle/lib/flutter_plugin_android_lifecycle.dart +++ b/packages/flutter_plugin_android_lifecycle/lib/flutter_plugin_android_lifecycle.dart @@ -1,2 +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. + // 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 a671381b3f47..2fefc8616868 100644 --- a/packages/flutter_plugin_android_lifecycle/pubspec.yaml +++ b/packages/flutter_plugin_android_lifecycle/pubspec.yaml @@ -1,11 +1,19 @@ name: flutter_plugin_android_lifecycle description: Flutter plugin for accessing an Android Lifecycle within other plugins. -version: 1.0.11 -homepage: https://github.com/flutter/plugins/tree/master/packages/flutter_plugin_android_lifecycle +repository: https://github.com/flutter/plugins/tree/master/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.2 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13 <2.0.0" + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +flutter: + plugin: + platforms: + android: + package: io.flutter.plugins.flutter_plugin_android_lifecycle + pluginClass: FlutterAndroidLifecyclePlugin dependencies: flutter: @@ -15,10 +23,3 @@ dev_dependencies: flutter_test: sdk: flutter pedantic: ^1.8.0 - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.flutter_plugin_android_lifecycle - pluginClass: FlutterAndroidLifecyclePlugin diff --git a/packages/google_maps_flutter/analysis_options.yaml b/packages/google_maps_flutter/analysis_options.yaml new file mode 100644 index 000000000000..cda4f6e153e6 --- /dev/null +++ b/packages/google_maps_flutter/analysis_options.yaml @@ -0,0 +1 @@ +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 new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md index 7d5324a17e7c..5ba399311661 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,97 @@ +## NEXT + +* Add iOS unit and UI integration test targets. + +## 2.0.6 + +* Migrate maven repo from jcenter to mavenCentral. + +## 2.0.5 + +* Google Maps requires at least Android SDK 20. + +## 2.0.4 + +* Unpin iOS GoogleMaps pod dependency version. + +## 2.0.3 + +* Fix incorrect typecast in TileOverlay example. +* Fix english wording in instructions. + +## 2.0.2 + +* Update flutter\_plugin\_android\_lifecycle dependency to 2.0.1 to fix an R8 issue + on some versions. + +## 2.0.1 + +* Update platform\_plugin\_interface version requirement. + +## 2.0.0 + +* Migrate to null-safety +* BREAKING CHANGE: Passing an unknown map object ID (e.g., MarkerId) to a + method, it will throw an `UnknownMapObjectIDError`. Previously it would + either silently do nothing, or throw an error trying to call a function on + `null`, depneding on the method. + +## 1.2.0 + +* Support custom tiles. + +## 1.1.1 + +* Fix in example app to properly place polyline at initial camera position. + +## 1.1.0 + +* Add support for holes in Polygons. + +## 1.0.10 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 1.0.9 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 1.0.8 + +* Update Flutter SDK constraint. + +## 1.0.7 + +* Android: Handle deprecation & unchecked warning as error. + +## 1.0.6 + +* Update Dart SDK constraint in example. +* Remove unused `test` dependency in the example app. + +## 1.0.5 + +Overhaul lifecycle management in GoogleMapsPlugin. + +GoogleMapController is now uniformly driven by implementing `DefaultLifecycleObserver`. That observer is registered to a lifecycle from one of three sources: + +1. For v2 plugin registration, `GoogleMapsPlugin` obtains the lifecycle via `ActivityAware` methods. +2. For v1 plugin registration, if the activity implements `LifecycleOwner`, it's lifecycle is used directly. +3. For v1 plugin registration, if the activity does not implement `LifecycleOwner`, a proxy lifecycle is created and driven via `ActivityLifecycleCallbacks`. + +## 1.0.4 + +* Cleanup of Android code: +* A few minor formatting changes and additions of `@Nullable` annotations. +* Removed pass-through of `activityHashCode` to `GoogleMapController`. +* Replaced custom lifecycle state ints with `androidx.lifecycle.Lifecycle.State` enum. +* Fixed a bug where the Lifecycle object was being leaked `onDetachFromActivity`, by nulling out the field. +* Moved GoogleMapListener to its own file. Declaring multiple top level classes in the same file is discouraged. + +## 1.0.3 + +* Update android compileSdkVersion to 29. + ## 1.0.2 * Remove `io.flutter.embedded_views_preview` requirement from readme. diff --git a/packages/google_maps_flutter/google_maps_flutter/LICENSE b/packages/google_maps_flutter/google_maps_flutter/LICENSE index ad33cf3c3ed1..c6823b81eb84 100644 --- a/packages/google_maps_flutter/google_maps_flutter/LICENSE +++ b/packages/google_maps_flutter/google_maps_flutter/LICENSE @@ -1,4 +1,4 @@ -Copyright 2018 The Chromium Authors. All rights reserved. +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: diff --git a/packages/google_maps_flutter/google_maps_flutter/README.md b/packages/google_maps_flutter/google_maps_flutter/README.md index cbc0ccbf62b8..c80fcb949dad 100644 --- a/packages/google_maps_flutter/google_maps_flutter/README.md +++ b/packages/google_maps_flutter/google_maps_flutter/README.md @@ -1,12 +1,12 @@ # Google Maps for Flutter -[![pub package](https://img.shields.io/pub/v/google_maps_flutter.svg)](https://pub.dartlang.org/packages/google_maps_flutter) +[![pub package](https://img.shields.io/pub/v/google_maps_flutter.svg)](https://pub.dev/packages/google_maps_flutter) A Flutter plugin that provides a [Google Maps](https://developers.google.com/maps/) widget. ## Usage -To use this plugin, add `google_maps_flutter` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). +To use this plugin, add `google_maps_flutter` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). ## Getting Started @@ -21,11 +21,23 @@ To use this plugin, add `google_maps_flutter` as a [dependency in your pubspec.y * To enable Google Maps for iOS, select "Maps SDK for iOS" in the "Additional APIs" section, then select "ENABLE". * Make sure the APIs you enabled are under the "Enabled APIs" section. -* You can also find detailed steps to get start with Google Maps Platform [here](https://developers.google.com/maps/gmp-get-started). +For more details, see [Getting started with Google Maps Platform](https://developers.google.com/maps/gmp-get-started). ### Android -Specify your API key in the application manifest `android/app/src/main/AndroidManifest.xml`: +1. Set the `minSdkVersion` in `android/app/build.gradle`: + +```groovy +android { + defaultConfig { + minSdkVersion 20 + } +} +``` + +This means that app will only be available for users that run Android SDK 20 or higher. + +2. Specify your API key in the application manifest `android/app/src/main/AndroidManifest.xml`: ```xml data = toList(o); switch (toString(data.get(0))) { @@ -75,7 +79,8 @@ private static BitmapDescriptor getBitmapFromBytes(List data) { } } else { throw new IllegalArgumentException( - "fromBytes should have exactly one argument, the bytes. Got: " + data.size()); + "fromBytes should have exactly one argument, interpretTileOverlayOptions the bytes. Got: " + + data.size()); } } @@ -197,6 +202,20 @@ static Object circleIdToJson(String circleId) { return data; } + static Map tileOverlayArgumentsToJson( + String tileOverlayId, int x, int y, int zoom) { + + if (tileOverlayId == null) { + return null; + } + final Map data = new HashMap<>(4); + data.put("tileOverlayId", tileOverlayId); + data.put("x", x); + data.put("y", y); + data.put("zoom", zoom); + return data; + } + static Object latLngToJson(LatLng latLng) { return Arrays.asList(latLng.latitude, latLng.longitude); } @@ -207,8 +226,9 @@ static LatLng toLatLng(Object o) { } static Point toPoint(Object o) { - Map screenCoordinate = (Map) o; - return new Point(screenCoordinate.get("x"), screenCoordinate.get("y")); + Object x = toMap(o).get("x"); + Object y = toMap(o).get("y"); + return new Point((int) x, (int) y); } static Map pointToJson(Point point) { @@ -234,6 +254,18 @@ private static List toList(Object o) { return (Map) o; } + private static Map toObjectMap(Object o) { + Map hashMap = new HashMap<>(); + Map map = (Map) o; + for (Object key : map.keySet()) { + Object object = map.get(key); + if (object != null) { + hashMap.put((String) key, object); + } + } + return hashMap; + } + private static float toFractionalPixels(Object o, float density) { return toFloat(o) * density; } @@ -377,7 +409,7 @@ static String interpretMarkerOptions(Object o, MarkerOptionsSink sink) { final Object infoWindow = data.get("infoWindow"); if (infoWindow != null) { - interpretInfoWindowOptions(sink, (Map) infoWindow); + interpretInfoWindowOptions(sink, toObjectMap(infoWindow)); } final Object position = data.get("position"); if (position != null) { @@ -452,6 +484,10 @@ static String interpretPolygonOptions(Object o, PolygonOptionsSink sink) { if (points != null) { sink.setPoints(toPoints(points)); } + final Object holes = data.get("holes"); + if (holes != null) { + sink.setHoles(toHoles(holes)); + } final String polygonId = (String) data.get("polygonId"); if (polygonId == null) { throw new IllegalArgumentException("polygonId was null"); @@ -560,13 +596,23 @@ private static List toPoints(Object o) { final List data = toList(o); final List points = new ArrayList<>(data.size()); - for (Object ob : data) { - final List point = toList(ob); + for (Object rawPoint : data) { + final List point = toList(rawPoint); points.add(new LatLng(toFloat(point.get(0)), toFloat(point.get(1)))); } return points; } + private static List> toHoles(Object o) { + final List data = toList(o); + final List> holes = new ArrayList<>(data.size()); + + for (Object rawHole : data) { + holes.add(toPoints(rawHole)); + } + return holes; + } + private static List toPattern(Object o) { final List data = toList(o); @@ -615,4 +661,39 @@ private static Cap toCap(Object o) { throw new IllegalArgumentException("Cannot interpret " + o + " as Cap"); } } + + static String interpretTileOverlayOptions(Map data, TileOverlaySink sink) { + final Object fadeIn = data.get("fadeIn"); + if (fadeIn != null) { + sink.setFadeIn(toBoolean(fadeIn)); + } + final Object transparency = data.get("transparency"); + if (transparency != null) { + sink.setTransparency(toFloat(transparency)); + } + final Object zIndex = data.get("zIndex"); + if (zIndex != null) { + sink.setZIndex(toFloat(zIndex)); + } + final Object visible = data.get("visible"); + if (visible != null) { + sink.setVisible(toBoolean(visible)); + } + final String tileOverlayId = (String) data.get("tileOverlayId"); + if (tileOverlayId == null) { + throw new IllegalArgumentException("tileOverlayId was null"); + } else { + return tileOverlayId; + } + } + + static Tile interpretTile(Map data) { + int width = toInt(data.get("width")); + int height = toInt(data.get("height")); + byte[] dataArray = null; + if (data.get("data") != null) { + dataArray = (byte[]) data.get("data"); + } + return new Tile(width, height, dataArray); + } } diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java index 97af63c9f63b..ad5179a69a45 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapBuilder.java @@ -1,19 +1,17 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package io.flutter.plugins.googlemaps; -import android.app.Application; import android.content.Context; import android.graphics.Rect; -import androidx.lifecycle.Lifecycle; import com.google.android.gms.maps.GoogleMapOptions; import com.google.android.gms.maps.model.CameraPosition; import com.google.android.gms.maps.model.LatLngBounds; import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.PluginRegistry; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.List; +import java.util.Map; class GoogleMapBuilder implements GoogleMapOptionsSink { private final GoogleMapOptions options = new GoogleMapOptions(); @@ -27,28 +25,16 @@ class GoogleMapBuilder implements GoogleMapOptionsSink { private Object initialPolygons; private Object initialPolylines; private Object initialCircles; + private List> initialTileOverlays; private Rect padding = new Rect(0, 0, 0, 0); GoogleMapController build( int id, Context context, - AtomicInteger state, BinaryMessenger binaryMessenger, - Application application, - Lifecycle lifecycle, - PluginRegistry.Registrar registrar, - int activityHashCode) { + LifecycleProvider lifecycleProvider) { final GoogleMapController controller = - new GoogleMapController( - id, - context, - state, - binaryMessenger, - application, - lifecycle, - registrar, - activityHashCode, - options); + new GoogleMapController(id, context, binaryMessenger, lifecycleProvider, options); controller.init(); controller.setMyLocationEnabled(myLocationEnabled); controller.setMyLocationButtonEnabled(myLocationButtonEnabled); @@ -61,6 +47,7 @@ GoogleMapController build( controller.setInitialPolylines(initialPolylines); controller.setInitialCircles(initialCircles); controller.setPadding(padding.top, padding.left, padding.bottom, padding.right); + controller.setInitialTileOverlays(initialTileOverlays); return controller; } @@ -182,4 +169,9 @@ public void setInitialPolylines(Object initialPolylines) { public void setInitialCircles(Object initialCircles) { this.initialCircles = initialCircles; } + + @Override + public void setInitialTileOverlays(List> initialTileOverlays) { + this.initialTileOverlays = initialTileOverlays; + } } diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapController.java index 58e8bb073bb7..05e016c32e27 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 @@ -1,20 +1,11 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // 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 io.flutter.plugins.googlemaps.GoogleMapsPlugin.CREATED; -import static io.flutter.plugins.googlemaps.GoogleMapsPlugin.DESTROYED; -import static io.flutter.plugins.googlemaps.GoogleMapsPlugin.PAUSED; -import static io.flutter.plugins.googlemaps.GoogleMapsPlugin.RESUMED; -import static io.flutter.plugins.googlemaps.GoogleMapsPlugin.STARTED; -import static io.flutter.plugins.googlemaps.GoogleMapsPlugin.STOPPED; - import android.Manifest; import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.Application; import android.content.Context; import android.content.pm.PackageManager; import android.graphics.Bitmap; @@ -45,7 +36,6 @@ 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 io.flutter.plugin.platform.PlatformView; import java.io.ByteArrayOutputStream; import java.util.ArrayList; @@ -53,12 +43,10 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; /** Controller of a single GoogleMaps MapView instance. */ final class GoogleMapController - implements Application.ActivityLifecycleCallbacks, - DefaultLifecycleObserver, + implements DefaultLifecycleObserver, ActivityPluginBinding.OnSaveInstanceStateListener, GoogleMapOptionsSink, MethodChannel.MethodCallHandler, @@ -68,7 +56,6 @@ final class GoogleMapController private static final String TAG = "GoogleMapController"; private final int id; - private final AtomicInteger activityState; private final MethodChannel methodChannel; private final GoogleMapOptions options; @Nullable private MapView mapView; @@ -83,48 +70,38 @@ final class GoogleMapController private boolean disposed = false; private final float density; private MethodChannel.Result mapReadyResult; - private final int - activityHashCode; // Do not use directly, use getActivityHashCode() instead to get correct hashCode for both v1 and v2 embedding. - private final Lifecycle lifecycle; private final Context context; - private final Application - mApplication; // Do not use direclty, use getApplication() instead to get correct application object for both v1 and v2 embedding. - private final PluginRegistry.Registrar registrar; // For v1 embedding only. + private final LifecycleProvider lifecycleProvider; private final MarkersController markersController; private final PolygonsController polygonsController; private final PolylinesController polylinesController; private final CirclesController circlesController; + private final TileOverlaysController tileOverlaysController; private List initialMarkers; private List initialPolygons; private List initialPolylines; private List initialCircles; + private List> initialTileOverlays; GoogleMapController( int id, Context context, - AtomicInteger activityState, BinaryMessenger binaryMessenger, - Application application, - Lifecycle lifecycle, - PluginRegistry.Registrar registrar, - int registrarActivityHashCode, + LifecycleProvider lifecycleProvider, GoogleMapOptions options) { this.id = id; this.context = context; - this.activityState = activityState; this.options = options; this.mapView = new MapView(context, options); this.density = context.getResources().getDisplayMetrics().density; methodChannel = new MethodChannel(binaryMessenger, "plugins.flutter.io/google_maps_" + id); methodChannel.setMethodCallHandler(this); - mApplication = application; - this.lifecycle = lifecycle; - this.registrar = registrar; - this.activityHashCode = registrarActivityHashCode; + this.lifecycleProvider = lifecycleProvider; this.markersController = new MarkersController(methodChannel); this.polygonsController = new PolygonsController(methodChannel, density); this.polylinesController = new PolylinesController(methodChannel, density); this.circlesController = new CirclesController(methodChannel, density); + this.tileOverlaysController = new TileOverlaysController(methodChannel); } @Override @@ -133,44 +110,7 @@ public View getView() { } void init() { - switch (activityState.get()) { - case STOPPED: - mapView.onCreate(null); - mapView.onStart(); - mapView.onResume(); - mapView.onPause(); - mapView.onStop(); - break; - case PAUSED: - mapView.onCreate(null); - mapView.onStart(); - mapView.onResume(); - mapView.onPause(); - break; - case RESUMED: - mapView.onCreate(null); - mapView.onStart(); - mapView.onResume(); - break; - case STARTED: - mapView.onCreate(null); - mapView.onStart(); - break; - case CREATED: - mapView.onCreate(null); - break; - case DESTROYED: - // Nothing to do, the activity has been completely destroyed. - break; - default: - throw new IllegalArgumentException( - "Cannot interpret " + activityState.get() + " as an activity state"); - } - if (lifecycle != null) { - lifecycle.addObserver(this); - } else { - getApplication().registerActivityLifecycleCallbacks(this); - } + lifecycleProvider.getLifecycle().addObserver(this); mapView.getMapAsync(this); } @@ -203,10 +143,12 @@ public void onMapReady(GoogleMap googleMap) { polygonsController.setGoogleMap(googleMap); polylinesController.setGoogleMap(googleMap); circlesController.setGoogleMap(googleMap); + tileOverlaysController.setGoogleMap(googleMap); updateInitialMarkers(); updateInitialPolygons(); updateInitialPolylines(); updateInitialCircles(); + updateInitialTileOverlays(); } @Override @@ -302,12 +244,12 @@ public void onSnapshotReady(Bitmap bitmap) { } case "markers#update": { - Object markersToAdd = call.argument("markersToAdd"); - markersController.addMarkers((List) markersToAdd); - Object markersToChange = call.argument("markersToChange"); - markersController.changeMarkers((List) markersToChange); - Object markerIdsToRemove = call.argument("markerIdsToRemove"); - markersController.removeMarkers((List) markerIdsToRemove); + List markersToAdd = call.argument("markersToAdd"); + markersController.addMarkers(markersToAdd); + List markersToChange = call.argument("markersToChange"); + markersController.changeMarkers(markersToChange); + List markerIdsToRemove = call.argument("markerIdsToRemove"); + markersController.removeMarkers(markerIdsToRemove); result.success(null); break; } @@ -331,34 +273,34 @@ public void onSnapshotReady(Bitmap bitmap) { } case "polygons#update": { - Object polygonsToAdd = call.argument("polygonsToAdd"); - polygonsController.addPolygons((List) polygonsToAdd); - Object polygonsToChange = call.argument("polygonsToChange"); - polygonsController.changePolygons((List) polygonsToChange); - Object polygonIdsToRemove = call.argument("polygonIdsToRemove"); - polygonsController.removePolygons((List) polygonIdsToRemove); + List polygonsToAdd = call.argument("polygonsToAdd"); + polygonsController.addPolygons(polygonsToAdd); + List polygonsToChange = call.argument("polygonsToChange"); + polygonsController.changePolygons(polygonsToChange); + List polygonIdsToRemove = call.argument("polygonIdsToRemove"); + polygonsController.removePolygons(polygonIdsToRemove); result.success(null); break; } case "polylines#update": { - Object polylinesToAdd = call.argument("polylinesToAdd"); - polylinesController.addPolylines((List) polylinesToAdd); - Object polylinesToChange = call.argument("polylinesToChange"); - polylinesController.changePolylines((List) polylinesToChange); - Object polylineIdsToRemove = call.argument("polylineIdsToRemove"); - polylinesController.removePolylines((List) polylineIdsToRemove); + List polylinesToAdd = call.argument("polylinesToAdd"); + polylinesController.addPolylines(polylinesToAdd); + List polylinesToChange = call.argument("polylinesToChange"); + polylinesController.changePolylines(polylinesToChange); + List polylineIdsToRemove = call.argument("polylineIdsToRemove"); + polylinesController.removePolylines(polylineIdsToRemove); result.success(null); break; } case "circles#update": { - Object circlesToAdd = call.argument("circlesToAdd"); - circlesController.addCircles((List) circlesToAdd); - Object circlesToChange = call.argument("circlesToChange"); - circlesController.changeCircles((List) circlesToChange); - Object circleIdsToRemove = call.argument("circleIdsToRemove"); - circlesController.removeCircles((List) circleIdsToRemove); + List circlesToAdd = call.argument("circlesToAdd"); + circlesController.addCircles(circlesToAdd); + List circlesToChange = call.argument("circlesToChange"); + circlesController.changeCircles(circlesToChange); + List circleIdsToRemove = call.argument("circleIdsToRemove"); + circlesController.removeCircles(circleIdsToRemove); result.success(null); break; } @@ -448,6 +390,30 @@ public void onSnapshotReady(Bitmap bitmap) { result.success(mapStyleResult); break; } + case "tileOverlays#update": + { + List> tileOverlaysToAdd = call.argument("tileOverlaysToAdd"); + tileOverlaysController.addTileOverlays(tileOverlaysToAdd); + List> tileOverlaysToChange = call.argument("tileOverlaysToChange"); + tileOverlaysController.changeTileOverlays(tileOverlaysToChange); + List tileOverlaysToRemove = call.argument("tileOverlayIdsToRemove"); + tileOverlaysController.removeTileOverlays(tileOverlaysToRemove); + result.success(null); + break; + } + case "tileOverlays#clearTileCache": + { + String tileOverlayId = call.argument("tileOverlayId"); + tileOverlaysController.clearTileCache(tileOverlayId); + result.success(null); + break; + } + case "map#getTileOverlayInfo": + { + String tileOverlayId = call.argument("tileOverlayId"); + result.success(tileOverlaysController.getTileOverlayInfo(tileOverlayId)); + break; + } default: result.notImplemented(); } @@ -535,7 +501,10 @@ public void dispose() { methodChannel.setMethodCallHandler(null); setGoogleMapListener(null); destroyMapViewIfNecessary(); - getApplication().unregisterActivityLifecycleCallbacks(this); + Lifecycle lifecycle = lifecycleProvider.getLifecycle(); + if (lifecycle != null) { + lifecycle.removeObserver(this); + } } private void setGoogleMapListener(@Nullable GoogleMapListener listener) { @@ -556,73 +525,16 @@ private void setGoogleMapListener(@Nullable GoogleMapListener listener) { // does. This will override it when available even with the annotation commented out. public void onInputConnectionLocked() { // TODO(mklim): Remove this empty override once https://github.com/flutter/flutter/issues/40126 is fixed in stable. - }; + } // @Override // The minimum supported version of Flutter doesn't have this method on the PlatformView interface, but the maximum // does. This will override it when available even with the annotation commented out. public void onInputConnectionUnlocked() { // TODO(mklim): Remove this empty override once https://github.com/flutter/flutter/issues/40126 is fixed in stable. - }; - - // Application.ActivityLifecycleCallbacks methods - @Override - public void onActivityCreated(Activity activity, Bundle savedInstanceState) { - if (disposed || activity.hashCode() != getActivityHashCode()) { - return; - } - mapView.onCreate(savedInstanceState); - } - - @Override - public void onActivityStarted(Activity activity) { - if (disposed || activity.hashCode() != getActivityHashCode()) { - return; - } - mapView.onStart(); - } - - @Override - public void onActivityResumed(Activity activity) { - if (disposed || activity.hashCode() != getActivityHashCode()) { - return; - } - mapView.onResume(); - } - - @Override - public void onActivityPaused(Activity activity) { - if (disposed || activity.hashCode() != getActivityHashCode()) { - return; - } - mapView.onPause(); - } - - @Override - public void onActivityStopped(Activity activity) { - if (disposed || activity.hashCode() != getActivityHashCode()) { - return; - } - mapView.onStop(); - } - - @Override - public void onActivitySaveInstanceState(Activity activity, Bundle outState) { - if (disposed || activity.hashCode() != getActivityHashCode()) { - return; - } - mapView.onSaveInstanceState(outState); - } - - @Override - public void onActivityDestroyed(Activity activity) { - if (disposed || activity.hashCode() != getActivityHashCode()) { - return; - } - destroyMapViewIfNecessary(); } - // DefaultLifecycleObserver and OnSaveInstanceStateListener + // DefaultLifecycleObserver @Override public void onCreate(@NonNull LifecycleOwner owner) { @@ -666,6 +578,7 @@ public void onStop(@NonNull LifecycleOwner owner) { @Override public void onDestroy(@NonNull LifecycleOwner owner) { + owner.getLifecycle().removeObserver(this); if (disposed) { return; } @@ -798,7 +711,8 @@ public void setZoomControlsEnabled(boolean zoomControlsEnabled) { @Override public void setInitialMarkers(Object initialMarkers) { - this.initialMarkers = (List) initialMarkers; + ArrayList markers = (ArrayList) initialMarkers; + this.initialMarkers = markers != null ? new ArrayList<>(markers) : null; if (googleMap != null) { updateInitialMarkers(); } @@ -810,7 +724,8 @@ private void updateInitialMarkers() { @Override public void setInitialPolygons(Object initialPolygons) { - this.initialPolygons = (List) initialPolygons; + ArrayList polygons = (ArrayList) initialPolygons; + this.initialPolygons = polygons != null ? new ArrayList<>(polygons) : null; if (googleMap != null) { updateInitialPolygons(); } @@ -822,7 +737,8 @@ private void updateInitialPolygons() { @Override public void setInitialPolylines(Object initialPolylines) { - this.initialPolylines = (List) initialPolylines; + ArrayList polylines = (ArrayList) initialPolylines; + this.initialPolylines = polylines != null ? new ArrayList<>(polylines) : null; if (googleMap != null) { updateInitialPolylines(); } @@ -834,7 +750,8 @@ private void updateInitialPolylines() { @Override public void setInitialCircles(Object initialCircles) { - this.initialCircles = (List) initialCircles; + ArrayList circles = (ArrayList) initialCircles; + this.initialCircles = circles != null ? new ArrayList<>(circles) : null; if (googleMap != null) { updateInitialCircles(); } @@ -844,6 +761,18 @@ private void updateInitialCircles() { circlesController.addCircles(initialCircles); } + @Override + public void setInitialTileOverlays(List> initialTileOverlays) { + this.initialTileOverlays = initialTileOverlays; + if (googleMap != null) { + updateInitialTileOverlays(); + } + } + + private void updateInitialTileOverlays() { + tileOverlaysController.addTileOverlays(initialTileOverlays); + } + @SuppressLint("MissingPermission") private void updateMyLocationSettings() { if (hasLocationPermission()) { @@ -876,22 +805,6 @@ private int checkSelfPermission(String permission) { permission, android.os.Process.myPid(), android.os.Process.myUid()); } - private int getActivityHashCode() { - if (registrar != null && registrar.activity() != null) { - return registrar.activity().hashCode(); - } else { - return activityHashCode; - } - } - - private Application getApplication() { - if (registrar != null && registrar.activity() != null) { - return registrar.activity().getApplication(); - } else { - return mApplication; - } - } - private void destroyMapViewIfNecessary() { if (mapView == null) { return; @@ -916,16 +829,3 @@ public void setBuildingsEnabled(boolean buildingsEnabled) { this.buildingsEnabled = buildingsEnabled; } } - -interface GoogleMapListener - extends GoogleMap.OnCameraIdleListener, - GoogleMap.OnCameraMoveListener, - GoogleMap.OnCameraMoveStartedListener, - GoogleMap.OnInfoWindowClickListener, - GoogleMap.OnMarkerClickListener, - GoogleMap.OnPolygonClickListener, - GoogleMap.OnPolylineClickListener, - GoogleMap.OnCircleClickListener, - GoogleMap.OnMapClickListener, - GoogleMap.OnMapLongClickListener, - GoogleMap.OnMarkerDragListener {} diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java index b6bc7e5d4c45..ca9ac184a76e 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapFactory.java @@ -1,44 +1,27 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package io.flutter.plugins.googlemaps; -import android.app.Application; import android.content.Context; -import androidx.lifecycle.Lifecycle; import com.google.android.gms.maps.model.CameraPosition; import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.PluginRegistry; import io.flutter.plugin.common.StandardMessageCodec; import io.flutter.plugin.platform.PlatformView; import io.flutter.plugin.platform.PlatformViewFactory; +import java.util.List; import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; public class GoogleMapFactory extends PlatformViewFactory { - private final AtomicInteger mActivityState; private final BinaryMessenger binaryMessenger; - private final Application application; - private final int activityHashCode; - private final Lifecycle lifecycle; - private final PluginRegistry.Registrar registrar; // V1 embedding only. + private final LifecycleProvider lifecycleProvider; - GoogleMapFactory( - AtomicInteger state, - BinaryMessenger binaryMessenger, - Application application, - Lifecycle lifecycle, - PluginRegistry.Registrar registrar, - int activityHashCode) { + GoogleMapFactory(BinaryMessenger binaryMessenger, LifecycleProvider lifecycleProvider) { super(StandardMessageCodec.INSTANCE); - mActivityState = state; this.binaryMessenger = binaryMessenger; - this.application = application; - this.activityHashCode = activityHashCode; - this.lifecycle = lifecycle; - this.registrar = registrar; + this.lifecycleProvider = lifecycleProvider; } @SuppressWarnings("unchecked") @@ -64,14 +47,9 @@ public PlatformView create(Context context, int id, Object args) { if (params.containsKey("circlesToAdd")) { builder.setInitialCircles(params.get("circlesToAdd")); } - return builder.build( - id, - context, - mActivityState, - binaryMessenger, - application, - lifecycle, - registrar, - activityHashCode); + if (params.containsKey("tileOverlaysToAdd")) { + builder.setInitialTileOverlays((List>) params.get("tileOverlaysToAdd")); + } + return builder.build(id, context, binaryMessenger, lifecycleProvider); } } diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapListener.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapListener.java new file mode 100644 index 000000000000..0a5c3ec67e27 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapListener.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.googlemaps; + +import com.google.android.gms.maps.GoogleMap; + +interface GoogleMapListener + extends GoogleMap.OnCameraIdleListener, + GoogleMap.OnCameraMoveListener, + GoogleMap.OnCameraMoveStartedListener, + GoogleMap.OnInfoWindowClickListener, + GoogleMap.OnMarkerClickListener, + GoogleMap.OnPolygonClickListener, + GoogleMap.OnPolylineClickListener, + GoogleMap.OnCircleClickListener, + GoogleMap.OnMapClickListener, + GoogleMap.OnMapLongClickListener, + GoogleMap.OnMarkerDragListener {} diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java index 9e6fa2a27236..17f0d970a4ef 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapOptionsSink.java @@ -1,10 +1,12 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package io.flutter.plugins.googlemaps; import com.google.android.gms.maps.model.LatLngBounds; +import java.util.List; +import java.util.Map; /** Receiver of GoogleMap configuration options. */ interface GoogleMapOptionsSink { @@ -51,4 +53,6 @@ interface GoogleMapOptionsSink { void setInitialPolylines(Object initialPolylines); void setInitialCircles(Object initialCircles); + + void setInitialTileOverlays(List> initialTileOverlays); } diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java index f434a05bf1b7..763cd9e3e72e 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/GoogleMapsPlugin.java @@ -1,21 +1,22 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package io.flutter.plugins.googlemaps; import android.app.Activity; -import android.app.Application; +import android.app.Application.ActivityLifecycleCallbacks; import android.os.Bundle; import androidx.annotation.NonNull; -import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.annotation.Nullable; import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.Lifecycle.Event; import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.LifecycleRegistry; 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 java.util.concurrent.atomic.AtomicInteger; /** * Plugin for controlling a set of GoogleMap views to be shown as overlays on top of the Flutter @@ -23,38 +24,41 @@ * the map. A Texture drawn using GoogleMap bitmap snapshots can then be shown instead of the * overlay. */ -public class GoogleMapsPlugin - implements Application.ActivityLifecycleCallbacks, - FlutterPlugin, - ActivityAware, - DefaultLifecycleObserver { - static final int CREATED = 1; - static final int STARTED = 2; - static final int RESUMED = 3; - static final int PAUSED = 4; - static final int STOPPED = 5; - static final int DESTROYED = 6; - private final AtomicInteger state = new AtomicInteger(0); - private int registrarActivityHashCode; - private FlutterPluginBinding pluginBinding; - private Lifecycle lifecycle; +public class GoogleMapsPlugin implements FlutterPlugin, ActivityAware { + + @Nullable private Lifecycle lifecycle; private static final String VIEW_TYPE = "plugins.flutter.io/google_maps"; @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - if (registrar.activity() == null) { + public static void registerWith( + final io.flutter.plugin.common.PluginRegistry.Registrar registrar) { + final Activity activity = registrar.activity(); + if (activity == null) { // When a background flutter view tries to register the plugin, the registrar has no activity. // We stop the registration process as this plugin is foreground only. return; } - final GoogleMapsPlugin plugin = new GoogleMapsPlugin(registrar.activity()); - registrar.activity().getApplication().registerActivityLifecycleCallbacks(plugin); - registrar - .platformViewRegistry() - .registerViewFactory( - VIEW_TYPE, - new GoogleMapFactory(plugin.state, registrar.messenger(), null, null, registrar, -1)); + if (activity instanceof LifecycleOwner) { + registrar + .platformViewRegistry() + .registerViewFactory( + VIEW_TYPE, + new GoogleMapFactory( + registrar.messenger(), + new LifecycleProvider() { + @Override + public Lifecycle getLifecycle() { + return ((LifecycleOwner) activity).getLifecycle(); + } + })); + } else { + registrar + .platformViewRegistry() + .registerViewFactory( + VIEW_TYPE, + new GoogleMapFactory(registrar.messenger(), new ProxyLifecycleProvider(activity))); + } } public GoogleMapsPlugin() {} @@ -63,136 +67,119 @@ public GoogleMapsPlugin() {} @Override public void onAttachedToEngine(FlutterPluginBinding binding) { - pluginBinding = binding; - } - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { - pluginBinding = null; - } - - // ActivityAware - - @Override - public void onAttachedToActivity(ActivityPluginBinding binding) { - lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding); - lifecycle.addObserver(this); - pluginBinding + binding .getPlatformViewRegistry() .registerViewFactory( VIEW_TYPE, new GoogleMapFactory( - state, - pluginBinding.getBinaryMessenger(), - binding.getActivity().getApplication(), - lifecycle, - null, - binding.getActivity().hashCode())); + binding.getBinaryMessenger(), + new LifecycleProvider() { + @Nullable + @Override + public Lifecycle getLifecycle() { + return lifecycle; + } + })); } @Override - public void onDetachedFromActivity() { - lifecycle.removeObserver(this); - } + public void onDetachedFromEngine(FlutterPluginBinding binding) {} - @Override - public void onDetachedFromActivityForConfigChanges() { - this.onDetachedFromActivity(); - } + // ActivityAware @Override - public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { + public void onAttachedToActivity(ActivityPluginBinding binding) { lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding); - lifecycle.addObserver(this); - } - - // DefaultLifecycleObserver methods - - @Override - public void onCreate(@NonNull LifecycleOwner owner) { - state.set(CREATED); } @Override - public void onStart(@NonNull LifecycleOwner owner) { - state.set(STARTED); + public void onDetachedFromActivity() { + lifecycle = null; } @Override - public void onResume(@NonNull LifecycleOwner owner) { - state.set(RESUMED); + public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { + onAttachedToActivity(binding); } @Override - public void onPause(@NonNull LifecycleOwner owner) { - state.set(PAUSED); + public void onDetachedFromActivityForConfigChanges() { + onDetachedFromActivity(); } - @Override - public void onStop(@NonNull LifecycleOwner owner) { - state.set(STOPPED); - } + /** + * This class provides a {@link LifecycleOwner} for the activity driven by {@link + * ActivityLifecycleCallbacks}. + * + *

This is used in the case where a direct Lifecycle/Owner is not available. + */ + private static final class ProxyLifecycleProvider + implements ActivityLifecycleCallbacks, LifecycleOwner, LifecycleProvider { - @Override - public void onDestroy(@NonNull LifecycleOwner owner) { - state.set(DESTROYED); - } + private final LifecycleRegistry lifecycle = new LifecycleRegistry(this); + private final int registrarActivityHashCode; - // Application.ActivityLifecycleCallbacks methods + private ProxyLifecycleProvider(Activity activity) { + this.registrarActivityHashCode = activity.hashCode(); + activity.getApplication().registerActivityLifecycleCallbacks(this); + } - @Override - public void onActivityCreated(Activity activity, Bundle savedInstanceState) { - if (activity.hashCode() != registrarActivityHashCode) { - return; + @Override + public void onActivityCreated(Activity activity, Bundle savedInstanceState) { + if (activity.hashCode() != registrarActivityHashCode) { + return; + } + lifecycle.handleLifecycleEvent(Event.ON_CREATE); } - state.set(CREATED); - } - @Override - public void onActivityStarted(Activity activity) { - if (activity.hashCode() != registrarActivityHashCode) { - return; + @Override + public void onActivityStarted(Activity activity) { + if (activity.hashCode() != registrarActivityHashCode) { + return; + } + lifecycle.handleLifecycleEvent(Event.ON_START); } - state.set(STARTED); - } - @Override - public void onActivityResumed(Activity activity) { - if (activity.hashCode() != registrarActivityHashCode) { - return; + @Override + public void onActivityResumed(Activity activity) { + if (activity.hashCode() != registrarActivityHashCode) { + return; + } + lifecycle.handleLifecycleEvent(Event.ON_RESUME); } - state.set(RESUMED); - } - @Override - public void onActivityPaused(Activity activity) { - if (activity.hashCode() != registrarActivityHashCode) { - return; + @Override + public void onActivityPaused(Activity activity) { + if (activity.hashCode() != registrarActivityHashCode) { + return; + } + lifecycle.handleLifecycleEvent(Event.ON_PAUSE); } - state.set(PAUSED); - } - @Override - public void onActivityStopped(Activity activity) { - if (activity.hashCode() != registrarActivityHashCode) { - return; + @Override + public void onActivityStopped(Activity activity) { + if (activity.hashCode() != registrarActivityHashCode) { + return; + } + lifecycle.handleLifecycleEvent(Event.ON_STOP); } - state.set(STOPPED); - } - @Override - public void onActivitySaveInstanceState(Activity activity, Bundle outState) {} + @Override + public void onActivitySaveInstanceState(Activity activity, Bundle outState) {} - @Override - public void onActivityDestroyed(Activity activity) { - if (activity.hashCode() != registrarActivityHashCode) { - return; + @Override + public void onActivityDestroyed(Activity activity) { + if (activity.hashCode() != registrarActivityHashCode) { + return; + } + activity.getApplication().unregisterActivityLifecycleCallbacks(this); + lifecycle.handleLifecycleEvent(Event.ON_DESTROY); } - activity.getApplication().unregisterActivityLifecycleCallbacks(this); - state.set(DESTROYED); - } - private GoogleMapsPlugin(Activity activity) { - this.registrarActivityHashCode = activity.hashCode(); + @NonNull + @Override + public Lifecycle getLifecycle() { + return lifecycle; + } } } diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/LifecycleProvider.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/LifecycleProvider.java new file mode 100644 index 000000000000..a3b6c0a3adf0 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/LifecycleProvider.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.googlemaps; + +import androidx.annotation.Nullable; +import androidx.lifecycle.Lifecycle; + +interface LifecycleProvider { + + @Nullable + Lifecycle getLifecycle(); +} diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkerBuilder.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkerBuilder.java index 29e4de00c5b0..ecc5f01bc87c 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkerBuilder.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkerBuilder.java @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkerController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkerController.java index 412daee5cf68..5c568a1c9a1e 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkerController.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkerController.java @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkerOptionsSink.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkerOptionsSink.java index 3f853b9f1459..88c970c1f14b 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkerOptionsSink.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkerOptionsSink.java @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/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 70feb978af3f..189cba03c1cd 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonBuilder.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonBuilder.java index 600762afe4ee..072fa746958f 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonBuilder.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonBuilder.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -41,6 +41,13 @@ public void setPoints(List points) { polygonOptions.addAll(points); } + @Override + public void setHoles(List> holes) { + for (List hole : holes) { + polygonOptions.addHole(hole); + } + } + @Override public void setConsumeTapEvents(boolean consumeTapEvents) { this.consumeTapEvents = consumeTapEvents; diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonController.java index adb01b8a490a..e66f05e18f93 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonController.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonController.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -52,6 +52,10 @@ public void setPoints(List points) { polygon.setPoints(points); } + public void setHoles(List> holes) { + polygon.setHoles(holes); + } + @Override public void setVisible(boolean visible) { polygon.setVisible(visible); diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonOptionsSink.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonOptionsSink.java index df4dae0fda4e..e9b0ec1413a2 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonOptionsSink.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonOptionsSink.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -20,6 +20,8 @@ interface PolygonOptionsSink { void setPoints(List points); + void setHoles(List> holes); + void setVisible(boolean visible); void setStrokeWidth(float width); diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonsController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonsController.java index 07f2ad0f7c38..6f855db07996 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonsController.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolygonsController.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineBuilder.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineBuilder.java index 9fd242a4706f..9120a1618237 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineBuilder.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineBuilder.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineController.java index ec0fed83be49..8bd84f5906f2 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineController.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineController.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineOptionsSink.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineOptionsSink.java index adaf867b92d1..5b3f193617cb 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineOptionsSink.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylineOptionsSink.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylinesController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylinesController.java index a6ad61adc170..399634933dc9 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylinesController.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/PolylinesController.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayBuilder.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayBuilder.java new file mode 100644 index 000000000000..ecbc2f8f9ee1 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayBuilder.java @@ -0,0 +1,46 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemaps; + +import com.google.android.gms.maps.model.TileOverlayOptions; +import com.google.android.gms.maps.model.TileProvider; + +class TileOverlayBuilder implements TileOverlaySink { + + private final TileOverlayOptions tileOverlayOptions; + + TileOverlayBuilder() { + this.tileOverlayOptions = new TileOverlayOptions(); + } + + TileOverlayOptions build() { + return tileOverlayOptions; + } + + @Override + public void setFadeIn(boolean fadeIn) { + tileOverlayOptions.fadeIn(fadeIn); + } + + @Override + public void setTransparency(float transparency) { + tileOverlayOptions.transparency(transparency); + } + + @Override + public void setZIndex(float zIndex) { + tileOverlayOptions.zIndex(zIndex); + } + + @Override + public void setVisible(boolean visible) { + tileOverlayOptions.visible(visible); + } + + @Override + public void setTileProvider(TileProvider tileProvider) { + tileOverlayOptions.tileProvider(tileProvider); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayController.java new file mode 100644 index 000000000000..7405b5fcc496 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlayController.java @@ -0,0 +1,62 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemaps; + +import com.google.android.gms.maps.model.TileOverlay; +import com.google.android.gms.maps.model.TileProvider; +import java.util.HashMap; +import java.util.Map; + +class TileOverlayController implements TileOverlaySink { + + private final TileOverlay tileOverlay; + + TileOverlayController(TileOverlay tileOverlay) { + this.tileOverlay = tileOverlay; + } + + void remove() { + tileOverlay.remove(); + } + + void clearTileCache() { + tileOverlay.clearTileCache(); + } + + Map getTileOverlayInfo() { + Map tileOverlayInfo = new HashMap<>(); + tileOverlayInfo.put("fadeIn", tileOverlay.getFadeIn()); + tileOverlayInfo.put("transparency", tileOverlay.getTransparency()); + tileOverlayInfo.put("id", tileOverlay.getId()); + tileOverlayInfo.put("zIndex", tileOverlay.getZIndex()); + tileOverlayInfo.put("visible", tileOverlay.isVisible()); + return tileOverlayInfo; + } + + @Override + public void setFadeIn(boolean fadeIn) { + tileOverlay.setFadeIn(fadeIn); + } + + @Override + public void setTransparency(float transparency) { + tileOverlay.setTransparency(transparency); + } + + @Override + public void setZIndex(float zIndex) { + tileOverlay.setZIndex(zIndex); + } + + @Override + public void setVisible(boolean visible) { + tileOverlay.setVisible(visible); + } + + @Override + public void setTileProvider(TileProvider tileProvider) { + // You can not change tile provider after creation + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaySink.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaySink.java new file mode 100644 index 000000000000..d167af7d4a6d --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaySink.java @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemaps; + +import com.google.android.gms.maps.model.TileProvider; + +/** Receiver of TileOverlayOptions configuration. */ +interface TileOverlaySink { + void setFadeIn(boolean fadeIn); + + void setTransparency(float transparency); + + void setZIndex(float zIndex); + + void setVisible(boolean visible); + + void setTileProvider(TileProvider tileProvider); +} diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaysController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaysController.java new file mode 100644 index 000000000000..82a3edcb32c0 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileOverlaysController.java @@ -0,0 +1,120 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemaps; + +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.TileOverlay; +import com.google.android.gms.maps.model.TileOverlayOptions; +import io.flutter.plugin.common.MethodChannel; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +class TileOverlaysController { + + private final Map tileOverlayIdToController; + private final MethodChannel methodChannel; + private GoogleMap googleMap; + + TileOverlaysController(MethodChannel methodChannel) { + this.tileOverlayIdToController = new HashMap<>(); + this.methodChannel = methodChannel; + } + + void setGoogleMap(GoogleMap googleMap) { + this.googleMap = googleMap; + } + + void addTileOverlays(List> tileOverlaysToAdd) { + if (tileOverlaysToAdd == null) { + return; + } + for (Map tileOverlayToAdd : tileOverlaysToAdd) { + addTileOverlay(tileOverlayToAdd); + } + } + + void changeTileOverlays(List> tileOverlaysToChange) { + if (tileOverlaysToChange == null) { + return; + } + for (Map tileOverlayToChange : tileOverlaysToChange) { + changeTileOverlay(tileOverlayToChange); + } + } + + void removeTileOverlays(List tileOverlayIdsToRemove) { + if (tileOverlayIdsToRemove == null) { + return; + } + for (String tileOverlayId : tileOverlayIdsToRemove) { + if (tileOverlayId == null) { + continue; + } + removeTileOverlay(tileOverlayId); + } + } + + void clearTileCache(String tileOverlayId) { + if (tileOverlayId == null) { + return; + } + TileOverlayController tileOverlayController = tileOverlayIdToController.get(tileOverlayId); + if (tileOverlayController != null) { + tileOverlayController.clearTileCache(); + } + } + + Map getTileOverlayInfo(String tileOverlayId) { + if (tileOverlayId == null) { + return null; + } + TileOverlayController tileOverlayController = tileOverlayIdToController.get(tileOverlayId); + if (tileOverlayController == null) { + return null; + } + return tileOverlayController.getTileOverlayInfo(); + } + + private void addTileOverlay(Map tileOverlayOptions) { + if (tileOverlayOptions == null) { + return; + } + TileOverlayBuilder tileOverlayOptionsBuilder = new TileOverlayBuilder(); + String tileOverlayId = + Convert.interpretTileOverlayOptions(tileOverlayOptions, tileOverlayOptionsBuilder); + TileProviderController tileProviderController = + new TileProviderController(methodChannel, tileOverlayId); + tileOverlayOptionsBuilder.setTileProvider(tileProviderController); + TileOverlayOptions options = tileOverlayOptionsBuilder.build(); + TileOverlay tileOverlay = googleMap.addTileOverlay(options); + TileOverlayController tileOverlayController = new TileOverlayController(tileOverlay); + tileOverlayIdToController.put(tileOverlayId, tileOverlayController); + } + + private void changeTileOverlay(Map tileOverlayOptions) { + if (tileOverlayOptions == null) { + return; + } + String tileOverlayId = getTileOverlayId(tileOverlayOptions); + TileOverlayController tileOverlayController = tileOverlayIdToController.get(tileOverlayId); + if (tileOverlayController != null) { + Convert.interpretTileOverlayOptions(tileOverlayOptions, tileOverlayController); + } + } + + private void removeTileOverlay(String tileOverlayId) { + TileOverlayController tileOverlayController = tileOverlayIdToController.get(tileOverlayId); + if (tileOverlayController != null) { + tileOverlayController.remove(); + tileOverlayIdToController.remove(tileOverlayId); + } + } + + @SuppressWarnings("unchecked") + private static String getTileOverlayId(Map tileOverlay) { + return (String) tileOverlay.get("tileOverlayId"); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java new file mode 100644 index 000000000000..f05d04550994 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/TileProviderController.java @@ -0,0 +1,100 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemaps; + +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import androidx.annotation.NonNull; +import com.google.android.gms.maps.model.Tile; +import com.google.android.gms.maps.model.TileProvider; +import io.flutter.plugin.common.MethodChannel; +import java.util.Map; +import java.util.concurrent.CountDownLatch; + +class TileProviderController implements TileProvider { + + private static final String TAG = "TileProviderController"; + + private final String tileOverlayId; + private final MethodChannel methodChannel; + private final Handler handler = new Handler(Looper.getMainLooper()); + + TileProviderController(MethodChannel methodChannel, String tileOverlayId) { + this.tileOverlayId = tileOverlayId; + this.methodChannel = methodChannel; + } + + @Override + public Tile getTile(final int x, final int y, final int zoom) { + Worker worker = new Worker(x, y, zoom); + return worker.getTile(); + } + + private final class Worker implements MethodChannel.Result { + + private final CountDownLatch countDownLatch = new CountDownLatch(1); + private final int x; + private final int y; + private final int zoom; + private Map result; + + Worker(int x, int y, int zoom) { + this.x = x; + this.y = y; + this.zoom = zoom; + } + + @NonNull + Tile getTile() { + handler.post( + () -> + methodChannel.invokeMethod( + "tileOverlay#getTile", + Convert.tileOverlayArgumentsToJson(tileOverlayId, x, y, zoom), + this)); + try { + // Because `methodChannel.invokeMethod` is async, we use a `countDownLatch` make it synchronized. + countDownLatch.await(); + } catch (InterruptedException e) { + Log.e( + TAG, + String.format("countDownLatch: can't get tile: x = %d, y= %d, zoom = %d", x, y, zoom), + e); + return TileProvider.NO_TILE; + } + try { + return Convert.interpretTile(result); + } catch (Exception e) { + Log.e(TAG, "Can't parse tile data", e); + return TileProvider.NO_TILE; + } + } + + @Override + public void success(Object data) { + result = (Map) data; + countDownLatch.countDown(); + } + + @Override + public void error(String errorCode, String errorMessage, Object data) { + Log.e( + TAG, + String.format( + "Can't get tile: errorCode = %s, errorMessage = %s, date = %s", + errorCode, errorCode, data)); + result = null; + countDownLatch.countDown(); + } + + @Override + public void notImplemented() { + Log.e(TAG, "Can't get tile: notImplemented"); + result = null; + countDownLatch.countDown(); + } + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/CircleBuilderTest.java b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/CircleBuilderTest.java index 6585090e6e26..269c35ebd864 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/CircleBuilderTest.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/CircleBuilderTest.java @@ -1,3 +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. + package io.flutter.plugins.googlemaps; import static junit.framework.TestCase.assertEquals; 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 e032dd436d5a..72a8cab626b5 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 @@ -1,3 +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. + package io.flutter.plugins.googlemaps; import static org.mockito.Mockito.mock; diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolygonBuilderTest.java b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolygonBuilderTest.java index 644e8982f246..c781afc0ede9 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolygonBuilderTest.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolygonBuilderTest.java @@ -1,3 +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. + package io.flutter.plugins.googlemaps; import static junit.framework.TestCase.assertEquals; 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 834c42766e07..29234b6adb42 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 @@ -1,3 +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. + package io.flutter.plugins.googlemaps; import static org.mockito.Mockito.mock; diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolylineBuilderTest.java b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolylineBuilderTest.java index bf6d06066fbf..9e2e9e81b829 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolylineBuilderTest.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/PolylineBuilderTest.java @@ -1,3 +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. + package io.flutter.plugins.googlemaps; import static junit.framework.TestCase.assertEquals; 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 acd231623825..bb7653aa2293 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 @@ -1,3 +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. + package io.flutter.plugins.googlemaps; import static org.mockito.Mockito.mock; 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 800387342121..b92b9c326143 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/README.md +++ b/packages/google_maps_flutter/google_maps_flutter/example/README.md @@ -5,4 +5,4 @@ Demonstrates how to use the google_maps_flutter plugin. ## Getting Started For help getting started with Flutter, view our online -[documentation](https://flutter.io/). +[documentation](https://flutter.dev/). diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/build.gradle b/packages/google_maps_flutter/google_maps_flutter/example/android/app/build.gradle index 786c7845db94..1a8cdf52cc46 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 28 + compileSdkVersion 29 lintOptions { disable 'InvalidPackage' @@ -33,7 +33,7 @@ android { defaultConfig { applicationId "io.flutter.plugins.googlemapsexample" - minSdkVersion 16 + minSdkVersion 20 targetSdkVersion 28 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemaps/EmbeddingV1ActivityTest.java b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemaps/EmbeddingV1ActivityTest.java index 76dc5df3422a..9da7185b8ace 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemaps/EmbeddingV1ActivityTest.java +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemaps/EmbeddingV1ActivityTest.java @@ -1,3 +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. + package io.flutter.plugins.googlemaps; import androidx.test.rule.ActivityTestRule; diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemaps/MainActivityTest.java b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemaps/MainActivityTest.java index c70f16a17454..fccd4c95c3ac 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemaps/MainActivityTest.java +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemaps/MainActivityTest.java @@ -1,3 +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. + package io.flutter.plugins.googlemaps; import androidx.test.rule.ActivityTestRule; diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/main/java/io/flutter/plugins/googlemapsexample/EmbeddingV1Activity.java b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/main/java/io/flutter/plugins/googlemapsexample/EmbeddingV1Activity.java index 3853b5d2d4df..cecf76a690e0 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/main/java/io/flutter/plugins/googlemapsexample/EmbeddingV1Activity.java +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/main/java/io/flutter/plugins/googlemapsexample/EmbeddingV1Activity.java @@ -1,3 +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. + package io.flutter.plugins.googlemapsexample; import android.os.Bundle; diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java index 04b5f6fd3793..2a81479988e0 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java @@ -1,41 +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. + package io.flutter.plugins.googlemaps; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; -import android.app.Application; import android.content.Context; -import androidx.lifecycle.LifecycleOwner; +import androidx.activity.ComponentActivity; import androidx.test.core.app.ApplicationProvider; import com.google.android.gms.maps.GoogleMap; import io.flutter.plugin.common.BinaryMessenger; -import java.util.concurrent.atomic.AtomicInteger; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; @RunWith(RobolectricTestRunner.class) public class GoogleMapControllerTest { private Context context; - private Application application; + private ComponentActivity activity; private GoogleMapController googleMapController; @Mock BinaryMessenger mockMessenger; @Mock GoogleMap mockGoogleMap; - @Mock LifecycleOwner lifecycleOwner; @Before public void before() { MockitoAnnotations.initMocks(this); context = ApplicationProvider.getApplicationContext(); - application = ApplicationProvider.getApplicationContext(); + activity = Robolectric.setupActivity(ComponentActivity.class); googleMapController = - new GoogleMapController( - 0, context, new AtomicInteger(1), mockMessenger, application, null, null, 0, null); + new GoogleMapController(0, context, mockMessenger, activity::getLifecycle, null); googleMapController.init(); } @@ -51,7 +52,7 @@ public void DisposeReleaseTheMap() throws InterruptedException { public void OnDestroyReleaseTheMap() throws InterruptedException { googleMapController.onMapReady(mockGoogleMap); assertTrue(googleMapController != null); - googleMapController.onDestroy(lifecycleOwner); + googleMapController.onDestroy(activity); assertNull(googleMapController.getView()); } } 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 e0d7ae2c11af..456d020f6e2c 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 @@ -1,7 +1,7 @@ buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -12,7 +12,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_map_inspector.dart b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_map_inspector.dart index 2fcfec15713a..a4833fe8561d 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_map_inspector.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_map_inspector.dart @@ -1,6 +1,6 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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'; @@ -16,64 +16,71 @@ class GoogleMapInspector { final MethodChannel _channel; - Future isCompassEnabled() async { + Future isCompassEnabled() async { return await _channel.invokeMethod('map#isCompassEnabled'); } - Future isMapToolbarEnabled() async { + Future isMapToolbarEnabled() async { return await _channel.invokeMethod('map#isMapToolbarEnabled'); } Future getMinMaxZoomLevels() async { final List zoomLevels = - (await _channel.invokeMethod>('map#getMinMaxZoomLevels')) + (await _channel.invokeMethod>('map#getMinMaxZoomLevels'))! .cast(); return MinMaxZoomPreference(zoomLevels[0], zoomLevels[1]); } - Future getZoomLevel() async { - final double zoomLevel = + Future getZoomLevel() async { + final double? zoomLevel = await _channel.invokeMethod('map#getZoomLevel'); return zoomLevel; } - Future isZoomGesturesEnabled() async { + Future isZoomGesturesEnabled() async { return await _channel.invokeMethod('map#isZoomGesturesEnabled'); } - Future isZoomControlsEnabled() async { + Future isZoomControlsEnabled() async { return await _channel.invokeMethod('map#isZoomControlsEnabled'); } - Future isLiteModeEnabled() async { + Future isLiteModeEnabled() async { return await _channel.invokeMethod('map#isLiteModeEnabled'); } - Future isRotateGesturesEnabled() async { + Future isRotateGesturesEnabled() async { return await _channel.invokeMethod('map#isRotateGesturesEnabled'); } - Future isTiltGesturesEnabled() async { + Future isTiltGesturesEnabled() async { return await _channel.invokeMethod('map#isTiltGesturesEnabled'); } - Future isScrollGesturesEnabled() async { + Future isScrollGesturesEnabled() async { return await _channel.invokeMethod('map#isScrollGesturesEnabled'); } - Future isMyLocationButtonEnabled() async { + Future isMyLocationButtonEnabled() async { return await _channel.invokeMethod('map#isMyLocationButtonEnabled'); } - Future isTrafficEnabled() async { + Future isTrafficEnabled() async { return await _channel.invokeMethod('map#isTrafficEnabled'); } - Future isBuildingsEnabled() async { + Future isBuildingsEnabled() async { return await _channel.invokeMethod('map#isBuildingsEnabled'); } - Future takeSnapshot() async { + Future takeSnapshot() async { return await _channel.invokeMethod('map#takeSnapshot'); } + + Future?> getTileOverlayInfo(String id) async { + return (await _channel.invokeMapMethod( + 'map#getTileOverlayInfo', { + 'tileOverlayId': id, + })); + } } diff --git a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart index 2a5bf80a4578..8bafca15c344 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart @@ -1,10 +1,11 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; +import 'dart:ui' as ui; import 'package:integration_test/integration_test.dart'; import 'package:flutter/material.dart'; @@ -35,14 +36,14 @@ void main() { onMapCreated: (GoogleMapController controller) { final GoogleMapInspector inspector = // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel); + GoogleMapInspector(controller.channel!); inspectorCompleter.complete(inspector); }, ), )); final GoogleMapInspector inspector = await inspectorCompleter.future; - bool compassEnabled = await inspector.isCompassEnabled(); + bool? compassEnabled = await inspector.isCompassEnabled(); expect(compassEnabled, false); await tester.pumpWidget(Directionality( @@ -75,14 +76,14 @@ void main() { onMapCreated: (GoogleMapController controller) { final GoogleMapInspector inspector = // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel); + GoogleMapInspector(controller.channel!); inspectorCompleter.complete(inspector); }, ), )); final GoogleMapInspector inspector = await inspectorCompleter.future; - bool mapToolbarEnabled = await inspector.isMapToolbarEnabled(); + bool? mapToolbarEnabled = await inspector.isMapToolbarEnabled(); expect(mapToolbarEnabled, false); await tester.pumpWidget(Directionality( @@ -115,7 +116,7 @@ void main() { final Key key = GlobalKey(); final Completer inspectorCompleter = Completer(); - GoogleMapController controller; + late GoogleMapController controller; const MinMaxZoomPreference initialZoomLevel = MinMaxZoomPreference(4, 8); const MinMaxZoomPreference finalZoomLevel = MinMaxZoomPreference(6, 10); @@ -129,7 +130,7 @@ void main() { onMapCreated: (GoogleMapController c) async { final GoogleMapInspector inspector = // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(c.channel); + GoogleMapInspector(c.channel!); controller = c; inspectorCompleter.complete(inspector); }, @@ -144,7 +145,7 @@ void main() { } else if (Platform.isAndroid) { await controller.moveCamera(CameraUpdate.zoomTo(15)); await tester.pumpAndSettle(); - double zoomLevel = await inspector.getZoomLevel(); + double? zoomLevel = await inspector.getZoomLevel(); expect(zoomLevel, equals(initialZoomLevel.maxZoom)); await controller.moveCamera(CameraUpdate.zoomTo(1)); @@ -171,7 +172,7 @@ void main() { } else { await controller.moveCamera(CameraUpdate.zoomTo(15)); await tester.pumpAndSettle(); - double zoomLevel = await inspector.getZoomLevel(); + double? zoomLevel = await inspector.getZoomLevel(); expect(zoomLevel, equals(finalZoomLevel.maxZoom)); await controller.moveCamera(CameraUpdate.zoomTo(1)); @@ -195,14 +196,14 @@ void main() { onMapCreated: (GoogleMapController controller) { final GoogleMapInspector inspector = // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel); + GoogleMapInspector(controller.channel!); inspectorCompleter.complete(inspector); }, ), )); final GoogleMapInspector inspector = await inspectorCompleter.future; - bool zoomGesturesEnabled = await inspector.isZoomGesturesEnabled(); + bool? zoomGesturesEnabled = await inspector.isZoomGesturesEnabled(); expect(zoomGesturesEnabled, false); await tester.pumpWidget(Directionality( @@ -234,14 +235,14 @@ void main() { onMapCreated: (GoogleMapController controller) { final GoogleMapInspector inspector = // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel); + GoogleMapInspector(controller.channel!); inspectorCompleter.complete(inspector); }, ), )); final GoogleMapInspector inspector = await inspectorCompleter.future; - bool zoomControlsEnabled = await inspector.isZoomControlsEnabled(); + bool? zoomControlsEnabled = await inspector.isZoomControlsEnabled(); expect(zoomControlsEnabled, Platform.isIOS ? false : true); /// Zoom Controls functionality is not available on iOS at the moment. @@ -277,14 +278,14 @@ void main() { onMapCreated: (GoogleMapController controller) { final GoogleMapInspector inspector = // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel); + GoogleMapInspector(controller.channel!); inspectorCompleter.complete(inspector); }, ), )); final GoogleMapInspector inspector = await inspectorCompleter.future; - bool liteModeEnabled = await inspector.isLiteModeEnabled(); + bool? liteModeEnabled = await inspector.isLiteModeEnabled(); expect(liteModeEnabled, false); await tester.pumpWidget(Directionality( @@ -317,14 +318,14 @@ void main() { onMapCreated: (GoogleMapController controller) { final GoogleMapInspector inspector = // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel); + GoogleMapInspector(controller.channel!); inspectorCompleter.complete(inspector); }, ), )); final GoogleMapInspector inspector = await inspectorCompleter.future; - bool rotateGesturesEnabled = await inspector.isRotateGesturesEnabled(); + bool? rotateGesturesEnabled = await inspector.isRotateGesturesEnabled(); expect(rotateGesturesEnabled, false); await tester.pumpWidget(Directionality( @@ -357,14 +358,14 @@ void main() { onMapCreated: (GoogleMapController controller) { final GoogleMapInspector inspector = // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel); + GoogleMapInspector(controller.channel!); inspectorCompleter.complete(inspector); }, ), )); final GoogleMapInspector inspector = await inspectorCompleter.future; - bool tiltGesturesEnabled = await inspector.isTiltGesturesEnabled(); + bool? tiltGesturesEnabled = await inspector.isTiltGesturesEnabled(); expect(tiltGesturesEnabled, false); await tester.pumpWidget(Directionality( @@ -397,14 +398,14 @@ void main() { onMapCreated: (GoogleMapController controller) { final GoogleMapInspector inspector = // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel); + GoogleMapInspector(controller.channel!); inspectorCompleter.complete(inspector); }, ), )); final GoogleMapInspector inspector = await inspectorCompleter.future; - bool scrollGesturesEnabled = await inspector.isScrollGesturesEnabled(); + bool? scrollGesturesEnabled = await inspector.isScrollGesturesEnabled(); expect(scrollGesturesEnabled, false); await tester.pumpWidget(Directionality( @@ -555,14 +556,14 @@ void main() { onMapCreated: (GoogleMapController controller) { final GoogleMapInspector inspector = // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel); + GoogleMapInspector(controller.channel!); inspectorCompleter.complete(inspector); }, ), )); final GoogleMapInspector inspector = await inspectorCompleter.future; - bool isTrafficEnabled = await inspector.isTrafficEnabled(); + bool? isTrafficEnabled = await inspector.isTrafficEnabled(); expect(isTrafficEnabled, true); await tester.pumpWidget(Directionality( @@ -595,14 +596,14 @@ void main() { onMapCreated: (GoogleMapController controller) { final GoogleMapInspector inspector = // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel); + GoogleMapInspector(controller.channel!); inspectorCompleter.complete(inspector); }, ), )); final GoogleMapInspector inspector = await inspectorCompleter.future; - final bool isBuildingsEnabled = await inspector.isBuildingsEnabled(); + final bool? isBuildingsEnabled = await inspector.isBuildingsEnabled(); expect(isBuildingsEnabled, true); }); @@ -622,14 +623,14 @@ void main() { onMapCreated: (GoogleMapController controller) { final GoogleMapInspector inspector = // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel); + GoogleMapInspector(controller.channel!); inspectorCompleter.complete(inspector); }, ), )); final GoogleMapInspector inspector = await inspectorCompleter.future; - bool myLocationButtonEnabled = await inspector.isMyLocationButtonEnabled(); + bool? myLocationButtonEnabled = await inspector.isMyLocationButtonEnabled(); expect(myLocationButtonEnabled, true); await tester.pumpWidget(Directionality( @@ -665,14 +666,14 @@ void main() { onMapCreated: (GoogleMapController controller) { final GoogleMapInspector inspector = // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel); + GoogleMapInspector(controller.channel!); inspectorCompleter.complete(inspector); }, ), )); final GoogleMapInspector inspector = await inspectorCompleter.future; - final bool myLocationButtonEnabled = + final bool? myLocationButtonEnabled = await inspector.isMyLocationButtonEnabled(); expect(myLocationButtonEnabled, false); }, skip: Platform.isAndroid); @@ -693,14 +694,14 @@ void main() { onMapCreated: (GoogleMapController controller) { final GoogleMapInspector inspector = // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel); + GoogleMapInspector(controller.channel!); inspectorCompleter.complete(inspector); }, ), )); final GoogleMapInspector inspector = await inspectorCompleter.future; - final bool myLocationButtonEnabled = + final bool? myLocationButtonEnabled = await inspector.isMyLocationButtonEnabled(); expect(myLocationButtonEnabled, true); }, skip: Platform.isAndroid); @@ -953,10 +954,8 @@ void main() { final BitmapDescriptor scaled = await BitmapDescriptor.fromAssetImage( imageConfiguration, 'red_square.png', mipmaps: false); - // ignore: invalid_use_of_visible_for_testing_member - expect(mip.toJson()[2], 1); - // ignore: invalid_use_of_visible_for_testing_member - expect(scaled.toJson()[2], 2); + expect((mip.toJson() as List)[2], 1); + expect((scaled.toJson() as List)[2], 2); }); testWidgets('testTakeSnapshot', (WidgetTester tester) async { @@ -971,7 +970,7 @@ void main() { onMapCreated: (GoogleMapController controller) { final GoogleMapInspector inspector = // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel); + GoogleMapInspector(controller.channel!); inspectorCompleter.complete(inspector); }, ), @@ -981,10 +980,253 @@ void main() { await tester.pumpAndSettle(const Duration(seconds: 3)); final GoogleMapInspector inspector = await inspectorCompleter.future; - final Uint8List bytes = await inspector.takeSnapshot(); + final Uint8List? bytes = await inspector.takeSnapshot(); expect(bytes?.isNotEmpty, true); }, // TODO(cyanglaz): un-skip the test when we can test this on CI with API key enabled. // https://github.com/flutter/flutter/issues/57057 skip: Platform.isAndroid); + + testWidgets( + 'set tileOverlay correctly', + (WidgetTester tester) async { + Completer inspectorCompleter = + Completer(); + final TileOverlay tileOverlay1 = TileOverlay( + tileOverlayId: TileOverlayId('tile_overlay_1'), + tileProvider: _DebugTileProvider(), + zIndex: 2, + visible: true, + transparency: 0.2, + fadeIn: true, + ); + + final TileOverlay tileOverlay2 = TileOverlay( + tileOverlayId: TileOverlayId('tile_overlay_2'), + tileProvider: _DebugTileProvider(), + zIndex: 1, + visible: false, + transparency: 0.3, + fadeIn: false, + ); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: _kInitialCameraPosition, + tileOverlays: {tileOverlay1, tileOverlay2}, + onMapCreated: (GoogleMapController controller) { + final GoogleMapInspector inspector = + // ignore: invalid_use_of_visible_for_testing_member + GoogleMapInspector(controller.channel!); + inspectorCompleter.complete(inspector); + }, + ), + ), + ); + await tester.pumpAndSettle(const Duration(seconds: 3)); + + final GoogleMapInspector inspector = await inspectorCompleter.future; + + Map tileOverlayInfo1 = + (await inspector.getTileOverlayInfo('tile_overlay_1'))!; + Map tileOverlayInfo2 = + (await inspector.getTileOverlayInfo('tile_overlay_2'))!; + + expect(tileOverlayInfo1['visible'], isTrue); + expect(tileOverlayInfo1['fadeIn'], isTrue); + expect(tileOverlayInfo1['transparency'], + moreOrLessEquals(0.2, epsilon: 0.001)); + expect(tileOverlayInfo1['zIndex'], 2); + + expect(tileOverlayInfo2['visible'], isFalse); + expect(tileOverlayInfo2['fadeIn'], isFalse); + expect(tileOverlayInfo2['transparency'], + moreOrLessEquals(0.3, epsilon: 0.001)); + expect(tileOverlayInfo2['zIndex'], 1); + }, + ); + + testWidgets( + 'update tileOverlays correctly', + (WidgetTester tester) async { + Completer inspectorCompleter = + Completer(); + final Key key = GlobalKey(); + final TileOverlay tileOverlay1 = TileOverlay( + tileOverlayId: TileOverlayId('tile_overlay_1'), + tileProvider: _DebugTileProvider(), + zIndex: 2, + visible: true, + transparency: 0.2, + fadeIn: true, + ); + + final TileOverlay tileOverlay2 = TileOverlay( + tileOverlayId: TileOverlayId('tile_overlay_2'), + tileProvider: _DebugTileProvider(), + zIndex: 3, + visible: true, + transparency: 0.5, + fadeIn: true, + ); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + tileOverlays: {tileOverlay1, tileOverlay2}, + onMapCreated: (GoogleMapController controller) { + final GoogleMapInspector inspector = + // ignore: invalid_use_of_visible_for_testing_member + GoogleMapInspector(controller.channel!); + inspectorCompleter.complete(inspector); + }, + ), + ), + ); + + final GoogleMapInspector inspector = await inspectorCompleter.future; + + final TileOverlay tileOverlay1New = TileOverlay( + tileOverlayId: TileOverlayId('tile_overlay_1'), + tileProvider: _DebugTileProvider(), + zIndex: 1, + visible: false, + transparency: 0.3, + fadeIn: false, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + tileOverlays: {tileOverlay1New}, + onMapCreated: (GoogleMapController controller) { + fail('update: OnMapCreated should get called only once.'); + }, + ), + ), + ); + + await tester.pumpAndSettle(const Duration(seconds: 3)); + + Map tileOverlayInfo1 = + (await inspector.getTileOverlayInfo('tile_overlay_1'))!; + Map? tileOverlayInfo2 = + await inspector.getTileOverlayInfo('tile_overlay_2'); + + expect(tileOverlayInfo1['visible'], isFalse); + expect(tileOverlayInfo1['fadeIn'], isFalse); + expect(tileOverlayInfo1['transparency'], + moreOrLessEquals(0.3, epsilon: 0.001)); + expect(tileOverlayInfo1['zIndex'], 1); + + expect(tileOverlayInfo2, isNull); + }, + ); + + testWidgets( + 'remove tileOverlays correctly', + (WidgetTester tester) async { + Completer inspectorCompleter = + Completer(); + final Key key = GlobalKey(); + final TileOverlay tileOverlay1 = TileOverlay( + tileOverlayId: TileOverlayId('tile_overlay_1'), + tileProvider: _DebugTileProvider(), + zIndex: 2, + visible: true, + transparency: 0.2, + fadeIn: true, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + tileOverlays: {tileOverlay1}, + onMapCreated: (GoogleMapController controller) { + final GoogleMapInspector inspector = + // ignore: invalid_use_of_visible_for_testing_member + GoogleMapInspector(controller.channel!); + inspectorCompleter.complete(inspector); + }, + ), + ), + ); + + final GoogleMapInspector inspector = await inspectorCompleter.future; + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (GoogleMapController controller) { + fail('OnMapCreated should get called only once.'); + }, + ), + ), + ); + + await tester.pumpAndSettle(const Duration(seconds: 3)); + Map? tileOverlayInfo1 = + await inspector.getTileOverlayInfo('tile_overlay_1'); + + expect(tileOverlayInfo1, isNull); + }, + ); +} + +class _DebugTileProvider implements TileProvider { + _DebugTileProvider() { + boxPaint.isAntiAlias = true; + boxPaint.color = Colors.blue; + boxPaint.strokeWidth = 2.0; + boxPaint.style = PaintingStyle.stroke; + } + + static const int width = 100; + static const int height = 100; + static final Paint boxPaint = Paint(); + static final TextStyle textStyle = TextStyle( + color: Colors.red, + fontSize: 20, + ); + + @override + Future getTile(int x, int y, int? zoom) async { + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final Canvas canvas = Canvas(recorder); + final TextSpan textSpan = TextSpan( + text: "$x,$y", + style: textStyle, + ); + final TextPainter textPainter = TextPainter( + text: textSpan, + textDirection: TextDirection.ltr, + ); + textPainter.layout( + minWidth: 0.0, + maxWidth: width.toDouble(), + ); + final Offset offset = const Offset(0, 0); + textPainter.paint(canvas, offset); + canvas.drawRect( + Rect.fromLTRB(0, 0, width.toDouble(), width.toDouble()), boxPaint); + final ui.Picture picture = recorder.endRecording(); + final Uint8List byteData = await picture + .toImage(width, height) + .then((ui.Image image) => + image.toByteData(format: ui.ImageByteFormat.png)) + .then((ByteData? byteData) => byteData!.buffer.asUint8List()); + return Tile(width, height, byteData); + } } diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Podfile b/packages/google_maps_flutter/google_maps_flutter/example/ios/Podfile new file mode 100644 index 000000000000..3924e59aa0f9 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/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/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj index f6a2d6ec291a..2093134f0bfb 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,18 +9,34 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 4510D964F3B1259FEDD3ABA6 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + F7151F13265D7ED70028CB91 /* GoogleMapsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F12265D7ED70028CB91 /* GoogleMapsTests.m */; }; + F7151F21265D7EE50028CB91 /* GoogleMapsUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F20265D7EE50028CB91 /* GoogleMapsUITests.m */; }; + FC8F35FC8CD533B128950487 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = F267F68029D1A4E2E4C572A7 /* libPods-RunnerTests.a */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + F7151F15265D7ED70028CB91 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; + F7151F23265D7EE50028CB91 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -28,8 +44,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -40,14 +54,13 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; + 733AFAB37683A9DA7512F09C /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -55,7 +68,15 @@ 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 = ""; }; 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 = ""; }; + F267F68029D1A4E2E4C572A7 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + F7151F10265D7ED70028CB91 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F7151F12265D7ED70028CB91 /* GoogleMapsTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoogleMapsTests.m; sourceTree = ""; }; + F7151F14265D7ED70028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + F7151F1E265D7EE50028CB91 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F7151F20265D7EE50028CB91 /* GoogleMapsUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GoogleMapsUITests.m; sourceTree = ""; }; + F7151F22265D7EE50028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -63,12 +84,25 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 4510D964F3B1259FEDD3ABA6 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; + F7151F0D265D7ED70028CB91 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FC8F35FC8CD533B128950487 /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F1B265D7EE50028CB91 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -76,6 +110,7 @@ isa = PBXGroup; children = ( 7755F8F4BABC3D6A0BD4048B /* libPods-Runner.a */, + F267F68029D1A4E2E4C572A7 /* libPods-RunnerTests.a */, ); name = Frameworks; sourceTree = ""; @@ -83,9 +118,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, @@ -98,6 +131,8 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, + F7151F11265D7ED70028CB91 /* RunnerTests */, + F7151F1F265D7EE50028CB91 /* RunnerUITests */, 97C146EF1CF9000F007C117D /* Products */, A189CFE5474BF8A07908B2E0 /* Pods */, 1E7CF0857EFC88FC263CF3B2 /* Frameworks */, @@ -108,6 +143,8 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + F7151F10265D7ED70028CB91 /* RunnerTests.xctest */, + F7151F1E265D7EE50028CB91 /* RunnerUITests.xctest */, ); name = Products; sourceTree = ""; @@ -141,10 +178,30 @@ children = ( B7AFC65E3DD5AC60D834D83D /* Pods-Runner.debug.xcconfig */, EA0E91726245EDC22B97E8B9 /* Pods-Runner.release.xcconfig */, + E52C6A6210A56F027C582EF9 /* Pods-RunnerTests.debug.xcconfig */, + 733AFAB37683A9DA7512F09C /* Pods-RunnerTests.release.xcconfig */, ); name = Pods; sourceTree = ""; }; + F7151F11265D7ED70028CB91 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + F7151F12265D7ED70028CB91 /* GoogleMapsTests.m */, + F7151F14265D7ED70028CB91 /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; + F7151F1F265D7EE50028CB91 /* RunnerUITests */ = { + isa = PBXGroup; + children = ( + F7151F20265D7EE50028CB91 /* GoogleMapsUITests.m */, + F7151F22265D7EE50028CB91 /* Info.plist */, + ); + path = RunnerUITests; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -159,7 +216,6 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - FE7DE34E225BB9A5F4DB58C6 /* [CP] Embed Pods Frameworks */, BB6BD9A1101E970BEF85B6D2 /* [CP] Copy Pods Resources */, ); buildRules = ( @@ -171,6 +227,43 @@ productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; + F7151F0F265D7ED70028CB91 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F7151F19265D7ED70028CB91 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + D067548A17DC238B80D2BD12 /* [CP] Check Pods Manifest.lock */, + F7151F0C265D7ED70028CB91 /* Sources */, + F7151F0D265D7ED70028CB91 /* Frameworks */, + F7151F0E265D7ED70028CB91 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F7151F16265D7ED70028CB91 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = F7151F10265D7ED70028CB91 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + F7151F1D265D7EE50028CB91 /* RunnerUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F7151F25265D7EE50028CB91 /* Build configuration list for PBXNativeTarget "RunnerUITests" */; + buildPhases = ( + F7151F1A265D7EE50028CB91 /* Sources */, + F7151F1B265D7EE50028CB91 /* Frameworks */, + F7151F1C265D7EE50028CB91 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F7151F24265D7EE50028CB91 /* PBXTargetDependency */, + ); + name = RunnerUITests; + productName = RunnerUITests; + productReference = F7151F1E265D7EE50028CB91 /* RunnerUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -178,11 +271,21 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; + ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; }; + F7151F0F265D7ED70028CB91 = { + CreatedOnToolsVersion = 12.5; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + F7151F1D265D7EE50028CB91 = { + CreatedOnToolsVersion = 12.5; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; @@ -199,6 +302,8 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + F7151F0F265D7ED70028CB91 /* RunnerTests */, + F7151F1D265D7EE50028CB91 /* RunnerUITests */, ); }; /* End PBXProject section */ @@ -215,6 +320,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F7151F0E265D7ED70028CB91 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F1C265D7EE50028CB91 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -230,7 +349,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 74BF216DF17B0C7F983459BD /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; @@ -270,28 +389,38 @@ files = ( ); inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_ROOT}/GoogleMaps/Maps/Frameworks/GoogleMaps.framework/Resources/GoogleMaps.bundle", ); name = "[CP] Copy Pods Resources"; outputPaths = ( + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleMaps.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; showEnvVarsInLog = 0; }; - FE7DE34E225BB9A5F4DB58C6 /* [CP] Embed Pods Frameworks */ = { + D067548A17DC238B80D2BD12 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\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; }; /* End PBXShellScriptBuildPhase section */ @@ -307,8 +436,37 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F7151F0C265D7ED70028CB91 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F7151F13265D7ED70028CB91 /* GoogleMapsTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F1A265D7EE50028CB91 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F7151F21265D7EE50028CB91 /* GoogleMapsUITests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + F7151F16265D7ED70028CB91 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = F7151F15265D7ED70028CB91 /* PBXContainerItemProxy */; + }; + F7151F24265D7EE50028CB91 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = F7151F23265D7EE50028CB91 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -331,7 +489,6 @@ /* Begin XCBuildConfiguration section */ 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -388,7 +545,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -453,7 +609,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.googleMobileMapsExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.googleMobileMapsExample; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -474,11 +630,67 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.googleMobileMapsExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.googleMobileMapsExample; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; }; + F7151F17265D7ED70028CB91 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E52C6A6210A56F027C582EF9 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Debug; + }; + F7151F18265D7ED70028CB91 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 733AFAB37683A9DA7512F09C /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Release; + }; + F7151F26265D7EE50028CB91 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_TARGET_NAME = Runner; + }; + name = Debug; + }; + F7151F27265D7EE50028CB91 /* Release */ = { + isa = XCBuildConfiguration; + 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 */ @@ -500,6 +712,24 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + F7151F19265D7ED70028CB91 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F7151F17265D7ED70028CB91 /* Debug */, + F7151F18265D7ED70028CB91 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + F7151F25265D7EE50028CB91 /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F7151F26265D7EE50028CB91 /* Debug */, + F7151F27265D7EE50028CB91 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16ed0f..919434a6254f 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> 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 3bb3697ef41c..afdb55fdfbdd 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 @@ -37,6 +37,26 @@ + + + + + + + + #import diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner/AppDelegate.m b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner/AppDelegate.m index 6896c5c190b1..d050cf771c8f 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner/AppDelegate.m +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner/AppDelegate.m @@ -1,3 +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 "AppDelegate.h" #import "GeneratedPluginRegistrant.h" diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner/Info.plist b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner/Info.plist index 372490e1a367..0fa9c73c5d42 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner/Info.plist +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner/Info.plist @@ -47,7 +47,5 @@ UIViewControllerBasedStatusBarAppearance - io.flutter.embedded_views_preview - diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner/main.m b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner/main.m index dff6597e4513..f97b9ef5c8a1 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner/main.m +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner/main.m @@ -1,3 +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 #import #import "AppDelegate.h" 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 new file mode 100644 index 000000000000..5249145f0c87 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/GoogleMapsTests.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 google_maps_flutter; +@import XCTest; + +@interface GoogleMapsTests : XCTestCase +@end + +@implementation GoogleMapsTests + +- (void)testPlugin { + FLTGoogleMapsPlugin* plugin = [[FLTGoogleMapsPlugin alloc] init]; + XCTAssertNotNil(plugin); +} + +@end diff --git a/packages/integration_test/example/ios/RunnerTests/Info.plist b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/Info.plist similarity index 100% rename from packages/integration_test/example/ios/RunnerTests/Info.plist rename to packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/Info.plist 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 new file mode 100644 index 000000000000..f56a2d17e3fe --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerUITests/GoogleMapsUITests.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 XCTest; +@import os.log; + +@interface GoogleMapsUITests : XCTestCase +@property(nonatomic, strong) XCUIApplication* app; +@end + +@implementation GoogleMapsUITests + +- (void)setUp { + self.continueAfterFailure = NO; + + self.app = [[XCUIApplication alloc] init]; + [self.app launch]; + + [self + addUIInterruptionMonitorWithDescription:@"Permission popups" + handler:^BOOL(XCUIElement* _Nonnull interruptingElement) { + if (@available(iOS 14, *)) { + XCUIElement* locationPermission = + interruptingElement.buttons[@"Allow While Using App"]; + if (![locationPermission + waitForExistenceWithTimeout:30.0]) { + XCTFail(@"Failed due to not able to find " + @"locationPermission button"); + } + [locationPermission tap]; + + } else { + XCUIElement* allow = + interruptingElement.buttons[@"Allow"]; + if (![allow waitForExistenceWithTimeout:30.0]) { + XCTFail(@"Failed due to not able to find Allow button"); + } + [allow tap]; + } + return YES; + }]; +} + +- (void)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]"]; + 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"]; + if (![compass waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find compass button"); + } + [compass tap]; +} + +@end diff --git a/packages/webview_flutter/example/ios/webview_flutter_exampleTests/Info.plist b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerUITests/Info.plist similarity index 100% rename from packages/webview_flutter/example/ios/webview_flutter_exampleTests/Info.plist rename to packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerUITests/Info.plist diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/animate_camera.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/animate_camera.dart index 37c79d302733..cc5fd257dfd3 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -26,7 +26,7 @@ class AnimateCamera extends StatefulWidget { } class AnimateCameraState extends State { - GoogleMapController mapController; + GoogleMapController? mapController; void _onMapCreated(GoogleMapController controller) { mapController = controller; @@ -54,9 +54,9 @@ class AnimateCameraState extends State { children: [ Column( children: [ - FlatButton( + TextButton( onPressed: () { - mapController.animateCamera( + mapController?.animateCamera( CameraUpdate.newCameraPosition( const CameraPosition( bearing: 270.0, @@ -69,9 +69,9 @@ class AnimateCameraState extends State { }, child: const Text('newCameraPosition'), ), - FlatButton( + TextButton( onPressed: () { - mapController.animateCamera( + mapController?.animateCamera( CameraUpdate.newLatLng( const LatLng(56.1725505, 10.1850512), ), @@ -79,9 +79,9 @@ class AnimateCameraState extends State { }, child: const Text('newLatLng'), ), - FlatButton( + TextButton( onPressed: () { - mapController.animateCamera( + mapController?.animateCamera( CameraUpdate.newLatLngBounds( LatLngBounds( southwest: const LatLng(-38.483935, 113.248673), @@ -93,9 +93,9 @@ class AnimateCameraState extends State { }, child: const Text('newLatLngBounds'), ), - FlatButton( + TextButton( onPressed: () { - mapController.animateCamera( + mapController?.animateCamera( CameraUpdate.newLatLngZoom( const LatLng(37.4231613, -122.087159), 11.0, @@ -104,9 +104,9 @@ class AnimateCameraState extends State { }, child: const Text('newLatLngZoom'), ), - FlatButton( + TextButton( onPressed: () { - mapController.animateCamera( + mapController?.animateCamera( CameraUpdate.scrollBy(150.0, -225.0), ); }, @@ -116,9 +116,9 @@ class AnimateCameraState extends State { ), Column( children: [ - FlatButton( + TextButton( onPressed: () { - mapController.animateCamera( + mapController?.animateCamera( CameraUpdate.zoomBy( -0.5, const Offset(30.0, 20.0), @@ -127,33 +127,33 @@ class AnimateCameraState extends State { }, child: const Text('zoomBy with focus'), ), - FlatButton( + TextButton( onPressed: () { - mapController.animateCamera( + mapController?.animateCamera( CameraUpdate.zoomBy(-0.5), ); }, child: const Text('zoomBy'), ), - FlatButton( + TextButton( onPressed: () { - mapController.animateCamera( + mapController?.animateCamera( CameraUpdate.zoomIn(), ); }, child: const Text('zoomIn'), ), - FlatButton( + TextButton( onPressed: () { - mapController.animateCamera( + mapController?.animateCamera( CameraUpdate.zoomOut(), ); }, child: const Text('zoomOut'), ), - FlatButton( + TextButton( onPressed: () { - mapController.animateCamera( + mapController?.animateCamera( CameraUpdate.zoomTo(16.0), ); }, diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/lite_mode.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/lite_mode.dart index 7c814f31a660..f6d6f54e135a 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_maps_flutter/google_maps_flutter/example/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/main.dart index 13763edb8216..15b14db0357a 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -20,6 +20,7 @@ import 'place_polygon.dart'; import 'place_polyline.dart'; import 'scrolling_map.dart'; import 'snapshot.dart'; +import 'tile_overlay.dart'; final List _allPages = [ MapUiPage(), @@ -36,6 +37,7 @@ final List _allPages = [ PaddingPage(), SnapshotPage(), LiteModePage(), + TileOverlayPage(), ]; class MapsDemo extends StatelessWidget { diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_click.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_click.dart index 029d3a1f187e..a46fc5fba420 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -31,9 +31,9 @@ class _MapClickBody extends StatefulWidget { class _MapClickBodyState extends State<_MapClickBody> { _MapClickBodyState(); - GoogleMapController mapController; - LatLng _lastTap; - LatLng _lastLongPress; + GoogleMapController? mapController; + LatLng? _lastTap; + LatLng? _lastLongPress; @override Widget build(BuildContext context) { diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_coordinates.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_coordinates.dart index efdbe016f7c4..99ab16802fea 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -31,7 +31,7 @@ class _MapCoordinatesBody extends StatefulWidget { class _MapCoordinatesBodyState extends State<_MapCoordinatesBody> { _MapCoordinatesBodyState(); - GoogleMapController mapController; + GoogleMapController? mapController; LatLngBounds _visibleRegion = LatLngBounds( southwest: const LatLng(0, 0), northeast: const LatLng(0, 0), @@ -83,11 +83,11 @@ class _MapCoordinatesBodyState extends State<_MapCoordinatesBody> { Widget _getVisibleRegionButton() { return Padding( padding: const EdgeInsets.all(8.0), - child: RaisedButton( + child: ElevatedButton( child: const Text('Get Visible Region Bounds'), onPressed: () async { final LatLngBounds visibleRegion = - await mapController.getVisibleRegion(); + await mapController!.getVisibleRegion(); setState(() { _visibleRegion = visibleRegion; }); diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/map_ui.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/map_ui.dart index f117c3a48b22..2e0d2d188a3f 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -56,7 +56,7 @@ class MapUiBodyState extends State { bool _myLocationEnabled = true; bool _myTrafficEnabled = false; bool _myLocationButtonEnabled = true; - GoogleMapController _controller; + late GoogleMapController _controller; bool _nightMode = false; @override @@ -70,7 +70,7 @@ class MapUiBodyState extends State { } Widget _compassToggler() { - return FlatButton( + return TextButton( child: Text('${_compassEnabled ? 'disable' : 'enable'} compass'), onPressed: () { setState(() { @@ -81,7 +81,7 @@ class MapUiBodyState extends State { } Widget _mapToolbarToggler() { - return FlatButton( + return TextButton( child: Text('${_mapToolbarEnabled ? 'disable' : 'enable'} map toolbar'), onPressed: () { setState(() { @@ -92,7 +92,7 @@ class MapUiBodyState extends State { } Widget _latLngBoundsToggler() { - return FlatButton( + return TextButton( child: Text( _cameraTargetBounds.bounds == null ? 'bound camera target' @@ -109,7 +109,7 @@ class MapUiBodyState extends State { } Widget _zoomBoundsToggler() { - return FlatButton( + return TextButton( child: Text(_minMaxZoomPreference.minZoom == null ? 'bound zoom' : 'release zoom'), @@ -126,7 +126,7 @@ class MapUiBodyState extends State { Widget _mapTypeCycler() { final MapType nextType = MapType.values[(_mapType.index + 1) % MapType.values.length]; - return FlatButton( + return TextButton( child: Text('change map type to $nextType'), onPressed: () { setState(() { @@ -137,7 +137,7 @@ class MapUiBodyState extends State { } Widget _rotateToggler() { - return FlatButton( + return TextButton( child: Text('${_rotateGesturesEnabled ? 'disable' : 'enable'} rotate'), onPressed: () { setState(() { @@ -148,7 +148,7 @@ class MapUiBodyState extends State { } Widget _scrollToggler() { - return FlatButton( + return TextButton( child: Text('${_scrollGesturesEnabled ? 'disable' : 'enable'} scroll'), onPressed: () { setState(() { @@ -159,7 +159,7 @@ class MapUiBodyState extends State { } Widget _tiltToggler() { - return FlatButton( + return TextButton( child: Text('${_tiltGesturesEnabled ? 'disable' : 'enable'} tilt'), onPressed: () { setState(() { @@ -170,7 +170,7 @@ class MapUiBodyState extends State { } Widget _zoomToggler() { - return FlatButton( + return TextButton( child: Text('${_zoomGesturesEnabled ? 'disable' : 'enable'} zoom'), onPressed: () { setState(() { @@ -181,7 +181,7 @@ class MapUiBodyState extends State { } Widget _zoomControlsToggler() { - return FlatButton( + return TextButton( child: Text('${_zoomControlsEnabled ? 'disable' : 'enable'} zoom controls'), onPressed: () { @@ -193,7 +193,7 @@ class MapUiBodyState extends State { } Widget _indoorViewToggler() { - return FlatButton( + return TextButton( child: Text('${_indoorViewEnabled ? 'disable' : 'enable'} indoor'), onPressed: () { setState(() { @@ -204,7 +204,7 @@ class MapUiBodyState extends State { } Widget _myLocationToggler() { - return FlatButton( + return TextButton( child: Text( '${_myLocationEnabled ? 'disable' : 'enable'} my location marker'), onPressed: () { @@ -216,7 +216,7 @@ class MapUiBodyState extends State { } Widget _myLocationButtonToggler() { - return FlatButton( + return TextButton( child: Text( '${_myLocationButtonEnabled ? 'disable' : 'enable'} my location button'), onPressed: () { @@ -228,7 +228,7 @@ class MapUiBodyState extends State { } Widget _myTrafficToggler() { - return FlatButton( + return TextButton( child: Text('${_myTrafficEnabled ? 'disable' : 'enable'} my traffic'), onPressed: () { setState(() { @@ -249,11 +249,10 @@ class MapUiBodyState extends State { }); } + // Should only be called if _isMapCreated is true. Widget _nightModeToggler() { - if (!_isMapCreated) { - return null; - } - return FlatButton( + assert(_isMapCreated); + return TextButton( child: Text('${_nightMode ? 'disable' : 'enable'} night mode'), onPressed: () { if (_nightMode) { diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/marker_icons.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/marker_icons.dart index b62d898c3a3b..da57b83a7e4f 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -29,8 +29,8 @@ class MarkerIconsBody extends StatefulWidget { const LatLng _kMapCenter = LatLng(52.4478, -3.5402); class MarkerIconsBodyState extends State { - GoogleMapController controller; - BitmapDescriptor _markerIcon; + GoogleMapController? controller; + BitmapDescriptor? _markerIcon; @override Widget build(BuildContext context) { @@ -48,7 +48,7 @@ class MarkerIconsBodyState extends State { target: _kMapCenter, zoom: 7.0, ), - markers: _createMarker(), + markers: {_createMarker()}, onMapCreated: _onMapCreated, ), ), @@ -57,17 +57,19 @@ class MarkerIconsBodyState extends State { ); } - Set _createMarker() { - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - return [ - Marker( + Marker _createMarker() { + if (_markerIcon != null) { + return Marker( markerId: MarkerId("marker_1"), position: _kMapCenter, - icon: _markerIcon, - ), - ].toSet(); + icon: _markerIcon!, + ); + } else { + return Marker( + markerId: MarkerId("marker_1"), + position: _kMapCenter, + ); + } } Future _createMarkerImageFromAsset(BuildContext context) async { diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/move_camera.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/move_camera.dart index 514a315e03db..f8274196770d 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -25,7 +25,7 @@ class MoveCamera extends StatefulWidget { } class MoveCameraState extends State { - GoogleMapController mapController; + GoogleMapController? mapController; void _onMapCreated(GoogleMapController controller) { mapController = controller; @@ -53,9 +53,9 @@ class MoveCameraState extends State { children: [ Column( children: [ - FlatButton( + TextButton( onPressed: () { - mapController.moveCamera( + mapController?.moveCamera( CameraUpdate.newCameraPosition( const CameraPosition( bearing: 270.0, @@ -68,9 +68,9 @@ class MoveCameraState extends State { }, child: const Text('newCameraPosition'), ), - FlatButton( + TextButton( onPressed: () { - mapController.moveCamera( + mapController?.moveCamera( CameraUpdate.newLatLng( const LatLng(56.1725505, 10.1850512), ), @@ -78,9 +78,9 @@ class MoveCameraState extends State { }, child: const Text('newLatLng'), ), - FlatButton( + TextButton( onPressed: () { - mapController.moveCamera( + mapController?.moveCamera( CameraUpdate.newLatLngBounds( LatLngBounds( southwest: const LatLng(-38.483935, 113.248673), @@ -92,9 +92,9 @@ class MoveCameraState extends State { }, child: const Text('newLatLngBounds'), ), - FlatButton( + TextButton( onPressed: () { - mapController.moveCamera( + mapController?.moveCamera( CameraUpdate.newLatLngZoom( const LatLng(37.4231613, -122.087159), 11.0, @@ -103,9 +103,9 @@ class MoveCameraState extends State { }, child: const Text('newLatLngZoom'), ), - FlatButton( + TextButton( onPressed: () { - mapController.moveCamera( + mapController?.moveCamera( CameraUpdate.scrollBy(150.0, -225.0), ); }, @@ -115,9 +115,9 @@ class MoveCameraState extends State { ), Column( children: [ - FlatButton( + TextButton( onPressed: () { - mapController.moveCamera( + mapController?.moveCamera( CameraUpdate.zoomBy( -0.5, const Offset(30.0, 20.0), @@ -126,33 +126,33 @@ class MoveCameraState extends State { }, child: const Text('zoomBy with focus'), ), - FlatButton( + TextButton( onPressed: () { - mapController.moveCamera( + mapController?.moveCamera( CameraUpdate.zoomBy(-0.5), ); }, child: const Text('zoomBy'), ), - FlatButton( + TextButton( onPressed: () { - mapController.moveCamera( + mapController?.moveCamera( CameraUpdate.zoomIn(), ); }, child: const Text('zoomIn'), ), - FlatButton( + TextButton( onPressed: () { - mapController.moveCamera( + mapController?.moveCamera( CameraUpdate.zoomOut(), ); }, child: const Text('zoomOut'), ), - FlatButton( + TextButton( onPressed: () { - mapController.moveCamera( + mapController?.moveCamera( CameraUpdate.zoomTo(16.0), ); }, diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/padding.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/padding.dart index 94b60b7758f9..d90005fa6998 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -27,7 +27,7 @@ class MarkerIconsBody extends StatefulWidget { const LatLng _kMapCenter = LatLng(52.4478, -3.5402); class MarkerIconsBodyState extends State { - GoogleMapController controller; + GoogleMapController? controller; EdgeInsets _padding = const EdgeInsets.all(0); @@ -147,19 +147,19 @@ class MarkerIconsBodyState extends State { child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - FlatButton( + TextButton( child: const Text("Set Padding"), onPressed: () { setState(() { _padding = EdgeInsets.fromLTRB( - double.tryParse(_leftController.value?.text) ?? 0, - double.tryParse(_topController.value?.text) ?? 0, - double.tryParse(_rightController.value?.text) ?? 0, - double.tryParse(_bottomController.value?.text) ?? 0); + double.tryParse(_leftController.value.text) ?? 0, + double.tryParse(_topController.value.text) ?? 0, + double.tryParse(_rightController.value.text) ?? 0, + double.tryParse(_bottomController.value.text) ?? 0); }); }, ), - FlatButton( + TextButton( child: const Text("Reset Padding"), onPressed: () { setState(() { diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/page.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/page.dart index eaa43fc9f26d..fb6eb3260f6d 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_maps_flutter/google_maps_flutter/example/lib/place_circle.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_circle.dart index 954d8876d1d5..a4953428f088 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -28,10 +28,10 @@ class PlaceCircleBody extends StatefulWidget { class PlaceCircleBodyState extends State { PlaceCircleBodyState(); - GoogleMapController controller; + GoogleMapController? controller; Map circles = {}; int _circleIdCounter = 1; - CircleId selectedCircle; + CircleId? selectedCircle; // Values when toggling circle color int fillColorsIndex = 0; @@ -62,12 +62,14 @@ class PlaceCircleBodyState extends State { }); } - void _remove() { + void _remove(CircleId circleId) { setState(() { - if (circles.containsKey(selectedCircle)) { - circles.remove(selectedCircle); + if (circles.containsKey(circleId)) { + circles.remove(circleId); + } + if (circleId == selectedCircle) { + selectedCircle = null; } - selectedCircle = null; }); } @@ -100,37 +102,37 @@ class PlaceCircleBodyState extends State { }); } - void _toggleVisible() { - final Circle circle = circles[selectedCircle]; + void _toggleVisible(CircleId circleId) { + final Circle circle = circles[circleId]!; setState(() { - circles[selectedCircle] = circle.copyWith( + circles[circleId] = circle.copyWith( visibleParam: !circle.visible, ); }); } - void _changeFillColor() { - final Circle circle = circles[selectedCircle]; + void _changeFillColor(CircleId circleId) { + final Circle circle = circles[circleId]!; setState(() { - circles[selectedCircle] = circle.copyWith( + circles[circleId] = circle.copyWith( fillColorParam: colors[++fillColorsIndex % colors.length], ); }); } - void _changeStrokeColor() { - final Circle circle = circles[selectedCircle]; + void _changeStrokeColor(CircleId circleId) { + final Circle circle = circles[circleId]!; setState(() { - circles[selectedCircle] = circle.copyWith( + circles[circleId] = circle.copyWith( strokeColorParam: colors[++strokeColorsIndex % colors.length], ); }); } - void _changeStrokeWidth() { - final Circle circle = circles[selectedCircle]; + void _changeStrokeWidth(CircleId circleId) { + final Circle circle = circles[circleId]!; setState(() { - circles[selectedCircle] = circle.copyWith( + circles[circleId] = circle.copyWith( strokeWidthParam: widths[++widthsIndex % widths.length], ); }); @@ -138,6 +140,7 @@ class PlaceCircleBodyState extends State { @override Widget build(BuildContext context) { + final CircleId? selectedId = selectedCircle; return Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -165,40 +168,43 @@ class PlaceCircleBodyState extends State { children: [ Column( children: [ - FlatButton( + TextButton( child: const Text('add'), onPressed: _add, ), - FlatButton( + TextButton( child: const Text('remove'), - onPressed: (selectedCircle == null) ? null : _remove, + onPressed: (selectedId == null) + ? null + : () => _remove(selectedId), ), - FlatButton( + TextButton( child: const Text('toggle visible'), - onPressed: - (selectedCircle == null) ? null : _toggleVisible, + onPressed: (selectedId == null) + ? null + : () => _toggleVisible(selectedId), ), ], ), Column( children: [ - FlatButton( + TextButton( child: const Text('change stroke width'), - onPressed: (selectedCircle == null) + onPressed: (selectedId == null) ? null - : _changeStrokeWidth, + : () => _changeStrokeWidth(selectedId), ), - FlatButton( + TextButton( child: const Text('change stroke color'), - onPressed: (selectedCircle == null) + onPressed: (selectedId == null) ? null - : _changeStrokeColor, + : () => _changeStrokeColor(selectedId), ), - FlatButton( + TextButton( child: const Text('change fill color'), - onPressed: (selectedCircle == null) + onPressed: (selectedId == null) ? null - : _changeFillColor, + : () => _changeFillColor(selectedId), ), ], ) diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_marker.dart index 6808e58c199e..4e9f4c14ebd0 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -35,9 +35,9 @@ class PlaceMarkerBodyState extends State { PlaceMarkerBodyState(); static final LatLng center = const LatLng(-33.86711, 151.1947171); - GoogleMapController controller; + GoogleMapController? controller; Map markers = {}; - MarkerId selectedMarker; + MarkerId? selectedMarker; int _markerIdCounter = 1; void _onMapCreated(GoogleMapController controller) { @@ -50,13 +50,14 @@ class PlaceMarkerBodyState extends State { } void _onMarkerTapped(MarkerId markerId) { - final Marker tappedMarker = markers[markerId]; + final Marker? tappedMarker = markers[markerId]; if (tappedMarker != null) { setState(() { - if (markers.containsKey(selectedMarker)) { - final Marker resetOld = markers[selectedMarker] + final MarkerId? previousMarkerId = selectedMarker; + if (previousMarkerId != null && markers.containsKey(previousMarkerId)) { + final Marker resetOld = markers[previousMarkerId]! .copyWith(iconParam: BitmapDescriptor.defaultMarker); - markers[selectedMarker] = resetOld; + markers[previousMarkerId] = resetOld; } selectedMarker = markerId; final Marker newMarker = tappedMarker.copyWith( @@ -70,14 +71,14 @@ class PlaceMarkerBodyState extends State { } void _onMarkerDragEnd(MarkerId markerId, LatLng newPosition) async { - final Marker tappedMarker = markers[markerId]; + final Marker? tappedMarker = markers[markerId]; if (tappedMarker != null) { await showDialog( context: context, builder: (BuildContext context) { return AlertDialog( actions: [ - FlatButton( + TextButton( child: const Text('OK'), onPressed: () => Navigator.of(context).pop(), ) @@ -126,23 +127,23 @@ class PlaceMarkerBodyState extends State { }); } - void _remove() { + void _remove(MarkerId markerId) { setState(() { - if (markers.containsKey(selectedMarker)) { - markers.remove(selectedMarker); + if (markers.containsKey(markerId)) { + markers.remove(markerId); } }); } - void _changePosition() { - final Marker marker = markers[selectedMarker]; + void _changePosition(MarkerId markerId) { + final Marker marker = markers[markerId]!; final LatLng current = marker.position; final Offset offset = Offset( center.latitude - current.latitude, center.longitude - current.longitude, ); setState(() { - markers[selectedMarker] = marker.copyWith( + markers[markerId] = marker.copyWith( positionParam: LatLng( center.latitude + offset.dy, center.longitude + offset.dx, @@ -151,23 +152,23 @@ class PlaceMarkerBodyState extends State { }); } - void _changeAnchor() { - final Marker marker = markers[selectedMarker]; + void _changeAnchor(MarkerId markerId) { + final Marker marker = markers[markerId]!; final Offset currentAnchor = marker.anchor; final Offset newAnchor = Offset(1.0 - currentAnchor.dy, currentAnchor.dx); setState(() { - markers[selectedMarker] = marker.copyWith( + markers[markerId] = marker.copyWith( anchorParam: newAnchor, ); }); } - Future _changeInfoAnchor() async { - final Marker marker = markers[selectedMarker]; + Future _changeInfoAnchor(MarkerId markerId) async { + final Marker marker = markers[markerId]!; final Offset currentAnchor = marker.infoWindow.anchor; final Offset newAnchor = Offset(1.0 - currentAnchor.dy, currentAnchor.dx); setState(() { - markers[selectedMarker] = marker.copyWith( + markers[markerId] = marker.copyWith( infoWindowParam: marker.infoWindow.copyWith( anchorParam: newAnchor, ), @@ -175,29 +176,29 @@ class PlaceMarkerBodyState extends State { }); } - Future _toggleDraggable() async { - final Marker marker = markers[selectedMarker]; + Future _toggleDraggable(MarkerId markerId) async { + final Marker marker = markers[markerId]!; setState(() { - markers[selectedMarker] = marker.copyWith( + markers[markerId] = marker.copyWith( draggableParam: !marker.draggable, ); }); } - Future _toggleFlat() async { - final Marker marker = markers[selectedMarker]; + Future _toggleFlat(MarkerId markerId) async { + final Marker marker = markers[markerId]!; setState(() { - markers[selectedMarker] = marker.copyWith( + markers[markerId] = marker.copyWith( flatParam: !marker.flat, ); }); } - Future _changeInfo() async { - final Marker marker = markers[selectedMarker]; - final String newSnippet = marker.infoWindow.snippet + '*'; + Future _changeInfo(MarkerId markerId) async { + final Marker marker = markers[markerId]!; + final String newSnippet = marker.infoWindow.snippet! + '*'; setState(() { - markers[selectedMarker] = marker.copyWith( + markers[markerId] = marker.copyWith( infoWindowParam: marker.infoWindow.copyWith( snippetParam: newSnippet, ), @@ -205,40 +206,40 @@ class PlaceMarkerBodyState extends State { }); } - Future _changeAlpha() async { - final Marker marker = markers[selectedMarker]; + Future _changeAlpha(MarkerId markerId) async { + final Marker marker = markers[markerId]!; final double current = marker.alpha; setState(() { - markers[selectedMarker] = marker.copyWith( + markers[markerId] = marker.copyWith( alphaParam: current < 0.1 ? 1.0 : current * 0.75, ); }); } - Future _changeRotation() async { - final Marker marker = markers[selectedMarker]; + Future _changeRotation(MarkerId markerId) async { + final Marker marker = markers[markerId]!; final double current = marker.rotation; setState(() { - markers[selectedMarker] = marker.copyWith( + markers[markerId] = marker.copyWith( rotationParam: current == 330.0 ? 0.0 : current + 30.0, ); }); } - Future _toggleVisible() async { - final Marker marker = markers[selectedMarker]; + Future _toggleVisible(MarkerId markerId) async { + final Marker marker = markers[markerId]!; setState(() { - markers[selectedMarker] = marker.copyWith( + markers[markerId] = marker.copyWith( visibleParam: !marker.visible, ); }); } - Future _changeZIndex() async { - final Marker marker = markers[selectedMarker]; + Future _changeZIndex(MarkerId markerId) async { + final Marker marker = markers[markerId]!; final double current = marker.zIndex; setState(() { - markers[selectedMarker] = marker.copyWith( + markers[markerId] = marker.copyWith( zIndexParam: current == 12.0 ? 0.0 : current + 1.0, ); }); @@ -283,6 +284,7 @@ class PlaceMarkerBodyState extends State { @override Widget build(BuildContext context) { + final MarkerId? selectedId = selectedMarker; return Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -297,9 +299,6 @@ class PlaceMarkerBodyState extends State { target: LatLng(-33.852, 151.211), zoom: 11.0, ), - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals markers: Set.of(markers.values), ), ), @@ -313,57 +312,79 @@ class PlaceMarkerBodyState extends State { children: [ Column( children: [ - FlatButton( + TextButton( child: const Text('add'), onPressed: _add, ), - FlatButton( + TextButton( child: const Text('remove'), - onPressed: _remove, + onPressed: selectedId == null + ? null + : () => _remove(selectedId), ), - FlatButton( + TextButton( child: const Text('change info'), - onPressed: _changeInfo, + onPressed: selectedId == null + ? null + : () => _changeInfo(selectedId), ), - FlatButton( + TextButton( child: const Text('change info anchor'), - onPressed: _changeInfoAnchor, + onPressed: selectedId == null + ? null + : () => _changeInfoAnchor(selectedId), ), ], ), Column( children: [ - FlatButton( + TextButton( child: const Text('change alpha'), - onPressed: _changeAlpha, + onPressed: selectedId == null + ? null + : () => _changeAlpha(selectedId), ), - FlatButton( + TextButton( child: const Text('change anchor'), - onPressed: _changeAnchor, + onPressed: selectedId == null + ? null + : () => _changeAnchor(selectedId), ), - FlatButton( + TextButton( child: const Text('toggle draggable'), - onPressed: _toggleDraggable, + onPressed: selectedId == null + ? null + : () => _toggleDraggable(selectedId), ), - FlatButton( + TextButton( child: const Text('toggle flat'), - onPressed: _toggleFlat, + onPressed: selectedId == null + ? null + : () => _toggleFlat(selectedId), ), - FlatButton( + TextButton( child: const Text('change position'), - onPressed: _changePosition, + onPressed: selectedId == null + ? null + : () => _changePosition(selectedId), ), - FlatButton( + TextButton( child: const Text('change rotation'), - onPressed: _changeRotation, + onPressed: selectedId == null + ? null + : () => _changeRotation(selectedId), ), - FlatButton( + TextButton( child: const Text('toggle visible'), - onPressed: _toggleVisible, + onPressed: selectedId == null + ? null + : () => _toggleVisible(selectedId), ), - FlatButton( + TextButton( child: const Text('change zIndex'), - onPressed: _changeZIndex, + onPressed: selectedId == null + ? null + : () => _changeZIndex(selectedId), ), // A breaking change to the ImageStreamListener API affects this sample. // I've updates the sample to use the new API, but as we cannot use the new @@ -371,7 +392,7 @@ class PlaceMarkerBodyState extends State { // TODO(amirh): uncomment this one the ImageStream API change makes it to stable. // https://github.com/flutter/flutter/issues/33438 // - // FlatButton( + // TextButton( // child: const Text('set marker icon'), // onPressed: () { // _getAssetIcon(context).then( diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_polygon.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_polygon.dart index 5713f9a099e6..476084defa75 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -28,10 +28,11 @@ class PlacePolygonBody extends StatefulWidget { class PlacePolygonBodyState extends State { PlacePolygonBodyState(); - GoogleMapController controller; + GoogleMapController? controller; Map polygons = {}; - int _polygonIdCounter = 1; - PolygonId selectedPolygon; + Map polygonOffsets = {}; + int _polygonIdCounter = 0; + PolygonId? selectedPolygon; // Values when toggling polygon color int strokeColorsIndex = 0; @@ -62,10 +63,10 @@ class PlacePolygonBodyState extends State { }); } - void _remove() { + void _remove(PolygonId polygonId) { setState(() { - if (polygons.containsKey(selectedPolygon)) { - polygons.remove(selectedPolygon); + if (polygons.containsKey(polygonId)) { + polygons.remove(polygonId); } selectedPolygon = null; }); @@ -79,7 +80,6 @@ class PlacePolygonBodyState extends State { } final String polygonIdVal = 'polygon_id_$_polygonIdCounter'; - _polygonIdCounter++; final PolygonId polygonId = PolygonId(polygonIdVal); final Polygon polygon = Polygon( @@ -96,56 +96,77 @@ class PlacePolygonBodyState extends State { setState(() { polygons[polygonId] = polygon; + polygonOffsets[polygonId] = _polygonIdCounter.ceilToDouble(); + // increment _polygonIdCounter to have unique polygon id each time + _polygonIdCounter++; }); } - void _toggleGeodesic() { - final Polygon polygon = polygons[selectedPolygon]; + void _toggleGeodesic(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; setState(() { - polygons[selectedPolygon] = polygon.copyWith( + polygons[polygonId] = polygon.copyWith( geodesicParam: !polygon.geodesic, ); }); } - void _toggleVisible() { - final Polygon polygon = polygons[selectedPolygon]; + void _toggleVisible(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; setState(() { - polygons[selectedPolygon] = polygon.copyWith( + polygons[polygonId] = polygon.copyWith( visibleParam: !polygon.visible, ); }); } - void _changeStrokeColor() { - final Polygon polygon = polygons[selectedPolygon]; + void _changeStrokeColor(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; setState(() { - polygons[selectedPolygon] = polygon.copyWith( + polygons[polygonId] = polygon.copyWith( strokeColorParam: colors[++strokeColorsIndex % colors.length], ); }); } - void _changeFillColor() { - final Polygon polygon = polygons[selectedPolygon]; + void _changeFillColor(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; setState(() { - polygons[selectedPolygon] = polygon.copyWith( + polygons[polygonId] = polygon.copyWith( fillColorParam: colors[++fillColorsIndex % colors.length], ); }); } - void _changeWidth() { - final Polygon polygon = polygons[selectedPolygon]; + void _changeWidth(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; setState(() { - polygons[selectedPolygon] = polygon.copyWith( + polygons[polygonId] = polygon.copyWith( strokeWidthParam: widths[++widthsIndex % widths.length], ); }); } + void _addHoles(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; + setState(() { + polygons[polygonId] = + polygon.copyWith(holesParam: _createHoles(polygonId)); + }); + } + + void _removeHoles(PolygonId polygonId) { + final Polygon polygon = polygons[polygonId]!; + setState(() { + polygons[polygonId] = polygon.copyWith( + holesParam: >[], + ); + }); + } + @override Widget build(BuildContext context) { + final PolygonId? selectedId = selectedPolygon; return Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -173,45 +194,65 @@ class PlacePolygonBodyState extends State { children: [ Column( children: [ - FlatButton( + TextButton( child: const Text('add'), onPressed: _add, ), - FlatButton( + TextButton( child: const Text('remove'), - onPressed: (selectedPolygon == null) ? null : _remove, + onPressed: (selectedId == null) + ? null + : () => _remove(selectedId), ), - FlatButton( + TextButton( child: const Text('toggle visible'), - onPressed: - (selectedPolygon == null) ? null : _toggleVisible, + onPressed: (selectedId == null) + ? null + : () => _toggleVisible(selectedId), ), - FlatButton( + TextButton( child: const Text('toggle geodesic'), - onPressed: (selectedPolygon == null) + onPressed: (selectedId == null) ? null - : _toggleGeodesic, + : () => _toggleGeodesic(selectedId), ), ], ), Column( children: [ - FlatButton( + TextButton( + child: const Text('add holes'), + onPressed: (selectedId == null) + ? null + : ((polygons[selectedId]!.holes.isNotEmpty) + ? null + : () => _addHoles(selectedId)), + ), + TextButton( + child: const Text('remove holes'), + onPressed: (selectedId == null) + ? null + : ((polygons[selectedId]!.holes.isEmpty) + ? null + : () => _removeHoles(selectedId)), + ), + TextButton( child: const Text('change stroke width'), - onPressed: - (selectedPolygon == null) ? null : _changeWidth, + onPressed: (selectedId == null) + ? null + : () => _changeWidth(selectedId), ), - FlatButton( + TextButton( child: const Text('change stroke color'), - onPressed: (selectedPolygon == null) + onPressed: (selectedId == null) ? null - : _changeStrokeColor, + : () => _changeStrokeColor(selectedId), ), - FlatButton( + TextButton( child: const Text('change fill color'), - onPressed: (selectedPolygon == null) + onPressed: (selectedId == null) ? null - : _changeFillColor, + : () => _changeFillColor(selectedId), ), ], ) @@ -235,6 +276,27 @@ class PlacePolygonBodyState extends State { return points; } + List> _createHoles(PolygonId polygonId) { + final List> holes = >[]; + final double offset = polygonOffsets[polygonId]!; + + final List hole1 = []; + hole1.add(_createLatLng(51.8395 + offset, -3.8814)); + hole1.add(_createLatLng(52.0234 + offset, -3.9914)); + hole1.add(_createLatLng(52.1351 + offset, -4.4435)); + hole1.add(_createLatLng(52.0231 + offset, -4.5829)); + holes.add(hole1); + + final List hole2 = []; + hole2.add(_createLatLng(52.2395 + offset, -3.6814)); + hole2.add(_createLatLng(52.4234 + offset, -3.7914)); + hole2.add(_createLatLng(52.5351 + offset, -4.2435)); + hole2.add(_createLatLng(52.4231 + offset, -4.3829)); + holes.add(hole2); + + return holes; + } + LatLng _createLatLng(double lat, double lng) { return LatLng(lat, lng); } diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/place_polyline.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/place_polyline.dart index 35ffd33a53c2..aeb9bf1b11eb 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -29,10 +29,10 @@ class PlacePolylineBody extends StatefulWidget { class PlacePolylineBodyState extends State { PlacePolylineBodyState(); - GoogleMapController controller; + GoogleMapController? controller; Map polylines = {}; - int _polylineIdCounter = 1; - PolylineId selectedPolyline; + int _polylineIdCounter = 0; + PolylineId? selectedPolyline; // Values when toggling polyline color int colorsIndex = 0; @@ -91,10 +91,10 @@ class PlacePolylineBodyState extends State { }); } - void _remove() { + void _remove(PolylineId polylineId) { setState(() { - if (polylines.containsKey(selectedPolyline)) { - polylines.remove(selectedPolyline); + if (polylines.containsKey(polylineId)) { + polylines.remove(polylineId); } selectedPolyline = null; }); @@ -127,73 +127,73 @@ class PlacePolylineBodyState extends State { }); } - void _toggleGeodesic() { - final Polyline polyline = polylines[selectedPolyline]; + void _toggleGeodesic(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; setState(() { - polylines[selectedPolyline] = polyline.copyWith( + polylines[polylineId] = polyline.copyWith( geodesicParam: !polyline.geodesic, ); }); } - void _toggleVisible() { - final Polyline polyline = polylines[selectedPolyline]; + void _toggleVisible(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; setState(() { - polylines[selectedPolyline] = polyline.copyWith( + polylines[polylineId] = polyline.copyWith( visibleParam: !polyline.visible, ); }); } - void _changeColor() { - final Polyline polyline = polylines[selectedPolyline]; + void _changeColor(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; setState(() { - polylines[selectedPolyline] = polyline.copyWith( + polylines[polylineId] = polyline.copyWith( colorParam: colors[++colorsIndex % colors.length], ); }); } - void _changeWidth() { - final Polyline polyline = polylines[selectedPolyline]; + void _changeWidth(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; setState(() { - polylines[selectedPolyline] = polyline.copyWith( + polylines[polylineId] = polyline.copyWith( widthParam: widths[++widthsIndex % widths.length], ); }); } - void _changeJointType() { - final Polyline polyline = polylines[selectedPolyline]; + void _changeJointType(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; setState(() { - polylines[selectedPolyline] = polyline.copyWith( + polylines[polylineId] = polyline.copyWith( jointTypeParam: jointTypes[++jointTypesIndex % jointTypes.length], ); }); } - void _changeEndCap() { - final Polyline polyline = polylines[selectedPolyline]; + void _changeEndCap(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; setState(() { - polylines[selectedPolyline] = polyline.copyWith( + polylines[polylineId] = polyline.copyWith( endCapParam: endCaps[++endCapsIndex % endCaps.length], ); }); } - void _changeStartCap() { - final Polyline polyline = polylines[selectedPolyline]; + void _changeStartCap(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; setState(() { - polylines[selectedPolyline] = polyline.copyWith( + polylines[polylineId] = polyline.copyWith( startCapParam: startCaps[++startCapsIndex % startCaps.length], ); }); } - void _changePattern() { - final Polyline polyline = polylines[selectedPolyline]; + void _changePattern(PolylineId polylineId) { + final Polyline polyline = polylines[polylineId]!; setState(() { - polylines[selectedPolyline] = polyline.copyWith( + polylines[polylineId] = polyline.copyWith( patternsParam: patterns[++patternsIndex % patterns.length], ); }); @@ -201,9 +201,9 @@ class PlacePolylineBodyState extends State { @override Widget build(BuildContext context) { - final bool iOSorNotSelected = - (!kIsWeb && defaultTargetPlatform == TargetPlatform.iOS) || - (selectedPolyline == null); + final bool isIOS = !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS; + + final PolylineId? selectedId = selectedPolyline; return Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, @@ -215,7 +215,7 @@ class PlacePolylineBodyState extends State { height: 300.0, child: GoogleMap( initialCameraPosition: const CameraPosition( - target: LatLng(52.4478, -3.5402), + target: LatLng(53.1721, -3.5402), zoom: 7.0, ), polylines: Set.of(polylines.values), @@ -232,56 +232,67 @@ class PlacePolylineBodyState extends State { children: [ Column( children: [ - FlatButton( + TextButton( child: const Text('add'), onPressed: _add, ), - FlatButton( + TextButton( child: const Text('remove'), - onPressed: - (selectedPolyline == null) ? null : _remove, + onPressed: (selectedId == null) + ? null + : () => _remove(selectedId), ), - FlatButton( + TextButton( child: const Text('toggle visible'), - onPressed: (selectedPolyline == null) + onPressed: (selectedId == null) ? null - : _toggleVisible, + : () => _toggleVisible(selectedId), ), - FlatButton( + TextButton( child: const Text('toggle geodesic'), - onPressed: (selectedPolyline == null) + onPressed: (selectedId == null) ? null - : _toggleGeodesic, + : () => _toggleGeodesic(selectedId), ), ], ), Column( children: [ - FlatButton( + TextButton( child: const Text('change width'), - onPressed: - (selectedPolyline == null) ? null : _changeWidth, + onPressed: (selectedId == null) + ? null + : () => _changeWidth(selectedId), ), - FlatButton( + TextButton( child: const Text('change color'), - onPressed: - (selectedPolyline == null) ? null : _changeColor, + onPressed: (selectedId == null) + ? null + : () => _changeColor(selectedId), ), - FlatButton( + TextButton( child: const Text('change start cap [Android only]'), - onPressed: iOSorNotSelected ? null : _changeStartCap, + onPressed: isIOS || (selectedId == null) + ? null + : () => _changeStartCap(selectedId), ), - FlatButton( + TextButton( child: const Text('change end cap [Android only]'), - onPressed: iOSorNotSelected ? null : _changeEndCap, + onPressed: isIOS || (selectedId == null) + ? null + : () => _changeEndCap(selectedId), ), - FlatButton( + TextButton( child: const Text('change joint type [Android only]'), - onPressed: iOSorNotSelected ? null : _changeJointType, + onPressed: isIOS || (selectedId == null) + ? null + : () => _changeJointType(selectedId), ), - FlatButton( + TextButton( child: const Text('change pattern [Android only]'), - onPressed: iOSorNotSelected ? null : _changePattern, + onPressed: isIOS || (selectedId == null) + ? null + : () => _changePattern(selectedId), ), ], ) diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/scrolling_map.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/scrolling_map.dart index 2aa1243fd27c..9611d36bc8e8 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -48,15 +48,12 @@ class ScrollingMapBody extends StatelessWidget { target: center, zoom: 11.0, ), - gestureRecognizers: - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - >[ + gestureRecognizers: // + >{ Factory( () => EagerGestureRecognizer(), ), - ].toSet(), + }, ), ), ), @@ -84,34 +81,25 @@ class ScrollingMapBody extends StatelessWidget { target: center, zoom: 11.0, ), - markers: - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - Set.of( - [ - Marker( - markerId: MarkerId("test_marker_id"), - position: LatLng( - center.latitude, - center.longitude, - ), - infoWindow: const InfoWindow( - title: 'An interesting location', - snippet: '*', - ), - ) - ], - ), - gestureRecognizers: - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - >[ + markers: { + Marker( + markerId: MarkerId("test_marker_id"), + position: LatLng( + center.latitude, + center.longitude, + ), + infoWindow: const InfoWindow( + title: 'An interesting location', + snippet: '*', + ), + ), + }, + gestureRecognizers: < + Factory>{ Factory( () => ScaleGestureRecognizer(), ), - ].toSet(), + }, ), ), ), diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/snapshot.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/snapshot.dart index 872060d86039..c85048f5b5aa 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 @@ -1,4 +1,4 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -30,8 +30,8 @@ class _SnapshotBody extends StatefulWidget { } class _SnapshotBodyState extends State<_SnapshotBody> { - GoogleMapController _mapController; - Uint8List _imageBytes; + GoogleMapController? _mapController; + Uint8List? _imageBytes; @override Widget build(BuildContext context) { @@ -47,7 +47,7 @@ class _SnapshotBodyState extends State<_SnapshotBody> { initialCameraPosition: _kInitialPosition, ), ), - FlatButton( + TextButton( child: Text('Take a snapshot'), onPressed: () async { final imageBytes = await _mapController?.takeSnapshot(); @@ -59,7 +59,7 @@ class _SnapshotBodyState extends State<_SnapshotBody> { Container( decoration: BoxDecoration(color: Colors.blueGrey[50]), height: 180, - child: _imageBytes != null ? Image.memory(_imageBytes) : null, + child: _imageBytes != null ? Image.memory(_imageBytes!) : null, ), ], ), diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/tile_overlay.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/tile_overlay.dart new file mode 100644 index 000000000000..1d6dd69c186b --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/tile_overlay.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. + +// ignore_for_file: public_member_api_docs + +import 'dart:typed_data'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +import 'page.dart'; + +class TileOverlayPage extends GoogleMapExampleAppPage { + TileOverlayPage() : super(const Icon(Icons.map), 'Tile overlay'); + + @override + Widget build(BuildContext context) { + return const TileOverlayBody(); + } +} + +class TileOverlayBody extends StatefulWidget { + const TileOverlayBody(); + + @override + State createState() => TileOverlayBodyState(); +} + +class TileOverlayBodyState extends State { + TileOverlayBodyState(); + + GoogleMapController? controller; + TileOverlay? _tileOverlay; + + void _onMapCreated(GoogleMapController controller) { + this.controller = controller; + } + + @override + void dispose() { + super.dispose(); + } + + void _removeTileOverlay() { + setState(() { + _tileOverlay = null; + }); + } + + void _addTileOverlay() { + final TileOverlay tileOverlay = TileOverlay( + tileOverlayId: TileOverlayId('tile_overlay_1'), + tileProvider: _DebugTileProvider(), + ); + setState(() { + _tileOverlay = tileOverlay; + }); + } + + void _clearTileCache() { + if (_tileOverlay != null && controller != null) { + controller!.clearTileCache(_tileOverlay!.tileOverlayId); + } + } + + @override + Widget build(BuildContext context) { + Set overlays = { + if (_tileOverlay != null) _tileOverlay!, + }; + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Center( + child: SizedBox( + width: 350.0, + height: 300.0, + child: GoogleMap( + initialCameraPosition: const CameraPosition( + target: LatLng(59.935460, 30.325177), + zoom: 7.0, + ), + tileOverlays: overlays, + onMapCreated: _onMapCreated, + ), + ), + ), + TextButton( + child: const Text('Add tile overlay'), + onPressed: _addTileOverlay, + ), + TextButton( + child: const Text('Remove tile overlay'), + onPressed: _removeTileOverlay, + ), + TextButton( + child: const Text('Clear tile cache'), + onPressed: _clearTileCache, + ), + ], + ); + } +} + +class _DebugTileProvider implements TileProvider { + _DebugTileProvider() { + boxPaint.isAntiAlias = true; + boxPaint.color = Colors.blue; + boxPaint.strokeWidth = 2.0; + boxPaint.style = PaintingStyle.stroke; + } + + static const int width = 100; + static const int height = 100; + static final Paint boxPaint = Paint(); + static final TextStyle textStyle = TextStyle( + color: Colors.red, + fontSize: 20, + ); + + @override + Future getTile(int x, int y, int? zoom) async { + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final Canvas canvas = Canvas(recorder); + final TextSpan textSpan = TextSpan( + text: '$x,$y', + style: textStyle, + ); + final TextPainter textPainter = TextPainter( + text: textSpan, + textDirection: TextDirection.ltr, + ); + textPainter.layout( + minWidth: 0.0, + maxWidth: width.toDouble(), + ); + final Offset offset = const Offset(0, 0); + textPainter.paint(canvas, offset); + canvas.drawRect( + Rect.fromLTRB(0, 0, width.toDouble(), width.toDouble()), boxPaint); + final ui.Picture picture = recorder.endRecording(); + final Uint8List byteData = await picture + .toImage(width, height) + .then((ui.Image image) => + image.toByteData(format: ui.ImageByteFormat.png)) + .then((ByteData? byteData) => byteData!.buffer.asUint8List()); + return Tile(width, height, byteData); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml index 7bfc7a6feb9c..74135b31e8d7 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml @@ -1,63 +1,33 @@ name: google_maps_flutter_example description: Demonstrates how to use the google_maps_flutter plugin. +publish_to: none + +environment: + sdk: '>=2.12.0 <3.0.0' + flutter: ">=1.22.0" dependencies: flutter: sdk: flutter - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^0.1.0 google_maps_flutter: + # When depending on this package from a real application you should use: + # google_maps_flutter: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ - flutter_plugin_android_lifecycle: ^1.0.0 + flutter_plugin_android_lifecycle: ^2.0.1 dev_dependencies: flutter_driver: sdk: flutter - test: ^1.6.0 integration_test: - path: ../../../integration_test - pedantic: ^1.8.0 - -# For information on the generic Dart part of this file, see the -# following page: https://www.dartlang.org/tools/pub/pubspec + sdk: flutter + pedantic: ^1.10.0 -# The following section is specific to Flutter. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: assets: - assets/ - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.io/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.io/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.io/custom-fonts/#from-packages diff --git a/packages/google_maps_flutter/google_maps_flutter/example/test_driver/integration_test.dart b/packages/google_maps_flutter/google_maps_flutter/example/test_driver/integration_test.dart index 7a2c21338786..4f10f2a522f3 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/test_driver/integration_test.dart +++ b/packages/google_maps_flutter/google_maps_flutter/example/test_driver/integration_test.dart @@ -1,17 +1,7 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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'; +import 'package:integration_test/integration_test_driver.dart'; -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String data = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - await driver.close(); - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} +Future main() => integrationDriver(); diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.h new file mode 100644 index 000000000000..356a13faba62 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.h @@ -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 + +NS_ASSUME_NONNULL_BEGIN + +// Defines map UI options writable from Flutter. +@protocol FLTGoogleMapTileOverlayOptionsSink +- (void)setFadeIn:(BOOL)fadeIn; +- (void)setTransparency:(float)transparency; +- (void)setZIndex:(int)zIndex; +- (void)setVisible:(BOOL)visible; +- (void)setTileSize:(NSInteger)tileSize; +@end + +@interface FLTGoogleMapTileOverlayController : NSObject +- (instancetype)initWithTileLayer:(GMSTileLayer *)tileLayer mapView:(GMSMapView *)mapView; +- (void)removeTileOverlay; +- (void)clearTileCache; +- (NSDictionary *)getTileOverlayInfo; +@end + +@interface FLTTileProviderController : GMSTileLayer +@property(copy, nonatomic, readonly) NSString *tileOverlayId; +- (instancetype)init:(FlutterMethodChannel *)methodChannel tileOverlayId:(NSString *)tileOverlayId; +@end + +@interface FLTTileOverlaysController : NSObject +- (instancetype)init:(FlutterMethodChannel *)methodChannel + mapView:(GMSMapView *)mapView + registrar:(NSObject *)registrar; +- (void)addTileOverlays:(NSArray *)tileOverlaysToAdd; +- (void)changeTileOverlays:(NSArray *)tileOverlaysToChange; +- (void)removeTileOverlayIds:(NSArray *)tileOverlayIdsToRemove; +- (void)clearTileCache:(NSString *)tileOverlayId; +- (nullable NSDictionary *)getTileOverlayInfo:(NSString *)tileverlayId; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.m new file mode 100644 index 000000000000..fb391380c92c --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapTileOverlayController.m @@ -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. + +#import "FLTGoogleMapTileOverlayController.h" +#import "JsonConversions.h" + +static void InterpretTileOverlayOptions(NSDictionary* data, + id sink, + NSObject* registrar) { + NSNumber* visible = data[@"visible"]; + if (visible != nil) { + [sink setVisible:visible.boolValue]; + } + + NSNumber* transparency = data[@"transparency"]; + if (transparency != nil) { + [sink setTransparency:transparency.floatValue]; + } + + NSNumber* zIndex = data[@"zIndex"]; + if (zIndex != nil) { + [sink setZIndex:zIndex.intValue]; + } + + NSNumber* fadeIn = data[@"fadeIn"]; + if (fadeIn != nil) { + [sink setFadeIn:fadeIn.boolValue]; + } + + NSNumber* tileSize = data[@"tileSize"]; + if (tileSize != nil) { + [sink setTileSize:tileSize.integerValue]; + } +} + +@interface FLTGoogleMapTileOverlayController () + +@property(strong, nonatomic) GMSTileLayer* layer; +@property(weak, nonatomic) GMSMapView* mapView; + +@end + +@implementation FLTGoogleMapTileOverlayController + +- (instancetype)initWithTileLayer:(GMSTileLayer*)tileLayer mapView:(GMSMapView*)mapView { + self = [super init]; + if (self) { + self.layer = tileLayer; + self.mapView = mapView; + } + return self; +} + +- (void)removeTileOverlay { + self.layer.map = nil; +} + +- (void)clearTileCache { + [self.layer clearTileCache]; +} + +- (NSDictionary*)getTileOverlayInfo { + NSMutableDictionary* info = [[NSMutableDictionary alloc] init]; + BOOL visible = self.layer.map != nil; + info[@"visible"] = @(visible); + info[@"fadeIn"] = @(self.layer.fadeIn); + float transparency = 1.0 - self.layer.opacity; + info[@"transparency"] = @(transparency); + info[@"zIndex"] = @(self.layer.zIndex); + return info; +} + +#pragma mark - FLTGoogleMapTileOverlayOptionsSink methods + +- (void)setFadeIn:(BOOL)fadeIn { + self.layer.fadeIn = fadeIn; +} + +- (void)setTransparency:(float)transparency { + float opacity = 1.0 - transparency; + self.layer.opacity = opacity; +} + +- (void)setVisible:(BOOL)visible { + self.layer.map = visible ? self.mapView : nil; +} + +- (void)setZIndex:(int)zIndex { + self.layer.zIndex = zIndex; +} + +- (void)setTileSize:(NSInteger)tileSize { + self.layer.tileSize = tileSize; +} +@end + +@interface FLTTileProviderController () + +@property(weak, nonatomic) FlutterMethodChannel* methodChannel; +@property(copy, nonatomic, readwrite) NSString* tileOverlayId; + +@end + +@implementation FLTTileProviderController + +- (instancetype)init:(FlutterMethodChannel*)methodChannel tileOverlayId:(NSString*)tileOverlayId { + self = [super init]; + if (self) { + self.methodChannel = methodChannel; + self.tileOverlayId = tileOverlayId; + } + return self; +} + +#pragma mark - GMSTileLayer method + +- (void)requestTileForX:(NSUInteger)x + y:(NSUInteger)y + zoom:(NSUInteger)zoom + receiver:(id)receiver { + [self.methodChannel + invokeMethod:@"tileOverlay#getTile" + arguments:@{ + @"tileOverlayId" : self.tileOverlayId, + @"x" : @(x), + @"y" : @(y), + @"zoom" : @(zoom) + } + result:^(id _Nullable result) { + UIImage* tileImage; + if ([result isKindOfClass:[NSDictionary class]]) { + FlutterStandardTypedData* typedData = (FlutterStandardTypedData*)result[@"data"]; + if (typedData == nil) { + tileImage = kGMSTileLayerNoTile; + } else { + tileImage = [UIImage imageWithData:typedData.data]; + } + } else { + if ([result isKindOfClass:[FlutterError class]]) { + FlutterError* error = (FlutterError*)result; + NSLog(@"Can't get tile: errorCode = %@, errorMessage = %@, details = %@", + [error code], [error message], [error details]); + } + if ([result isKindOfClass:[FlutterMethodNotImplemented class]]) { + NSLog(@"Can't get tile: notImplemented"); + } + tileImage = kGMSTileLayerNoTile; + } + + [receiver receiveTileWithX:x y:y zoom:zoom image:tileImage]; + }]; +} + +@end + +@interface FLTTileOverlaysController () + +@property(strong, nonatomic) NSMutableDictionary* tileOverlayIdToController; +@property(weak, nonatomic) FlutterMethodChannel* methodChannel; +@property(weak, nonatomic) NSObject* registrar; +@property(weak, nonatomic) GMSMapView* mapView; + +@end + +@implementation FLTTileOverlaysController + +- (instancetype)init:(FlutterMethodChannel*)methodChannel + mapView:(GMSMapView*)mapView + registrar:(NSObject*)registrar { + self = [super init]; + if (self) { + self.methodChannel = methodChannel; + self.mapView = mapView; + self.tileOverlayIdToController = [[NSMutableDictionary alloc] init]; + self.registrar = registrar; + } + return self; +} + +- (void)addTileOverlays:(NSArray*)tileOverlaysToAdd { + for (NSDictionary* tileOverlay in tileOverlaysToAdd) { + NSString* tileOverlayId = [FLTTileOverlaysController getTileOverlayId:tileOverlay]; + FLTTileProviderController* tileProvider = + [[FLTTileProviderController alloc] init:self.methodChannel tileOverlayId:tileOverlayId]; + FLTGoogleMapTileOverlayController* controller = + [[FLTGoogleMapTileOverlayController alloc] initWithTileLayer:tileProvider + mapView:self.mapView]; + InterpretTileOverlayOptions(tileOverlay, controller, self.registrar); + self.tileOverlayIdToController[tileOverlayId] = controller; + } +} + +- (void)changeTileOverlays:(NSArray*)tileOverlaysToChange { + for (NSDictionary* tileOverlay in tileOverlaysToChange) { + NSString* tileOverlayId = [FLTTileOverlaysController getTileOverlayId:tileOverlay]; + FLTGoogleMapTileOverlayController* controller = self.tileOverlayIdToController[tileOverlayId]; + if (!controller) { + continue; + } + InterpretTileOverlayOptions(tileOverlay, controller, self.registrar); + } +} +- (void)removeTileOverlayIds:(NSArray*)tileOverlayIdsToRemove { + for (NSString* tileOverlayId in tileOverlayIdsToRemove) { + FLTGoogleMapTileOverlayController* controller = self.tileOverlayIdToController[tileOverlayId]; + if (!controller) { + continue; + } + [controller removeTileOverlay]; + [self.tileOverlayIdToController removeObjectForKey:tileOverlayId]; + } +} + +- (void)clearTileCache:(NSString*)tileOverlayId { + FLTGoogleMapTileOverlayController* controller = self.tileOverlayIdToController[tileOverlayId]; + if (!controller) { + return; + } + [controller clearTileCache]; +} + +- (nullable NSDictionary*)getTileOverlayInfo:(NSString*)tileverlayId { + if (self.tileOverlayIdToController[tileverlayId] == nil) { + return nil; + } + return [self.tileOverlayIdToController[tileverlayId] getTileOverlayInfo]; +} + ++ (NSString*)getTileOverlayId:(NSDictionary*)tileOverlay { + return tileOverlay[@"tileOverlayId"]; +} + +@end diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapsPlugin.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapsPlugin.h index 645ace34f9ed..953c0557ff20 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapsPlugin.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/FLTGoogleMapsPlugin.m index 14585bcdb29c..7ce2cf1c204d 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapCircleController.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapCircleController.h index 166cf996a572..2e7a9967ebd3 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapCircleController.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapCircleController.m index 6688d4d57695..bdf36484aaf7 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.h index 1bc8536f97d9..a8cebb983347 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapController.m index 321ddd318536..be3728753a5d 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 @@ -1,8 +1,9 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #import "GoogleMapController.h" +#import "FLTGoogleMapTileOverlayController.h" #import "JsonConversions.h" #pragma mark - Conversion of JSON-like values sent via platform channels. Forward declarations. @@ -55,6 +56,7 @@ @implementation FLTGoogleMapController { FLTPolygonsController* _polygonsController; FLTPolylinesController* _polylinesController; FLTCirclesController* _circlesController; + FLTTileOverlaysController* _tileOverlaysController; } - (instancetype)initWithFrame:(CGRect)frame @@ -94,6 +96,9 @@ - (instancetype)initWithFrame:(CGRect)frame _circlesController = [[FLTCirclesController alloc] init:_channel mapView:_mapView registrar:registrar]; + _tileOverlaysController = [[FLTTileOverlaysController alloc] init:_channel + mapView:_mapView + registrar:registrar]; id markersToAdd = args[@"markersToAdd"]; if ([markersToAdd isKindOfClass:[NSArray class]]) { [_markersController addMarkers:markersToAdd]; @@ -110,6 +115,10 @@ - (instancetype)initWithFrame:(CGRect)frame if ([circlesToAdd isKindOfClass:[NSArray class]]) { [_circlesController addCircles:circlesToAdd]; } + id tileOverlaysToAdd = args[@"tileOverlaysToAdd"]; + if ([tileOverlaysToAdd isKindOfClass:[NSArray class]]) { + [_tileOverlaysController addTileOverlays:tileOverlaysToAdd]; + } } return self; } @@ -298,11 +307,29 @@ - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { [_circlesController removeCircleIds:circleIdsToRemove]; } result(nil); + } else if ([call.method isEqualToString:@"tileOverlays#update"]) { + id tileOverlaysToAdd = call.arguments[@"tileOverlaysToAdd"]; + if ([tileOverlaysToAdd isKindOfClass:[NSArray class]]) { + [_tileOverlaysController addTileOverlays:tileOverlaysToAdd]; + } + id tileOverlaysToChange = call.arguments[@"tileOverlaysToChange"]; + if ([tileOverlaysToChange isKindOfClass:[NSArray class]]) { + [_tileOverlaysController changeTileOverlays:tileOverlaysToChange]; + } + id tileOverlayIdsToRemove = call.arguments[@"tileOverlayIdsToRemove"]; + if ([tileOverlayIdsToRemove isKindOfClass:[NSArray class]]) { + [_tileOverlaysController removeTileOverlayIds:tileOverlayIdsToRemove]; + } + result(nil); + } else if ([call.method isEqualToString:@"tileOverlays#clearTileCache"]) { + id rawTileOverlayId = call.arguments[@"tileOverlayId"]; + [_tileOverlaysController clearTileCache:rawTileOverlayId]; + result(nil); } else if ([call.method isEqualToString:@"map#isCompassEnabled"]) { NSNumber* isCompassEnabled = @(_mapView.settings.compassButton); result(isCompassEnabled); } else if ([call.method isEqualToString:@"map#isMapToolbarEnabled"]) { - NSNumber* isMapToolbarEnabled = [NSNumber numberWithBool:NO]; + NSNumber* isMapToolbarEnabled = @NO; result(isMapToolbarEnabled); } else if ([call.method isEqualToString:@"map#getMinMaxZoomLevels"]) { NSArray* zoomLevels = @[ @(_mapView.minZoom), @(_mapView.maxZoom) ]; @@ -313,7 +340,7 @@ - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { NSNumber* isZoomGesturesEnabled = @(_mapView.settings.zoomGestures); result(isZoomGesturesEnabled); } else if ([call.method isEqualToString:@"map#isZoomControlsEnabled"]) { - NSNumber* isZoomControlsEnabled = [NSNumber numberWithBool:NO]; + NSNumber* isZoomControlsEnabled = @NO; result(isZoomControlsEnabled); } else if ([call.method isEqualToString:@"map#isTiltGesturesEnabled"]) { NSNumber* isTiltGesturesEnabled = @(_mapView.settings.tiltGestures); @@ -341,6 +368,9 @@ - (void)onMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { } else { result(@[ @(NO), error ]); } + } else if ([call.method isEqualToString:@"map#getTileOverlayInfo"]) { + NSString* rawTileOverlayId = call.arguments[@"tileOverlayId"]; + result([_tileOverlaysController getTileOverlayInfo:rawTileOverlayId]); } else { result(FlutterMethodNotImplemented); } diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapMarkerController.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapMarkerController.h index 593d2ff9931b..d3e835435ed9 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -52,4 +52,4 @@ NS_ASSUME_NONNULL_BEGIN - (void)isMarkerInfoWindowShown:(NSString*)markerId result:(FlutterResult)result; @end -NS_ASSUME_NONNULL_END \ No newline at end of file +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 cd51b2fd9896..6a9fb885afac 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolygonController.h b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolygonController.h index c7613fde5f93..b123ac0a3d68 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -13,6 +13,7 @@ - (void)setStrokeColor:(UIColor*)color; - (void)setStrokeWidth:(CGFloat)width; - (void)setPoints:(NSArray*)points; +- (void)setHoles:(NSArray*>*)holes; - (void)setZIndex:(int)zIndex; @end diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolygonController.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolygonController.m index 678d40e3efec..5ad8d4d3bc0e 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -45,6 +45,19 @@ - (void)setPoints:(NSArray*)points { } _polygon.path = path; } +- (void)setHoles:(NSArray*>*)rawHoles { + NSMutableArray* holes = [[NSMutableArray alloc] init]; + + for (NSArray* points in rawHoles) { + GMSMutablePath* path = [GMSMutablePath path]; + for (CLLocation* location in points) { + [path addCoordinate:location.coordinate]; + } + [holes addObject:path]; + } + + _polygon.holes = holes; +} - (void)setFillColor:(UIColor*)color { _polygon.fillColor = color; @@ -65,6 +78,10 @@ - (void)setStrokeWidth:(CGFloat)width { return [FLTGoogleMapJsonConversions toPoints:data]; } +static NSArray*>* ToHoles(NSArray* data) { + return [FLTGoogleMapJsonConversions toHoles:data]; +} + static UIColor* ToColor(NSNumber* data) { return [FLTGoogleMapJsonConversions toColor:data]; } static void InterpretPolygonOptions(NSDictionary* data, id sink, @@ -89,6 +106,11 @@ static void InterpretPolygonOptions(NSDictionary* data, id*)toPoints:(NSArray*)data; ++ (NSArray*>*)toHoles:(NSArray*)data; @end diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/JsonConversions.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/JsonConversions.m index 6381beaee8d2..592d7e825b38 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/JsonConversions.m +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/JsonConversions.m @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -58,4 +58,14 @@ + (UIColor*)toColor:(NSNumber*)numberColor { return points; } ++ (NSArray*>*)toHoles:(NSArray*)data { + NSMutableArray*>* holes = [[[NSMutableArray alloc] init] init]; + for (unsigned i = 0; i < [data count]; i++) { + NSArray* points = [FLTGoogleMapJsonConversions toPoints:data[i]]; + [holes addObject:points]; + } + + return holes; +} + @end diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/google_maps_flutter.podspec b/packages/google_maps_flutter/google_maps_flutter/ios/google_maps_flutter.podspec index 021abfee71ab..9a1f04d59759 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 @@ -17,8 +17,7 @@ Downloaded by pub (not CocoaPods). s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' - # TODO: Unpin this once the fix for b/163474612 or b/163359804 rolls (avoid v3.10!) - s.dependency 'GoogleMaps', '< 3.10' + s.dependency 'GoogleMaps' s.static_framework = true s.platform = :ios, '8.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart index b879f3d302cf..93bb0566dd1f 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -13,7 +13,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'package:google_maps_flutter_platform_interface/src/method_channel/method_channel_google_maps_flutter.dart'; @@ -43,7 +42,11 @@ export 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf PolygonId, Polyline, PolylineId, - ScreenCoordinate; + ScreenCoordinate, + Tile, + TileOverlayId, + TileOverlay, + TileProvider; part 'src/controller.dart'; part 'src/google_map.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/controller.dart index f47b8e57b049..ba18c5ffc17b 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 @@ -1,12 +1,9 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. part of google_maps_flutter; -final GoogleMapsFlutterPlatform _googleMapsFlutterPlatform = - GoogleMapsFlutterPlatform.instance; - /// Controller for a single GoogleMap instance running on the host platform. class GoogleMapController { /// The mapId for this controller @@ -15,8 +12,8 @@ class GoogleMapController { GoogleMapController._( CameraPosition initialCameraPosition, this._googleMapState, { - @required this.mapId, - }) : assert(_googleMapsFlutterPlatform != null) { + required this.mapId, + }) { _connectStreams(mapId); } @@ -30,7 +27,7 @@ class GoogleMapController { _GoogleMapState googleMapState, ) async { assert(id != null); - await _googleMapsFlutterPlatform.init(id); + await GoogleMapsFlutterPlatform.instance.init(id); return GoogleMapController._( initialCameraPosition, googleMapState, @@ -43,9 +40,10 @@ class GoogleMapController { /// Accessible only for testing. // TODO(dit) https://github.com/flutter/flutter/issues/55504 Remove this getter. @visibleForTesting - MethodChannel get channel { - if (_googleMapsFlutterPlatform is MethodChannelGoogleMapsFlutter) { - return (_googleMapsFlutterPlatform as MethodChannelGoogleMapsFlutter) + MethodChannel? get channel { + if (GoogleMapsFlutterPlatform.instance is MethodChannelGoogleMapsFlutter) { + return (GoogleMapsFlutterPlatform.instance + as MethodChannelGoogleMapsFlutter) .channel(mapId); } return null; @@ -55,40 +53,40 @@ class GoogleMapController { void _connectStreams(int mapId) { if (_googleMapState.widget.onCameraMoveStarted != null) { - _googleMapsFlutterPlatform + GoogleMapsFlutterPlatform.instance .onCameraMoveStarted(mapId: mapId) - .listen((_) => _googleMapState.widget.onCameraMoveStarted()); + .listen((_) => _googleMapState.widget.onCameraMoveStarted!()); } if (_googleMapState.widget.onCameraMove != null) { - _googleMapsFlutterPlatform.onCameraMove(mapId: mapId).listen( - (CameraMoveEvent e) => _googleMapState.widget.onCameraMove(e.value)); + GoogleMapsFlutterPlatform.instance.onCameraMove(mapId: mapId).listen( + (CameraMoveEvent e) => _googleMapState.widget.onCameraMove!(e.value)); } if (_googleMapState.widget.onCameraIdle != null) { - _googleMapsFlutterPlatform + GoogleMapsFlutterPlatform.instance .onCameraIdle(mapId: mapId) - .listen((_) => _googleMapState.widget.onCameraIdle()); + .listen((_) => _googleMapState.widget.onCameraIdle!()); } - _googleMapsFlutterPlatform + GoogleMapsFlutterPlatform.instance .onMarkerTap(mapId: mapId) .listen((MarkerTapEvent e) => _googleMapState.onMarkerTap(e.value)); - _googleMapsFlutterPlatform.onMarkerDragEnd(mapId: mapId).listen( + GoogleMapsFlutterPlatform.instance.onMarkerDragEnd(mapId: mapId).listen( (MarkerDragEndEvent e) => _googleMapState.onMarkerDragEnd(e.value, e.position)); - _googleMapsFlutterPlatform.onInfoWindowTap(mapId: mapId).listen( + GoogleMapsFlutterPlatform.instance.onInfoWindowTap(mapId: mapId).listen( (InfoWindowTapEvent e) => _googleMapState.onInfoWindowTap(e.value)); - _googleMapsFlutterPlatform + GoogleMapsFlutterPlatform.instance .onPolylineTap(mapId: mapId) .listen((PolylineTapEvent e) => _googleMapState.onPolylineTap(e.value)); - _googleMapsFlutterPlatform + GoogleMapsFlutterPlatform.instance .onPolygonTap(mapId: mapId) .listen((PolygonTapEvent e) => _googleMapState.onPolygonTap(e.value)); - _googleMapsFlutterPlatform + GoogleMapsFlutterPlatform.instance .onCircleTap(mapId: mapId) .listen((CircleTapEvent e) => _googleMapState.onCircleTap(e.value)); - _googleMapsFlutterPlatform + GoogleMapsFlutterPlatform.instance .onTap(mapId: mapId) .listen((MapTapEvent e) => _googleMapState.onTap(e.position)); - _googleMapsFlutterPlatform.onLongPress(mapId: mapId).listen( + GoogleMapsFlutterPlatform.instance.onLongPress(mapId: mapId).listen( (MapLongPressEvent e) => _googleMapState.onLongPress(e.position)); } @@ -100,8 +98,8 @@ class GoogleMapController { /// The returned [Future] completes after listeners have been notified. Future _updateMapOptions(Map optionsUpdate) { assert(optionsUpdate != null); - return _googleMapsFlutterPlatform.updateMapOptions(optionsUpdate, - mapId: mapId); + return GoogleMapsFlutterPlatform.instance + .updateMapOptions(optionsUpdate, mapId: mapId); } /// Updates marker configuration. @@ -112,8 +110,8 @@ class GoogleMapController { /// The returned [Future] completes after listeners have been notified. Future _updateMarkers(MarkerUpdates markerUpdates) { assert(markerUpdates != null); - return _googleMapsFlutterPlatform.updateMarkers(markerUpdates, - mapId: mapId); + return GoogleMapsFlutterPlatform.instance + .updateMarkers(markerUpdates, mapId: mapId); } /// Updates polygon configuration. @@ -124,8 +122,8 @@ class GoogleMapController { /// The returned [Future] completes after listeners have been notified. Future _updatePolygons(PolygonUpdates polygonUpdates) { assert(polygonUpdates != null); - return _googleMapsFlutterPlatform.updatePolygons(polygonUpdates, - mapId: mapId); + return GoogleMapsFlutterPlatform.instance + .updatePolygons(polygonUpdates, mapId: mapId); } /// Updates polyline configuration. @@ -136,8 +134,8 @@ class GoogleMapController { /// The returned [Future] completes after listeners have been notified. Future _updatePolylines(PolylineUpdates polylineUpdates) { assert(polylineUpdates != null); - return _googleMapsFlutterPlatform.updatePolylines(polylineUpdates, - mapId: mapId); + return GoogleMapsFlutterPlatform.instance + .updatePolylines(polylineUpdates, mapId: mapId); } /// Updates circle configuration. @@ -148,8 +146,32 @@ class GoogleMapController { /// The returned [Future] completes after listeners have been notified. Future _updateCircles(CircleUpdates circleUpdates) { assert(circleUpdates != null); - return _googleMapsFlutterPlatform.updateCircles(circleUpdates, - mapId: mapId); + return GoogleMapsFlutterPlatform.instance + .updateCircles(circleUpdates, mapId: mapId); + } + + /// Updates tile overlays configuration. + /// + /// Change listeners are notified once the update has been made on the + /// platform side. + /// + /// The returned [Future] completes after listeners have been notified. + Future _updateTileOverlays(Set newTileOverlays) { + return GoogleMapsFlutterPlatform.instance + .updateTileOverlays(newTileOverlays: newTileOverlays, mapId: mapId); + } + + /// Clears the tile cache so that all tiles will be requested again from the + /// [TileProvider]. + /// + /// The current tiles from this tile overlay will also be + /// cleared from the map after calling this method. The API maintains a small + /// in-memory cache of tiles. If you want to cache tiles for longer, you + /// should implement an on-disk cache. + Future clearTileCache(TileOverlayId tileOverlayId) async { + assert(tileOverlayId != null); + return GoogleMapsFlutterPlatform.instance + .clearTileCache(tileOverlayId, mapId: mapId); } /// Starts an animated change of the map camera position. @@ -157,7 +179,8 @@ class GoogleMapController { /// The returned [Future] completes after the change has been started on the /// platform side. Future animateCamera(CameraUpdate cameraUpdate) { - return _googleMapsFlutterPlatform.animateCamera(cameraUpdate, mapId: mapId); + return GoogleMapsFlutterPlatform.instance + .animateCamera(cameraUpdate, mapId: mapId); } /// Changes the map camera position. @@ -165,7 +188,8 @@ class GoogleMapController { /// The returned [Future] completes after the change has been made on the /// platform side. Future moveCamera(CameraUpdate cameraUpdate) { - return _googleMapsFlutterPlatform.moveCamera(cameraUpdate, mapId: mapId); + return GoogleMapsFlutterPlatform.instance + .moveCamera(cameraUpdate, mapId: mapId); } /// Sets the styling of the base map. @@ -181,13 +205,14 @@ class GoogleMapController { /// Also, refer [iOS](https://developers.google.com/maps/documentation/ios-sdk/style-reference) /// and [Android](https://developers.google.com/maps/documentation/android-sdk/style-reference) /// style reference for more information regarding the supported styles. - Future setMapStyle(String mapStyle) { - return _googleMapsFlutterPlatform.setMapStyle(mapStyle, mapId: mapId); + Future setMapStyle(String? mapStyle) { + return GoogleMapsFlutterPlatform.instance + .setMapStyle(mapStyle, mapId: mapId); } /// Return [LatLngBounds] defining the region that is visible in a map. Future getVisibleRegion() { - return _googleMapsFlutterPlatform.getVisibleRegion(mapId: mapId); + return GoogleMapsFlutterPlatform.instance.getVisibleRegion(mapId: mapId); } /// Return [ScreenCoordinate] of the [LatLng] in the current map view. @@ -196,7 +221,8 @@ class GoogleMapController { /// Screen location is in screen pixels (not display pixels) with respect to the top left corner /// of the map, not necessarily of the whole screen. Future getScreenCoordinate(LatLng latLng) { - return _googleMapsFlutterPlatform.getScreenCoordinate(latLng, mapId: mapId); + return GoogleMapsFlutterPlatform.instance + .getScreenCoordinate(latLng, mapId: mapId); } /// Returns [LatLng] corresponding to the [ScreenCoordinate] in the current map view. @@ -204,7 +230,8 @@ class GoogleMapController { /// Returned [LatLng] corresponds to a screen location. The screen location is specified in screen /// pixels (not display pixels) relative to the top left of the map, not top left of the whole screen. Future getLatLng(ScreenCoordinate screenCoordinate) { - return _googleMapsFlutterPlatform.getLatLng(screenCoordinate, mapId: mapId); + return GoogleMapsFlutterPlatform.instance + .getLatLng(screenCoordinate, mapId: mapId); } /// Programmatically show the Info Window for a [Marker]. @@ -217,8 +244,8 @@ class GoogleMapController { /// * [isMarkerInfoWindowShown] to check if the Info Window is showing. Future showMarkerInfoWindow(MarkerId markerId) { assert(markerId != null); - return _googleMapsFlutterPlatform.showMarkerInfoWindow(markerId, - mapId: mapId); + return GoogleMapsFlutterPlatform.instance + .showMarkerInfoWindow(markerId, mapId: mapId); } /// Programmatically hide the Info Window for a [Marker]. @@ -231,8 +258,8 @@ class GoogleMapController { /// * [isMarkerInfoWindowShown] to check if the Info Window is showing. Future hideMarkerInfoWindow(MarkerId markerId) { assert(markerId != null); - return _googleMapsFlutterPlatform.hideMarkerInfoWindow(markerId, - mapId: mapId); + return GoogleMapsFlutterPlatform.instance + .hideMarkerInfoWindow(markerId, mapId: mapId); } /// Returns `true` when the [InfoWindow] is showing, `false` otherwise. @@ -245,22 +272,22 @@ class GoogleMapController { /// * [hideMarkerInfoWindow] to hide the Info Window. Future isMarkerInfoWindowShown(MarkerId markerId) { assert(markerId != null); - return _googleMapsFlutterPlatform.isMarkerInfoWindowShown(markerId, - mapId: mapId); + return GoogleMapsFlutterPlatform.instance + .isMarkerInfoWindowShown(markerId, mapId: mapId); } /// Returns the current zoom level of the map Future getZoomLevel() { - return _googleMapsFlutterPlatform.getZoomLevel(mapId: mapId); + return GoogleMapsFlutterPlatform.instance.getZoomLevel(mapId: mapId); } /// Returns the image bytes of the map - Future takeSnapshot() { - return _googleMapsFlutterPlatform.takeSnapshot(mapId: mapId); + Future takeSnapshot() { + return GoogleMapsFlutterPlatform.instance.takeSnapshot(mapId: mapId); } /// Disposes of the platform resources void dispose() { - _googleMapsFlutterPlatform.dispose(mapId: mapId); + GoogleMapsFlutterPlatform.instance.dispose(mapId: mapId); } } diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart b/packages/google_maps_flutter/google_maps_flutter/lib/src/google_map.dart index d7f0f1a4e280..26b9d6b83c84 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -14,7 +14,29 @@ typedef void MapCreatedCallback(GoogleMapController controller); // to the buildView function, so the web implementation can use it as a // cache key. This needs to be provided from the outside, because web // views seem to re-render much more often that mobile platform views. -int _webOnlyMapId = 0; +int _nextMapCreationId = 0; + +/// Error thrown when an unknown map object ID is provided to a method. +class UnknownMapObjectIdError extends Error { + /// Creates an assertion error with the provided [message]. + UnknownMapObjectIdError(this.objectType, this.objectId, [this.context]); + + /// The name of the map object whose ID is unknown. + final String objectType; + + /// The unknown maps object ID. + final MapsObjectId objectId; + + /// The context where the error occurred. + final String? context; + + String toString() { + if (context != null) { + return 'Unknown $objectType ID "${objectId.value}" in $context'; + } + return 'Unknown $objectType ID "${objectId.value}"'; + } +} /// A widget which displays a map with data obtained from the Google Maps service. class GoogleMap extends StatefulWidget { @@ -22,10 +44,10 @@ class GoogleMap extends StatefulWidget { /// /// [AssertionError] will be thrown if [initialCameraPosition] is null; const GoogleMap({ - Key key, - @required this.initialCameraPosition, + Key? key, + required this.initialCameraPosition, this.onMapCreated, - this.gestureRecognizers, + this.gestureRecognizers = const >{}, this.compassEnabled = true, this.mapToolbarEnabled = true, this.cameraTargetBounds = CameraTargetBounds.unbounded, @@ -45,11 +67,12 @@ class GoogleMap extends StatefulWidget { this.indoorViewEnabled = false, this.trafficEnabled = false, this.buildingsEnabled = true, - this.markers, - this.polygons, - this.polylines, - this.circles, + this.markers = const {}, + this.polygons = const {}, + this.polylines = const {}, + this.circles = const {}, this.onCameraMoveStarted, + this.tileOverlays = const {}, this.onCameraMove, this.onCameraIdle, this.onTap, @@ -60,7 +83,7 @@ class GoogleMap extends StatefulWidget { /// Callback method for when the map is ready to be used. /// /// Used to receive a [GoogleMapController] for this [GoogleMap]. - final MapCreatedCallback onMapCreated; + final MapCreatedCallback? onMapCreated; /// The initial position of the map's camera. final CameraPosition initialCameraPosition; @@ -120,6 +143,9 @@ class GoogleMap extends StatefulWidget { /// Circles to be placed on the map. final Set circles; + /// Tile overlays to be placed on the map. + final Set tileOverlays; + /// Called when the camera starts moving. /// /// This can be initiated by the following: @@ -128,24 +154,24 @@ class GoogleMap extends StatefulWidget { /// 2. Programmatically initiated animation. /// 3. Camera motion initiated in response to user gestures on the map. /// For example: pan, tilt, pinch to zoom, or rotate. - final VoidCallback onCameraMoveStarted; + final VoidCallback? onCameraMoveStarted; /// Called repeatedly as the camera continues to move after an /// onCameraMoveStarted call. /// /// This may be called as often as once every frame and should /// not perform expensive operations. - final CameraPositionCallback onCameraMove; + final CameraPositionCallback? onCameraMove; /// Called when camera movement has ended, there are no pending /// animations and the user has stopped interacting with the map. - final VoidCallback onCameraIdle; + final VoidCallback? onCameraIdle; /// Called every time a [GoogleMap] is tapped. - final ArgumentCallback onTap; + final ArgumentCallback? onTap; /// Called every time a [GoogleMap] is long pressed. - final ArgumentCallback onLongPress; + final ArgumentCallback? onLongPress; /// True if a "My Location" layer should be shown on the map. /// @@ -201,7 +227,7 @@ class GoogleMap extends StatefulWidget { /// vertical drags. The map will claim gestures that are recognized by any of the /// recognizers on this list. /// - /// When this set is empty or null, the map will only handle pointer events for gestures that + /// When this set is empty, the map will only handle pointer events for gestures that /// were not claimed by any other gesture recognizer. final Set> gestureRecognizers; @@ -211,7 +237,7 @@ class GoogleMap extends StatefulWidget { } class _GoogleMapState extends State { - final _webOnlyMapCreationId = _webOnlyMapId++; + final _mapId = _nextMapCreationId++; final Completer _controller = Completer(); @@ -220,24 +246,20 @@ class _GoogleMapState extends State { Map _polygons = {}; Map _polylines = {}; Map _circles = {}; - _GoogleMapOptions _googleMapOptions; + late _GoogleMapOptions _googleMapOptions; @override Widget build(BuildContext context) { - final Map creationParams = { - 'initialCameraPosition': widget.initialCameraPosition?.toMap(), - 'options': _googleMapOptions.toMap(), - 'markersToAdd': serializeMarkerSet(widget.markers), - 'polygonsToAdd': serializePolygonSet(widget.polygons), - 'polylinesToAdd': serializePolylineSet(widget.polylines), - 'circlesToAdd': serializeCircleSet(widget.circles), - '_webOnlyMapCreationId': _webOnlyMapCreationId, - }; - - return _googleMapsFlutterPlatform.buildView( - creationParams, - widget.gestureRecognizers, + return GoogleMapsFlutterPlatform.instance.buildView( + _mapId, onPlatformViewCreated, + initialCameraPosition: widget.initialCameraPosition, + markers: widget.markers, + polygons: widget.polygons, + polylines: widget.polylines, + circles: widget.circles, + gestureRecognizers: widget.gestureRecognizers, + mapOptions: _googleMapOptions.toMap(), ); } @@ -266,6 +288,7 @@ class _GoogleMapState extends State { _updatePolygons(); _updatePolylines(); _updateCircles(); + _updateTileOverlays(); } void _updateOptions() async { @@ -313,6 +336,12 @@ class _GoogleMapState extends State { _circles = keyByCircleId(widget.circles); } + void _updateTileOverlays() async { + final GoogleMapController controller = await _controller.future; + // ignore: unawaited_futures + controller._updateTileOverlays(widget.tileOverlays); + } + Future onPlatformViewCreated(int id) async { final GoogleMapController controller = await GoogleMapController.init( id, @@ -320,116 +349,124 @@ class _GoogleMapState extends State { this, ); _controller.complete(controller); - if (widget.onMapCreated != null) { - widget.onMapCreated(controller); + _updateTileOverlays(); + final MapCreatedCallback? onMapCreated = widget.onMapCreated; + if (onMapCreated != null) { + onMapCreated(controller); } } void onMarkerTap(MarkerId markerId) { assert(markerId != null); - if (_markers[markerId]?.onTap != null) { - _markers[markerId].onTap(); + final Marker? marker = _markers[markerId]; + if (marker == null) { + throw UnknownMapObjectIdError('marker', markerId, 'onTap'); + } + final VoidCallback? onTap = marker.onTap; + if (onTap != null) { + onTap(); } } void onMarkerDragEnd(MarkerId markerId, LatLng position) { assert(markerId != null); - if (_markers[markerId]?.onDragEnd != null) { - _markers[markerId].onDragEnd(position); + final Marker? marker = _markers[markerId]; + if (marker == null) { + throw UnknownMapObjectIdError('marker', markerId, 'onDragEnd'); + } + final ValueChanged? onDragEnd = marker.onDragEnd; + if (onDragEnd != null) { + onDragEnd(position); } } void onPolygonTap(PolygonId polygonId) { assert(polygonId != null); - _polygons[polygonId].onTap(); + final Polygon? polygon = _polygons[polygonId]; + if (polygon == null) { + throw UnknownMapObjectIdError('polygon', polygonId, 'onTap'); + } + final VoidCallback? onTap = polygon.onTap; + if (onTap != null) { + onTap(); + } } void onPolylineTap(PolylineId polylineId) { assert(polylineId != null); - if (_polylines[polylineId]?.onTap != null) { - _polylines[polylineId].onTap(); + final Polyline? polyline = _polylines[polylineId]; + if (polyline == null) { + throw UnknownMapObjectIdError('polyline', polylineId, 'onTap'); + } + final VoidCallback? onTap = polyline.onTap; + if (onTap != null) { + onTap(); } } void onCircleTap(CircleId circleId) { assert(circleId != null); - _circles[circleId].onTap(); + final Circle? circle = _circles[circleId]; + if (circle == null) { + throw UnknownMapObjectIdError('marker', circleId, 'onTap'); + } + final VoidCallback? onTap = circle.onTap; + if (onTap != null) { + onTap(); + } } void onInfoWindowTap(MarkerId markerId) { assert(markerId != null); - if (_markers[markerId]?.infoWindow?.onTap != null) { - _markers[markerId].infoWindow.onTap(); + final Marker? marker = _markers[markerId]; + if (marker == null) { + throw UnknownMapObjectIdError('marker', markerId, 'InfoWindow onTap'); + } + final VoidCallback? onTap = marker.infoWindow.onTap; + if (onTap != null) { + onTap(); } } void onTap(LatLng position) { assert(position != null); - if (widget.onTap != null) { - widget.onTap(position); + final ArgumentCallback? onTap = widget.onTap; + if (onTap != null) { + onTap(position); } } void onLongPress(LatLng position) { assert(position != null); - if (widget.onLongPress != null) { - widget.onLongPress(position); + final ArgumentCallback? onLongPress = widget.onLongPress; + if (onLongPress != null) { + onLongPress(position); } } } /// Configuration options for the GoogleMaps user interface. -/// -/// When used to change configuration, null values will be interpreted as -/// "do not change this configuration option". class _GoogleMapOptions { - _GoogleMapOptions({ - this.compassEnabled, - this.mapToolbarEnabled, - this.cameraTargetBounds, - this.mapType, - this.minMaxZoomPreference, - this.rotateGesturesEnabled, - this.scrollGesturesEnabled, - this.tiltGesturesEnabled, - this.trackCameraPosition, - this.zoomControlsEnabled, - this.zoomGesturesEnabled, - this.liteModeEnabled, - this.myLocationEnabled, - this.myLocationButtonEnabled, - this.padding, - this.indoorViewEnabled, - this.trafficEnabled, - this.buildingsEnabled, - }) { - assert(liteModeEnabled == null || - !liteModeEnabled || - (liteModeEnabled && Platform.isAndroid)); - } - - static _GoogleMapOptions fromWidget(GoogleMap map) { - return _GoogleMapOptions( - compassEnabled: map.compassEnabled, - mapToolbarEnabled: map.mapToolbarEnabled, - cameraTargetBounds: map.cameraTargetBounds, - mapType: map.mapType, - minMaxZoomPreference: map.minMaxZoomPreference, - rotateGesturesEnabled: map.rotateGesturesEnabled, - scrollGesturesEnabled: map.scrollGesturesEnabled, - tiltGesturesEnabled: map.tiltGesturesEnabled, - trackCameraPosition: map.onCameraMove != null, - zoomControlsEnabled: map.zoomControlsEnabled, - zoomGesturesEnabled: map.zoomGesturesEnabled, - liteModeEnabled: map.liteModeEnabled, - myLocationEnabled: map.myLocationEnabled, - myLocationButtonEnabled: map.myLocationButtonEnabled, - padding: map.padding, - indoorViewEnabled: map.indoorViewEnabled, - trafficEnabled: map.trafficEnabled, - buildingsEnabled: map.buildingsEnabled, - ); - } + _GoogleMapOptions.fromWidget(GoogleMap map) + : compassEnabled = map.compassEnabled, + mapToolbarEnabled = map.mapToolbarEnabled, + cameraTargetBounds = map.cameraTargetBounds, + mapType = map.mapType, + minMaxZoomPreference = map.minMaxZoomPreference, + rotateGesturesEnabled = map.rotateGesturesEnabled, + scrollGesturesEnabled = map.scrollGesturesEnabled, + tiltGesturesEnabled = map.tiltGesturesEnabled, + trackCameraPosition = map.onCameraMove != null, + zoomControlsEnabled = map.zoomControlsEnabled, + zoomGesturesEnabled = map.zoomGesturesEnabled, + liteModeEnabled = map.liteModeEnabled, + myLocationEnabled = map.myLocationEnabled, + myLocationButtonEnabled = map.myLocationButtonEnabled, + padding = map.padding, + indoorViewEnabled = map.indoorViewEnabled, + trafficEnabled = map.trafficEnabled, + buildingsEnabled = map.buildingsEnabled, + assert(!map.liteModeEnabled || Platform.isAndroid); final bool compassEnabled; @@ -468,38 +505,31 @@ class _GoogleMapOptions { final bool buildingsEnabled; Map toMap() { - final Map optionsMap = {}; - - void addIfNonNull(String fieldName, dynamic value) { - if (value != null) { - optionsMap[fieldName] = value; - } - } - - addIfNonNull('compassEnabled', compassEnabled); - addIfNonNull('mapToolbarEnabled', mapToolbarEnabled); - addIfNonNull('cameraTargetBounds', cameraTargetBounds?.toJson()); - addIfNonNull('mapType', mapType?.index); - addIfNonNull('minMaxZoomPreference', minMaxZoomPreference?.toJson()); - addIfNonNull('rotateGesturesEnabled', rotateGesturesEnabled); - addIfNonNull('scrollGesturesEnabled', scrollGesturesEnabled); - addIfNonNull('tiltGesturesEnabled', tiltGesturesEnabled); - addIfNonNull('zoomControlsEnabled', zoomControlsEnabled); - addIfNonNull('zoomGesturesEnabled', zoomGesturesEnabled); - addIfNonNull('liteModeEnabled', liteModeEnabled); - addIfNonNull('trackCameraPosition', trackCameraPosition); - addIfNonNull('myLocationEnabled', myLocationEnabled); - addIfNonNull('myLocationButtonEnabled', myLocationButtonEnabled); - addIfNonNull('padding', [ - padding?.top, - padding?.left, - padding?.bottom, - padding?.right, - ]); - addIfNonNull('indoorEnabled', indoorViewEnabled); - addIfNonNull('trafficEnabled', trafficEnabled); - addIfNonNull('buildingsEnabled', buildingsEnabled); - return optionsMap; + return { + 'compassEnabled': compassEnabled, + 'mapToolbarEnabled': mapToolbarEnabled, + 'cameraTargetBounds': cameraTargetBounds.toJson(), + 'mapType': mapType.index, + 'minMaxZoomPreference': minMaxZoomPreference.toJson(), + 'rotateGesturesEnabled': rotateGesturesEnabled, + 'scrollGesturesEnabled': scrollGesturesEnabled, + 'tiltGesturesEnabled': tiltGesturesEnabled, + 'zoomControlsEnabled': zoomControlsEnabled, + 'zoomGesturesEnabled': zoomGesturesEnabled, + 'liteModeEnabled': liteModeEnabled, + 'trackCameraPosition': trackCameraPosition, + 'myLocationEnabled': myLocationEnabled, + 'myLocationButtonEnabled': myLocationButtonEnabled, + 'padding': [ + padding.top, + padding.left, + padding.bottom, + padding.right, + ], + 'indoorEnabled': indoorViewEnabled, + 'trafficEnabled': trafficEnabled, + 'buildingsEnabled': buildingsEnabled, + }; } Map updatesMap(_GoogleMapOptions newOptions) { diff --git a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml index 0300a7c54589..0d7475857b31 100644 --- a/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/pubspec.yaml @@ -1,13 +1,27 @@ name: google_maps_flutter description: A Flutter plugin for integrating Google Maps in iOS and Android applications. -homepage: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter -version: 1.0.2 +repository: https://github.com/flutter/plugins/tree/master/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.6 + +environment: + sdk: '>=2.12.0 <3.0.0' + flutter: ">=2.0.0" + +flutter: + plugin: + platforms: + android: + package: io.flutter.plugins.googlemaps + pluginClass: GoogleMapsPlugin + ios: + pluginClass: FLTGoogleMapsPlugin dependencies: flutter: sdk: flutter - flutter_plugin_android_lifecycle: ^1.0.0 - google_maps_flutter_platform_interface: ^1.0.4 + flutter_plugin_android_lifecycle: ^2.0.1 + google_maps_flutter_platform_interface: ^2.0.0 dev_dependencies: flutter_test: @@ -17,20 +31,7 @@ dev_dependencies: # https://github.com/dart-lang/pub/issues/2101 is resolved. flutter_driver: sdk: flutter - test: ^1.6.0 - pedantic: ^1.8.0 - plugin_platform_interface: ^1.0.2 - mockito: ^4.1.1 - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.googlemaps - pluginClass: GoogleMapsPlugin - ios: - pluginClass: FLTGoogleMapsPlugin - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.22.0 <2.0.0" + 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/android_google_map_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/android_google_map_test.dart deleted file mode 100644 index 194efe9a66f0..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/test/android_google_map_test.dart +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -@TestOn('android') -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; - -import 'fake_maps_controllers.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final FakePlatformViewsController fakePlatformViewsController = - FakePlatformViewsController(); - - setUpAll(() { - SystemChannels.platform_views.setMockMethodCallHandler( - fakePlatformViewsController.fakePlatformViewsMethodHandler); - }); - - setUp(() { - fakePlatformViewsController.reset(); - }); - - testWidgets('Can update liteModeEnabled', (WidgetTester tester) async { - await tester.pumpWidget( - const Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), - liteModeEnabled: false, - ), - ), - ); - - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; - - expect(platformGoogleMap.liteModeEnabled, false); - - await tester.pumpWidget( - const Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), - liteModeEnabled: true, - ), - ), - ); - - expect(platformGoogleMap.liteModeEnabled, true); - }); -} diff --git a/packages/google_maps_flutter/google_maps_flutter/test/circle_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/circle_updates_test.dart index 3533ceb229e3..e0d1180a0abb 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -9,20 +9,6 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'fake_maps_controllers.dart'; -Set _toSet({Circle c1, Circle c2, Circle c3}) { - final Set res = Set.identity(); - if (c1 != null) { - res.add(c1); - } - if (c2 != null) { - res.add(c2); - } - if (c3 != null) { - res.add(c3); - } - return res; -} - Widget _mapWithCircles(Set circles) { return Directionality( textDirection: TextDirection.ltr, @@ -50,10 +36,10 @@ void main() { testWidgets('Initializing a circle', (WidgetTester tester) async { final Circle c1 = Circle(circleId: CircleId("circle_1")); - await tester.pumpWidget(_mapWithCircles(_toSet(c1: c1))); + await tester.pumpWidget(_mapWithCircles({c1})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.circlesToAdd.length, 1); final Circle initializedCircle = platformGoogleMap.circlesToAdd.first; @@ -66,11 +52,11 @@ void main() { final Circle c1 = Circle(circleId: CircleId("circle_1")); final Circle c2 = Circle(circleId: CircleId("circle_2")); - await tester.pumpWidget(_mapWithCircles(_toSet(c1: c1))); - await tester.pumpWidget(_mapWithCircles(_toSet(c1: c1, c2: c2))); + await tester.pumpWidget(_mapWithCircles({c1})); + await tester.pumpWidget(_mapWithCircles({c1, c2})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.circlesToAdd.length, 1); final Circle addedCircle = platformGoogleMap.circlesToAdd.first; @@ -84,11 +70,11 @@ void main() { testWidgets("Removing a circle", (WidgetTester tester) async { final Circle c1 = Circle(circleId: CircleId("circle_1")); - await tester.pumpWidget(_mapWithCircles(_toSet(c1: c1))); - await tester.pumpWidget(_mapWithCircles(null)); + await tester.pumpWidget(_mapWithCircles({c1})); + await tester.pumpWidget(_mapWithCircles({})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.circleIdsToRemove.length, 1); expect(platformGoogleMap.circleIdsToRemove.first, equals(c1.circleId)); @@ -100,11 +86,11 @@ void main() { final Circle c1 = Circle(circleId: CircleId("circle_1")); final Circle c2 = Circle(circleId: CircleId("circle_1"), radius: 10); - await tester.pumpWidget(_mapWithCircles(_toSet(c1: c1))); - await tester.pumpWidget(_mapWithCircles(_toSet(c1: c2))); + await tester.pumpWidget(_mapWithCircles({c1})); + await tester.pumpWidget(_mapWithCircles({c2})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.circlesToChange.length, 1); expect(platformGoogleMap.circlesToChange.first, equals(c2)); @@ -116,11 +102,11 @@ void main() { final Circle c1 = Circle(circleId: CircleId("circle_1")); final Circle c2 = Circle(circleId: CircleId("circle_1"), radius: 10); - await tester.pumpWidget(_mapWithCircles(_toSet(c1: c1))); - await tester.pumpWidget(_mapWithCircles(_toSet(c1: c2))); + await tester.pumpWidget(_mapWithCircles({c1})); + await tester.pumpWidget(_mapWithCircles({c2})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.circlesToChange.length, 1); final Circle update = platformGoogleMap.circlesToChange.first; @@ -131,16 +117,16 @@ void main() { testWidgets("Multi Update", (WidgetTester tester) async { Circle c1 = Circle(circleId: CircleId("circle_1")); Circle c2 = Circle(circleId: CircleId("circle_2")); - final Set prev = _toSet(c1: c1, c2: c2); + final Set prev = {c1, c2}; c1 = Circle(circleId: CircleId("circle_1"), visible: false); c2 = Circle(circleId: CircleId("circle_2"), radius: 10); - final Set cur = _toSet(c1: c1, c2: c2); + final Set cur = {c1, c2}; await tester.pumpWidget(_mapWithCircles(prev)); await tester.pumpWidget(_mapWithCircles(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.circlesToChange, cur); expect(platformGoogleMap.circleIdsToRemove.isEmpty, true); @@ -150,18 +136,18 @@ void main() { testWidgets("Multi Update", (WidgetTester tester) async { Circle c2 = Circle(circleId: CircleId("circle_2")); final Circle c3 = Circle(circleId: CircleId("circle_3")); - final Set prev = _toSet(c2: c2, c3: c3); + final Set prev = {c2, c3}; // c1 is added, c2 is updated, c3 is removed. final Circle c1 = Circle(circleId: CircleId("circle_1")); c2 = Circle(circleId: CircleId("circle_2"), radius: 10); - final Set cur = _toSet(c1: c1, c2: c2); + final Set cur = {c1, c2}; await tester.pumpWidget(_mapWithCircles(prev)); await tester.pumpWidget(_mapWithCircles(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.circlesToChange.length, 1); expect(platformGoogleMap.circlesToAdd.length, 1); @@ -176,32 +162,32 @@ void main() { final Circle c1 = Circle(circleId: CircleId("circle_1")); final Circle c2 = Circle(circleId: CircleId("circle_2")); Circle c3 = Circle(circleId: CircleId("circle_3")); - final Set prev = _toSet(c1: c1, c2: c2, c3: c3); + final Set prev = {c1, c2, c3}; c3 = Circle(circleId: CircleId("circle_3"), radius: 10); - final Set cur = _toSet(c1: c1, c2: c2, c3: c3); + final Set cur = {c1, c2, c3}; await tester.pumpWidget(_mapWithCircles(prev)); await tester.pumpWidget(_mapWithCircles(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.circlesToChange, _toSet(c3: c3)); + expect(platformGoogleMap.circlesToChange, {c3}); expect(platformGoogleMap.circleIdsToRemove.isEmpty, true); expect(platformGoogleMap.circlesToAdd.isEmpty, true); }); testWidgets("Update non platform related attr", (WidgetTester tester) async { Circle c1 = Circle(circleId: CircleId("circle_1")); - final Set prev = _toSet(c1: c1); + final Set prev = {c1}; c1 = Circle(circleId: CircleId("circle_1"), onTap: () => print("hello")); - final Set cur = _toSet(c1: c1); + final Set cur = {c1}; await tester.pumpWidget(_mapWithCircles(prev)); await tester.pumpWidget(_mapWithCircles(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.circlesToChange.isEmpty, true); expect(platformGoogleMap.circleIdsToRemove.isEmpty, true); diff --git a/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart b/packages/google_maps_flutter/google_maps_flutter/test/fake_maps_controllers.dart index adca8b4c2a0e..37270ea34d29 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -9,79 +9,87 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; class FakePlatformGoogleMap { - FakePlatformGoogleMap(int id, Map params) { - cameraPosition = CameraPosition.fromMap(params['initialCameraPosition']); - channel = MethodChannel( - 'plugins.flutter.io/google_maps_$id', const StandardMethodCodec()); + FakePlatformGoogleMap(int id, Map params) + : cameraPosition = + CameraPosition.fromMap(params['initialCameraPosition']), + channel = MethodChannel( + 'plugins.flutter.io/google_maps_$id', const StandardMethodCodec()) { channel.setMockMethodCallHandler(onMethodCall); updateOptions(params['options']); updateMarkers(params); updatePolygons(params); updatePolylines(params); updateCircles(params); + updateTileOverlays(Map.castFrom(params)); } MethodChannel channel; - CameraPosition cameraPosition; + CameraPosition? cameraPosition; - bool compassEnabled; + bool? compassEnabled; - bool mapToolbarEnabled; + bool? mapToolbarEnabled; - CameraTargetBounds cameraTargetBounds; + CameraTargetBounds? cameraTargetBounds; - MapType mapType; + MapType? mapType; - MinMaxZoomPreference minMaxZoomPreference; + MinMaxZoomPreference? minMaxZoomPreference; - bool rotateGesturesEnabled; + bool? rotateGesturesEnabled; - bool scrollGesturesEnabled; + bool? scrollGesturesEnabled; - bool tiltGesturesEnabled; + bool? tiltGesturesEnabled; - bool zoomGesturesEnabled; + bool? zoomGesturesEnabled; - bool zoomControlsEnabled; + bool? zoomControlsEnabled; - bool liteModeEnabled; + bool? liteModeEnabled; - bool trackCameraPosition; + bool? trackCameraPosition; - bool myLocationEnabled; + bool? myLocationEnabled; - bool trafficEnabled; + bool? trafficEnabled; - bool buildingsEnabled; + bool? buildingsEnabled; - bool myLocationButtonEnabled; + bool? myLocationButtonEnabled; - List padding; + List? padding; - Set markerIdsToRemove; + Set markerIdsToRemove = {}; - Set markersToAdd; + Set markersToAdd = {}; - Set markersToChange; + Set markersToChange = {}; - Set polygonIdsToRemove; + Set polygonIdsToRemove = {}; - Set polygonsToAdd; + Set polygonsToAdd = {}; - Set polygonsToChange; + Set polygonsToChange = {}; - Set polylineIdsToRemove; + Set polylineIdsToRemove = {}; - Set polylinesToAdd; + Set polylinesToAdd = {}; - Set polylinesToChange; + Set polylinesToChange = {}; - Set circleIdsToRemove; + Set circleIdsToRemove = {}; - Set circlesToAdd; + Set circlesToAdd = {}; - Set circlesToChange; + Set circlesToChange = {}; + + Set tileOverlayIdsToRemove = {}; + + Set tileOverlaysToAdd = {}; + + Set tileOverlaysToChange = {}; Future onMethodCall(MethodCall call) { switch (call.method) { @@ -97,6 +105,10 @@ class FakePlatformGoogleMap { case 'polylines#update': updatePolylines(call.arguments); return Future.sync(() {}); + case 'tileOverlays#update': + updateTileOverlays( + Map.castFrom(call.arguments)); + return Future.sync(() {}); case 'circles#update': updateCircles(call.arguments); return Future.sync(() {}); @@ -105,7 +117,7 @@ class FakePlatformGoogleMap { } } - void updateMarkers(Map markerUpdates) { + void updateMarkers(Map? markerUpdates) { if (markerUpdates == null) { return; } @@ -115,29 +127,21 @@ class FakePlatformGoogleMap { markersToChange = _deserializeMarkers(markerUpdates['markersToChange']); } - Set _deserializeMarkerIds(List markerIds) { + Set _deserializeMarkerIds(List? markerIds) { if (markerIds == null) { - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - return Set(); + return {}; } return markerIds.map((dynamic markerId) => MarkerId(markerId)).toSet(); } Set _deserializeMarkers(dynamic markers) { if (markers == null) { - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - return Set(); + return {}; } final List markersData = markers; - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - final Set result = Set(); - for (Map markerData in markersData) { + final Set result = {}; + for (Map markerData + in markersData.cast>()) { final String markerId = markerData['markerId']; final double alpha = markerData['alpha']; final bool draggable = markerData['draggable']; @@ -165,7 +169,7 @@ class FakePlatformGoogleMap { return result; } - void updatePolygons(Map polygonUpdates) { + void updatePolygons(Map? polygonUpdates) { if (polygonUpdates == null) { return; } @@ -175,39 +179,33 @@ class FakePlatformGoogleMap { polygonsToChange = _deserializePolygons(polygonUpdates['polygonsToChange']); } - Set _deserializePolygonIds(List polygonIds) { + Set _deserializePolygonIds(List? polygonIds) { if (polygonIds == null) { - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - return Set(); + return {}; } return polygonIds.map((dynamic polygonId) => PolygonId(polygonId)).toSet(); } Set _deserializePolygons(dynamic polygons) { if (polygons == null) { - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - return Set(); + return {}; } final List polygonsData = polygons; - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - final Set result = Set(); - for (Map polygonData in polygonsData) { + final Set result = {}; + for (Map polygonData + in polygonsData.cast>()) { final String polygonId = polygonData['polygonId']; final bool visible = polygonData['visible']; final bool geodesic = polygonData['geodesic']; final List points = _deserializePoints(polygonData['points']); + final List> holes = _deserializeHoles(polygonData['holes']); result.add(Polygon( polygonId: PolygonId(polygonId), visible: visible, geodesic: geodesic, points: points, + holes: holes, )); } @@ -220,7 +218,15 @@ class FakePlatformGoogleMap { }).toList(); } - void updatePolylines(Map polylineUpdates) { + List> _deserializeHoles(List holes) { + return holes.map>((dynamic hole) { + return hole.map((dynamic list) { + return LatLng(list[0], list[1]); + }).toList(); + }).toList(); + } + + void updatePolylines(Map? polylineUpdates) { if (polylineUpdates == null) { return; } @@ -231,12 +237,9 @@ class FakePlatformGoogleMap { _deserializePolylines(polylineUpdates['polylinesToChange']); } - Set _deserializePolylineIds(List polylineIds) { + Set _deserializePolylineIds(List? polylineIds) { if (polylineIds == null) { - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - return Set(); + return {}; } return polylineIds .map((dynamic polylineId) => PolylineId(polylineId)) @@ -245,17 +248,12 @@ class FakePlatformGoogleMap { Set _deserializePolylines(dynamic polylines) { if (polylines == null) { - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - return Set(); + return {}; } final List polylinesData = polylines; - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - final Set result = Set(); - for (Map polylineData in polylinesData) { + final Set result = {}; + for (Map polylineData + in polylinesData.cast>()) { final String polylineId = polylineData['polylineId']; final bool visible = polylineData['visible']; final bool geodesic = polylineData['geodesic']; @@ -272,7 +270,7 @@ class FakePlatformGoogleMap { return result; } - void updateCircles(Map circleUpdates) { + void updateCircles(Map? circleUpdates) { if (circleUpdates == null) { return; } @@ -282,29 +280,46 @@ class FakePlatformGoogleMap { circlesToChange = _deserializeCircles(circleUpdates['circlesToChange']); } - Set _deserializeCircleIds(List circleIds) { + void updateTileOverlays(Map updateTileOverlayUpdates) { + if (updateTileOverlayUpdates == null) { + return; + } + final List>? tileOverlaysToAddList = + updateTileOverlayUpdates['tileOverlaysToAdd'] != null + ? List.castFrom>( + updateTileOverlayUpdates['tileOverlaysToAdd']) + : null; + final List? tileOverlayIdsToRemoveList = + updateTileOverlayUpdates['tileOverlayIdsToRemove'] != null + ? List.castFrom( + updateTileOverlayUpdates['tileOverlayIdsToRemove']) + : null; + final List>? tileOverlaysToChangeList = + updateTileOverlayUpdates['tileOverlaysToChange'] != null + ? List.castFrom>( + updateTileOverlayUpdates['tileOverlaysToChange']) + : null; + tileOverlaysToAdd = _deserializeTileOverlays(tileOverlaysToAddList); + tileOverlayIdsToRemove = + _deserializeTileOverlayIds(tileOverlayIdsToRemoveList); + tileOverlaysToChange = _deserializeTileOverlays(tileOverlaysToChangeList); + } + + Set _deserializeCircleIds(List? circleIds) { if (circleIds == null) { - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - return Set(); + return {}; } return circleIds.map((dynamic circleId) => CircleId(circleId)).toSet(); } Set _deserializeCircles(dynamic circles) { if (circles == null) { - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - return Set(); + return {}; } final List circlesData = circles; - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // https://github.com/flutter/flutter/issues/28312 - // ignore: prefer_collection_literals - final Set result = Set(); - for (Map circleData in circlesData) { + final Set result = {}; + for (Map circleData + in circlesData.cast>()) { final String circleId = circleData['circleId']; final bool visible = circleData['visible']; final double radius = circleData['radius']; @@ -319,6 +334,40 @@ class FakePlatformGoogleMap { return result; } + Set _deserializeTileOverlayIds(List? tileOverlayIds) { + if (tileOverlayIds == null || tileOverlayIds.isEmpty) { + return {}; + } + return tileOverlayIds + .map((String tileOverlayId) => TileOverlayId(tileOverlayId)) + .toSet(); + } + + Set _deserializeTileOverlays( + List>? tileOverlays) { + if (tileOverlays == null || tileOverlays.isEmpty) { + return {}; + } + final Set result = {}; + for (Map tileOverlayData in tileOverlays) { + final String tileOverlayId = tileOverlayData['tileOverlayId']; + final bool fadeIn = tileOverlayData['fadeIn']; + final double transparency = tileOverlayData['transparency']; + final int zIndex = tileOverlayData['zIndex']; + final bool visible = tileOverlayData['visible']; + + result.add(TileOverlay( + tileOverlayId: TileOverlayId(tileOverlayId), + fadeIn: fadeIn, + transparency: transparency, + zIndex: zIndex, + visible: visible, + )); + } + + return result; + } + void updateOptions(Map options) { if (options.containsKey('compassEnabled')) { compassEnabled = options['compassEnabled']; @@ -380,13 +429,13 @@ class FakePlatformGoogleMap { } class FakePlatformViewsController { - FakePlatformGoogleMap lastCreatedView; + FakePlatformGoogleMap? lastCreatedView; Future fakePlatformViewsMethodHandler(MethodCall call) { switch (call.method) { case 'create': final Map args = call.arguments; - final Map params = _decodeParams(args['params']); + final Map params = _decodeParams(args['params'])!; lastCreatedView = FakePlatformGoogleMap( args['id'], params, @@ -402,7 +451,7 @@ class FakePlatformViewsController { } } -Map _decodeParams(Uint8List paramsMessage) { +Map? _decodeParams(Uint8List paramsMessage) { final ByteBuffer buffer = paramsMessage.buffer; final ByteData messageBytes = buffer.asByteData( paramsMessage.offsetInBytes, diff --git a/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/google_map_test.dart index 3c1eadb8d2a4..2b754afbd359 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -35,7 +35,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.cameraPosition, const CameraPosition(target: LatLng(10.0, 15.0))); @@ -62,7 +62,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.cameraPosition, const CameraPosition(target: LatLng(10.0, 15.0))); @@ -80,7 +80,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.compassEnabled, false); @@ -109,7 +109,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.mapToolbarEnabled, false); @@ -144,7 +144,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect( platformGoogleMap.cameraTargetBounds, @@ -193,7 +193,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.mapType, MapType.hybrid); @@ -222,7 +222,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.minMaxZoomPreference, const MinMaxZoomPreference(1.0, 3.0)); @@ -253,7 +253,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.rotateGesturesEnabled, false); @@ -282,7 +282,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.scrollGesturesEnabled, false); @@ -311,7 +311,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.tiltGesturesEnabled, false); @@ -339,7 +339,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.trackCameraPosition, false); @@ -369,7 +369,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.zoomGesturesEnabled, false); @@ -398,7 +398,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.zoomControlsEnabled, false); @@ -427,7 +427,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.myLocationEnabled, false); @@ -457,7 +457,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.myLocationButtonEnabled, true); @@ -485,7 +485,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.padding, [0, 0, 0, 0]); }); @@ -501,7 +501,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.padding, [0, 0, 0, 0]); @@ -542,7 +542,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.trafficEnabled, false); @@ -571,7 +571,7 @@ void main() { ); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.buildingsEnabled, false); diff --git a/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart index 5ea9a679a1be..6e0f5ed3e4f5 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 @@ -1,27 +1,27 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:mockito/mockito.dart'; - -class MockGoogleMapsFlutterPlatform extends Mock - with MockPlatformInterfaceMixin - implements GoogleMapsFlutterPlatform {} +import 'package:stream_transform/stream_transform.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - final platform = MockGoogleMapsFlutterPlatform(); + late TestGoogleMapsFlutterPlatform platform; setUp(() { // Use a mock platform so we never need to hit the MethodChannel code. + platform = TestGoogleMapsFlutterPlatform(); GoogleMapsFlutterPlatform.instance = platform; - resetMockitoState(); - _setupMock(platform); }); testWidgets('_webOnlyMapCreationId increments with each GoogleMap widget', ( @@ -49,18 +49,9 @@ void main() { ); // Verify that each one was created with a different _webOnlyMapCreationId. - verifyInOrder([ - platform.buildView( - argThat(containsPair('_webOnlyMapCreationId', 0)), - any, - any, - ), - platform.buildView( - argThat(containsPair('_webOnlyMapCreationId', 1)), - any, - any, - ), - ]); + expect(platform.createdIds.length, 2); + expect(platform.createdIds[0], 0); + expect(platform.createdIds[1], 1); }); testWidgets('Calls platform.dispose when GoogleMap is disposed of', ( @@ -75,47 +66,220 @@ void main() { // Now dispose of the map... await tester.pumpWidget(Container()); - verify(platform.dispose(mapId: anyNamed('mapId'))); + expect(platform.disposed, true); }); } -// Some test setup classes below... +// A dummy implementation of the platform interface for tests. +class TestGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { + TestGoogleMapsFlutterPlatform(); + + // The IDs passed to each call to buildView, in call order. + List createdIds = []; + + // Whether `dispose` has been called. + bool disposed = false; + + // Stream controller to inject events for testing. + final StreamController mapEventStreamController = + StreamController.broadcast(); + + @override + Future init(int mapId) async {} + + @override + Future updateMapOptions( + Map optionsUpdate, { + required int mapId, + }) async {} + + @override + Future updateMarkers( + MarkerUpdates markerUpdates, { + required int mapId, + }) async {} + + @override + Future updatePolygons( + PolygonUpdates polygonUpdates, { + required int mapId, + }) async {} + + @override + Future updatePolylines( + PolylineUpdates polylineUpdates, { + required int mapId, + }) async {} + + @override + Future updateCircles( + CircleUpdates circleUpdates, { + required int mapId, + }) async {} + + @override + Future updateTileOverlays({ + required Set newTileOverlays, + required int mapId, + }) async {} + + @override + Future clearTileCache( + TileOverlayId tileOverlayId, { + required int mapId, + }) async {} + + @override + Future animateCamera( + CameraUpdate cameraUpdate, { + required int mapId, + }) async {} + + @override + Future moveCamera( + CameraUpdate cameraUpdate, { + required int mapId, + }) async {} + + @override + Future setMapStyle( + String? mapStyle, { + required int mapId, + }) async {} + + @override + Future getVisibleRegion({ + required int mapId, + }) async { + return LatLngBounds(southwest: LatLng(0, 0), northeast: LatLng(0, 0)); + } -class _MockStream extends Mock implements Stream {} + @override + Future getScreenCoordinate( + LatLng latLng, { + required int mapId, + }) async { + return ScreenCoordinate(x: 0, y: 0); + } -typedef _CreationCallback = void Function(int); + @override + Future getLatLng( + ScreenCoordinate screenCoordinate, { + required int mapId, + }) async { + return LatLng(0, 0); + } -// Installs test mocks on the platform -void _setupMock(MockGoogleMapsFlutterPlatform platform) { - // Used to create the view of the map... - when(platform.buildView(any, any, any)).thenAnswer((realInvocation) { - // Call the onPlatformViewCreated callback so the controller gets created. - _CreationCallback onPlatformViewCreatedCb = - realInvocation.positionalArguments[2]; - onPlatformViewCreatedCb.call(0); + @override + Future showMarkerInfoWindow( + MarkerId markerId, { + required int mapId, + }) async {} + + @override + Future hideMarkerInfoWindow( + MarkerId markerId, { + required int mapId, + }) async {} + + @override + Future isMarkerInfoWindowShown( + MarkerId markerId, { + required int mapId, + }) async { + return false; + } + + @override + Future getZoomLevel({ + required int mapId, + }) async { + return 0.0; + } + + @override + Future takeSnapshot({ + required int mapId, + }) async { + return null; + } + + @override + Stream onCameraMoveStarted({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onCameraMove({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onCameraIdle({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onMarkerTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onInfoWindowTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onMarkerDragEnd({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onPolylineTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onPolygonTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onCircleTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onTap({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onLongPress({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + void dispose({required int mapId}) { + disposed = true; + } + + @override + Widget buildView( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required CameraPosition initialCameraPosition, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set circles = const {}, + Set tileOverlays = const {}, + Set>? gestureRecognizers = + const >{}, + Map mapOptions = const {}, + }) { + onPlatformViewCreated(0); + createdIds.add(creationId); return Container(); - }); - // Used to create the Controller - when(platform.onCameraIdle(mapId: anyNamed('mapId'))) - .thenAnswer((_) => _MockStream()); - when(platform.onCameraMove(mapId: anyNamed('mapId'))) - .thenAnswer((_) => _MockStream()); - when(platform.onCameraMoveStarted(mapId: anyNamed('mapId'))) - .thenAnswer((_) => _MockStream()); - when(platform.onCircleTap(mapId: anyNamed('mapId'))) - .thenAnswer((_) => _MockStream()); - when(platform.onInfoWindowTap(mapId: anyNamed('mapId'))) - .thenAnswer((_) => _MockStream()); - when(platform.onLongPress(mapId: anyNamed('mapId'))) - .thenAnswer((_) => _MockStream()); - when(platform.onMarkerDragEnd(mapId: anyNamed('mapId'))) - .thenAnswer((_) => _MockStream()); - when(platform.onMarkerTap(mapId: anyNamed('mapId'))) - .thenAnswer((_) => _MockStream()); - when(platform.onPolygonTap(mapId: anyNamed('mapId'))) - .thenAnswer((_) => _MockStream()); - when(platform.onPolylineTap(mapId: anyNamed('mapId'))) - .thenAnswer((_) => _MockStream()); - when(platform.onTap(mapId: anyNamed('mapId'))) - .thenAnswer((_) => _MockStream()); + } } diff --git a/packages/google_maps_flutter/google_maps_flutter/test/marker_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/marker_updates_test.dart index 620e1ef4bfea..e295393fe15a 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -9,20 +9,6 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'fake_maps_controllers.dart'; -Set _toSet({Marker m1, Marker m2, Marker m3}) { - final Set res = Set.identity(); - if (m1 != null) { - res.add(m1); - } - if (m2 != null) { - res.add(m2); - } - if (m3 != null) { - res.add(m3); - } - return res; -} - Widget _mapWithMarkers(Set markers) { return Directionality( textDirection: TextDirection.ltr, @@ -50,10 +36,10 @@ void main() { testWidgets('Initializing a marker', (WidgetTester tester) async { final Marker m1 = Marker(markerId: MarkerId("marker_1")); - await tester.pumpWidget(_mapWithMarkers(_toSet(m1: m1))); + await tester.pumpWidget(_mapWithMarkers({m1})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.markersToAdd.length, 1); final Marker initializedMarker = platformGoogleMap.markersToAdd.first; @@ -66,11 +52,11 @@ void main() { final Marker m1 = Marker(markerId: MarkerId("marker_1")); final Marker m2 = Marker(markerId: MarkerId("marker_2")); - await tester.pumpWidget(_mapWithMarkers(_toSet(m1: m1))); - await tester.pumpWidget(_mapWithMarkers(_toSet(m1: m1, m2: m2))); + await tester.pumpWidget(_mapWithMarkers({m1})); + await tester.pumpWidget(_mapWithMarkers({m1, m2})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.markersToAdd.length, 1); final Marker addedMarker = platformGoogleMap.markersToAdd.first; @@ -84,11 +70,11 @@ void main() { testWidgets("Removing a marker", (WidgetTester tester) async { final Marker m1 = Marker(markerId: MarkerId("marker_1")); - await tester.pumpWidget(_mapWithMarkers(_toSet(m1: m1))); - await tester.pumpWidget(_mapWithMarkers(null)); + await tester.pumpWidget(_mapWithMarkers({m1})); + await tester.pumpWidget(_mapWithMarkers({})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.markerIdsToRemove.length, 1); expect(platformGoogleMap.markerIdsToRemove.first, equals(m1.markerId)); @@ -100,11 +86,11 @@ void main() { final Marker m1 = Marker(markerId: MarkerId("marker_1")); final Marker m2 = Marker(markerId: MarkerId("marker_1"), alpha: 0.5); - await tester.pumpWidget(_mapWithMarkers(_toSet(m1: m1))); - await tester.pumpWidget(_mapWithMarkers(_toSet(m1: m2))); + await tester.pumpWidget(_mapWithMarkers({m1})); + await tester.pumpWidget(_mapWithMarkers({m2})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.markersToChange.length, 1); expect(platformGoogleMap.markersToChange.first, equals(m2)); @@ -119,11 +105,11 @@ void main() { infoWindow: const InfoWindow(snippet: 'changed'), ); - await tester.pumpWidget(_mapWithMarkers(_toSet(m1: m1))); - await tester.pumpWidget(_mapWithMarkers(_toSet(m1: m2))); + await tester.pumpWidget(_mapWithMarkers({m1})); + await tester.pumpWidget(_mapWithMarkers({m2})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.markersToChange.length, 1); final Marker update = platformGoogleMap.markersToChange.first; @@ -134,16 +120,16 @@ void main() { testWidgets("Multi Update", (WidgetTester tester) async { Marker m1 = Marker(markerId: MarkerId("marker_1")); Marker m2 = Marker(markerId: MarkerId("marker_2")); - final Set prev = _toSet(m1: m1, m2: m2); + final Set prev = {m1, m2}; m1 = Marker(markerId: MarkerId("marker_1"), visible: false); m2 = Marker(markerId: MarkerId("marker_2"), draggable: true); - final Set cur = _toSet(m1: m1, m2: m2); + final Set cur = {m1, m2}; await tester.pumpWidget(_mapWithMarkers(prev)); await tester.pumpWidget(_mapWithMarkers(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.markersToChange, cur); expect(platformGoogleMap.markerIdsToRemove.isEmpty, true); @@ -153,18 +139,18 @@ void main() { testWidgets("Multi Update", (WidgetTester tester) async { Marker m2 = Marker(markerId: MarkerId("marker_2")); final Marker m3 = Marker(markerId: MarkerId("marker_3")); - final Set prev = _toSet(m2: m2, m3: m3); + final Set prev = {m2, m3}; // m1 is added, m2 is updated, m3 is removed. final Marker m1 = Marker(markerId: MarkerId("marker_1")); m2 = Marker(markerId: MarkerId("marker_2"), draggable: true); - final Set cur = _toSet(m1: m1, m2: m2); + final Set cur = {m1, m2}; await tester.pumpWidget(_mapWithMarkers(prev)); await tester.pumpWidget(_mapWithMarkers(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.markersToChange.length, 1); expect(platformGoogleMap.markersToAdd.length, 1); @@ -179,35 +165,35 @@ void main() { final Marker m1 = Marker(markerId: MarkerId("marker_1")); final Marker m2 = Marker(markerId: MarkerId("marker_2")); Marker m3 = Marker(markerId: MarkerId("marker_3")); - final Set prev = _toSet(m1: m1, m2: m2, m3: m3); + final Set prev = {m1, m2, m3}; m3 = Marker(markerId: MarkerId("marker_3"), draggable: true); - final Set cur = _toSet(m1: m1, m2: m2, m3: m3); + final Set cur = {m1, m2, m3}; await tester.pumpWidget(_mapWithMarkers(prev)); await tester.pumpWidget(_mapWithMarkers(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.markersToChange, _toSet(m3: m3)); + expect(platformGoogleMap.markersToChange, {m3}); expect(platformGoogleMap.markerIdsToRemove.isEmpty, true); expect(platformGoogleMap.markersToAdd.isEmpty, true); }); testWidgets("Update non platform related attr", (WidgetTester tester) async { Marker m1 = Marker(markerId: MarkerId("marker_1")); - final Set prev = _toSet(m1: m1); + final Set prev = {m1}; m1 = Marker( markerId: MarkerId("marker_1"), onTap: () => print("hello"), onDragEnd: (LatLng latLng) => print(latLng)); - final Set cur = _toSet(m1: m1); + final Set cur = {m1}; await tester.pumpWidget(_mapWithMarkers(prev)); await tester.pumpWidget(_mapWithMarkers(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.markersToChange.isEmpty, true); expect(platformGoogleMap.markerIdsToRemove.isEmpty, true); diff --git a/packages/google_maps_flutter/google_maps_flutter/test/polygon_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/polygon_updates_test.dart index 185c996113af..79c63c1c5459 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -9,20 +9,6 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'fake_maps_controllers.dart'; -Set _toSet({Polygon p1, Polygon p2, Polygon p3}) { - final Set res = Set.identity(); - if (p1 != null) { - res.add(p1); - } - if (p2 != null) { - res.add(p2); - } - if (p3 != null) { - res.add(p3); - } - return res; -} - Widget _mapWithPolygons(Set polygons) { return Directionality( textDirection: TextDirection.ltr, @@ -33,6 +19,29 @@ Widget _mapWithPolygons(Set polygons) { ); } +List _rectPoints({ + required double size, + LatLng center = const LatLng(0, 0), +}) { + final halfSize = size / 2; + + return [ + LatLng(center.latitude + halfSize, center.longitude + halfSize), + LatLng(center.latitude - halfSize, center.longitude + halfSize), + LatLng(center.latitude - halfSize, center.longitude - halfSize), + LatLng(center.latitude + halfSize, center.longitude - halfSize), + ]; +} + +Polygon _polygonWithPointsAndHole(PolygonId polygonId) { + _rectPoints(size: 1); + return Polygon( + polygonId: polygonId, + points: _rectPoints(size: 1), + holes: [_rectPoints(size: 0.5)], + ); +} + void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -50,10 +59,10 @@ void main() { testWidgets('Initializing a polygon', (WidgetTester tester) async { final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); - await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1))); + await tester.pumpWidget(_mapWithPolygons({p1})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polygonsToAdd.length, 1); final Polygon initializedPolygon = platformGoogleMap.polygonsToAdd.first; @@ -66,11 +75,11 @@ void main() { final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); final Polygon p2 = Polygon(polygonId: PolygonId("polygon_2")); - await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1))); - await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1, p2: p2))); + await tester.pumpWidget(_mapWithPolygons({p1})); + await tester.pumpWidget(_mapWithPolygons({p1, p2})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polygonsToAdd.length, 1); final Polygon addedPolygon = platformGoogleMap.polygonsToAdd.first; @@ -84,11 +93,11 @@ void main() { testWidgets("Removing a polygon", (WidgetTester tester) async { final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); - await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1))); - await tester.pumpWidget(_mapWithPolygons(null)); + await tester.pumpWidget(_mapWithPolygons({p1})); + await tester.pumpWidget(_mapWithPolygons({})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polygonIdsToRemove.length, 1); expect(platformGoogleMap.polygonIdsToRemove.first, equals(p1.polygonId)); @@ -101,11 +110,11 @@ void main() { final Polygon p2 = Polygon(polygonId: PolygonId("polygon_1"), geodesic: true); - await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1))); - await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p2))); + await tester.pumpWidget(_mapWithPolygons({p1})); + await tester.pumpWidget(_mapWithPolygons({p2})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polygonsToChange.length, 1); expect(platformGoogleMap.polygonsToChange.first, equals(p2)); @@ -113,35 +122,18 @@ void main() { expect(platformGoogleMap.polygonsToAdd.isEmpty, true); }); - testWidgets("Updating a polygon", (WidgetTester tester) async { - final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); - final Polygon p2 = - Polygon(polygonId: PolygonId("polygon_1"), geodesic: true); - - await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1))); - await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p2))); - - final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; - expect(platformGoogleMap.polygonsToChange.length, 1); - - final Polygon update = platformGoogleMap.polygonsToChange.first; - expect(update, equals(p2)); - expect(update.geodesic, true); - }); - testWidgets("Mutate a polygon", (WidgetTester tester) async { final Polygon p1 = Polygon( polygonId: PolygonId("polygon_1"), points: [const LatLng(0.0, 0.0)], ); - await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1))); + await tester.pumpWidget(_mapWithPolygons({p1})); p1.points.add(const LatLng(1.0, 1.0)); - await tester.pumpWidget(_mapWithPolygons(_toSet(p1: p1))); + await tester.pumpWidget(_mapWithPolygons({p1})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polygonsToChange.length, 1); expect(platformGoogleMap.polygonsToChange.first, equals(p1)); @@ -152,16 +144,16 @@ void main() { testWidgets("Multi Update", (WidgetTester tester) async { Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); Polygon p2 = Polygon(polygonId: PolygonId("polygon_2")); - final Set prev = _toSet(p1: p1, p2: p2); + final Set prev = {p1, p2}; p1 = Polygon(polygonId: PolygonId("polygon_1"), visible: false); p2 = Polygon(polygonId: PolygonId("polygon_2"), geodesic: true); - final Set cur = _toSet(p1: p1, p2: p2); + final Set cur = {p1, p2}; await tester.pumpWidget(_mapWithPolygons(prev)); await tester.pumpWidget(_mapWithPolygons(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polygonsToChange, cur); expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); @@ -171,18 +163,18 @@ void main() { testWidgets("Multi Update", (WidgetTester tester) async { Polygon p2 = Polygon(polygonId: PolygonId("polygon_2")); final Polygon p3 = Polygon(polygonId: PolygonId("polygon_3")); - final Set prev = _toSet(p2: p2, p3: p3); + final Set prev = {p2, p3}; // p1 is added, p2 is updated, p3 is removed. final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); p2 = Polygon(polygonId: PolygonId("polygon_2"), geodesic: true); - final Set cur = _toSet(p1: p1, p2: p2); + final Set cur = {p1, p2}; await tester.pumpWidget(_mapWithPolygons(prev)); await tester.pumpWidget(_mapWithPolygons(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polygonsToChange.length, 1); expect(platformGoogleMap.polygonsToAdd.length, 1); @@ -197,34 +189,215 @@ void main() { final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); final Polygon p2 = Polygon(polygonId: PolygonId("polygon_2")); Polygon p3 = Polygon(polygonId: PolygonId("polygon_3")); - final Set prev = _toSet(p1: p1, p2: p2, p3: p3); + final Set prev = {p1, p2, p3}; p3 = Polygon(polygonId: PolygonId("polygon_3"), geodesic: true); - final Set cur = _toSet(p1: p1, p2: p2, p3: p3); + final Set cur = {p1, p2, p3}; await tester.pumpWidget(_mapWithPolygons(prev)); await tester.pumpWidget(_mapWithPolygons(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polygonsToChange, _toSet(p3: p3)); + expect(platformGoogleMap.polygonsToChange, {p3}); expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); expect(platformGoogleMap.polygonsToAdd.isEmpty, true); }); testWidgets("Update non platform related attr", (WidgetTester tester) async { Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); - final Set prev = _toSet(p1: p1); + final Set prev = {p1}; p1 = Polygon(polygonId: PolygonId("polygon_1"), onTap: () => print(2 + 2)); - final Set cur = _toSet(p1: p1); + final Set cur = {p1}; await tester.pumpWidget(_mapWithPolygons(prev)); await tester.pumpWidget(_mapWithPolygons(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; + + expect(platformGoogleMap.polygonsToChange.isEmpty, true); + expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); + expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + }); + + testWidgets('Initializing a polygon with points and hole', + (WidgetTester tester) async { + final Polygon p1 = _polygonWithPointsAndHole(PolygonId("polygon_1")); + await tester.pumpWidget(_mapWithPolygons({p1})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.polygonsToAdd.length, 1); + + final Polygon initializedPolygon = platformGoogleMap.polygonsToAdd.first; + expect(initializedPolygon, equals(p1)); + expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); + expect(platformGoogleMap.polygonsToChange.isEmpty, true); + }); + + testWidgets("Adding a polygon with points and hole", + (WidgetTester tester) async { + final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); + final Polygon p2 = _polygonWithPointsAndHole(PolygonId("polygon_2")); + + await tester.pumpWidget(_mapWithPolygons({p1})); + await tester.pumpWidget(_mapWithPolygons({p1, p2})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.polygonsToAdd.length, 1); + + final Polygon addedPolygon = platformGoogleMap.polygonsToAdd.first; + expect(addedPolygon, equals(p2)); + + expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); expect(platformGoogleMap.polygonsToChange.isEmpty, true); + }); + + testWidgets("Removing a polygon with points and hole", + (WidgetTester tester) async { + final Polygon p1 = _polygonWithPointsAndHole(PolygonId("polygon_1")); + + await tester.pumpWidget(_mapWithPolygons({p1})); + await tester.pumpWidget(_mapWithPolygons({})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.polygonIdsToRemove.length, 1); + expect(platformGoogleMap.polygonIdsToRemove.first, equals(p1.polygonId)); + + expect(platformGoogleMap.polygonsToChange.isEmpty, true); + expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + }); + + testWidgets("Updating a polygon by adding points and hole", + (WidgetTester tester) async { + final Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); + final Polygon p2 = _polygonWithPointsAndHole(PolygonId("polygon_1")); + + await tester.pumpWidget(_mapWithPolygons({p1})); + await tester.pumpWidget(_mapWithPolygons({p2})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.polygonsToChange.length, 1); + expect(platformGoogleMap.polygonsToChange.first, equals(p2)); + + expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); + expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + }); + + testWidgets("Mutate a polygon with points and holes", + (WidgetTester tester) async { + final Polygon p1 = Polygon( + polygonId: PolygonId("polygon_1"), + points: _rectPoints(size: 1), + holes: [_rectPoints(size: 0.5)], + ); + await tester.pumpWidget(_mapWithPolygons({p1})); + + p1.points + ..clear() + ..addAll(_rectPoints(size: 2)); + p1.holes + ..clear() + ..addAll([_rectPoints(size: 1)]); + await tester.pumpWidget(_mapWithPolygons({p1})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.polygonsToChange.length, 1); + expect(platformGoogleMap.polygonsToChange.first, equals(p1)); + + expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); + expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + }); + + testWidgets("Multi Update polygons with points and hole", + (WidgetTester tester) async { + Polygon p1 = Polygon(polygonId: PolygonId("polygon_1")); + Polygon p2 = Polygon( + polygonId: PolygonId("polygon_2"), + points: _rectPoints(size: 2), + holes: [_rectPoints(size: 1)], + ); + final Set prev = {p1, p2}; + p1 = Polygon(polygonId: PolygonId("polygon_1"), visible: false); + p2 = p2.copyWith( + pointsParam: _rectPoints(size: 5), + holesParam: [_rectPoints(size: 2)], + ); + final Set cur = {p1, p2}; + + await tester.pumpWidget(_mapWithPolygons(prev)); + await tester.pumpWidget(_mapWithPolygons(cur)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + + expect(platformGoogleMap.polygonsToChange, cur); + expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); + expect(platformGoogleMap.polygonsToAdd.isEmpty, true); + }); + + testWidgets("Multi Update polygons with points and hole", + (WidgetTester tester) async { + Polygon p2 = Polygon( + polygonId: PolygonId("polygon_2"), + points: _rectPoints(size: 2), + holes: [_rectPoints(size: 1)], + ); + final Polygon p3 = Polygon(polygonId: PolygonId("polygon_3")); + final Set prev = {p2, p3}; + + // p1 is added, p2 is updated, p3 is removed. + final Polygon p1 = _polygonWithPointsAndHole(PolygonId("polygon_1")); + p2 = p2.copyWith( + pointsParam: _rectPoints(size: 5), + holesParam: [_rectPoints(size: 3)], + ); + final Set cur = {p1, p2}; + + await tester.pumpWidget(_mapWithPolygons(prev)); + await tester.pumpWidget(_mapWithPolygons(cur)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + + expect(platformGoogleMap.polygonsToChange.length, 1); + expect(platformGoogleMap.polygonsToAdd.length, 1); + expect(platformGoogleMap.polygonIdsToRemove.length, 1); + + expect(platformGoogleMap.polygonsToChange.first, equals(p2)); + expect(platformGoogleMap.polygonsToAdd.first, equals(p1)); + expect(platformGoogleMap.polygonIdsToRemove.first, equals(p3.polygonId)); + }); + + testWidgets("Partial Update polygons with points and hole", + (WidgetTester tester) async { + final Polygon p1 = _polygonWithPointsAndHole(PolygonId("polygon_1")); + final Polygon p2 = Polygon(polygonId: PolygonId("polygon_2")); + Polygon p3 = Polygon( + polygonId: PolygonId("polygon_3"), + points: _rectPoints(size: 2), + holes: [_rectPoints(size: 1)], + ); + final Set prev = {p1, p2, p3}; + p3 = p3.copyWith( + pointsParam: _rectPoints(size: 5), + holesParam: [_rectPoints(size: 3)], + ); + final Set cur = {p1, p2, p3}; + + await tester.pumpWidget(_mapWithPolygons(prev)); + await tester.pumpWidget(_mapWithPolygons(cur)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + + expect(platformGoogleMap.polygonsToChange, {p3}); expect(platformGoogleMap.polygonIdsToRemove.isEmpty, true); expect(platformGoogleMap.polygonsToAdd.isEmpty, true); }); diff --git a/packages/google_maps_flutter/google_maps_flutter/test/polyline_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/polyline_updates_test.dart index 269e8f1313f5..01eb2e2ce724 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -9,20 +9,6 @@ import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'fake_maps_controllers.dart'; -Set _toSet({Polyline p1, Polyline p2, Polyline p3}) { - final Set res = Set.identity(); - if (p1 != null) { - res.add(p1); - } - if (p2 != null) { - res.add(p2); - } - if (p3 != null) { - res.add(p3); - } - return res; -} - Widget _mapWithPolylines(Set polylines) { return Directionality( textDirection: TextDirection.ltr, @@ -50,10 +36,10 @@ void main() { testWidgets('Initializing a polyline', (WidgetTester tester) async { final Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); - await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p1))); + await tester.pumpWidget(_mapWithPolylines({p1})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polylinesToAdd.length, 1); final Polyline initializedPolyline = platformGoogleMap.polylinesToAdd.first; @@ -66,11 +52,11 @@ void main() { final Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); final Polyline p2 = Polyline(polylineId: PolylineId("polyline_2")); - await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p1))); - await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p1, p2: p2))); + await tester.pumpWidget(_mapWithPolylines({p1})); + await tester.pumpWidget(_mapWithPolylines({p1, p2})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polylinesToAdd.length, 1); final Polyline addedPolyline = platformGoogleMap.polylinesToAdd.first; @@ -84,11 +70,11 @@ void main() { testWidgets("Removing a polyline", (WidgetTester tester) async { final Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); - await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p1))); - await tester.pumpWidget(_mapWithPolylines(null)); + await tester.pumpWidget(_mapWithPolylines({p1})); + await tester.pumpWidget(_mapWithPolylines({})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polylineIdsToRemove.length, 1); expect(platformGoogleMap.polylineIdsToRemove.first, equals(p1.polylineId)); @@ -101,11 +87,11 @@ void main() { final Polyline p2 = Polyline(polylineId: PolylineId("polyline_1"), geodesic: true); - await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p1))); - await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p2))); + await tester.pumpWidget(_mapWithPolylines({p1})); + await tester.pumpWidget(_mapWithPolylines({p2})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polylinesToChange.length, 1); expect(platformGoogleMap.polylinesToChange.first, equals(p2)); @@ -118,11 +104,11 @@ void main() { final Polyline p2 = Polyline(polylineId: PolylineId("polyline_1"), geodesic: true); - await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p1))); - await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p2))); + await tester.pumpWidget(_mapWithPolylines({p1})); + await tester.pumpWidget(_mapWithPolylines({p2})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polylinesToChange.length, 1); final Polyline update = platformGoogleMap.polylinesToChange.first; @@ -135,13 +121,13 @@ void main() { polylineId: PolylineId("polyline_1"), points: [const LatLng(0.0, 0.0)], ); - await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p1))); + await tester.pumpWidget(_mapWithPolylines({p1})); p1.points.add(const LatLng(1.0, 1.0)); - await tester.pumpWidget(_mapWithPolylines(_toSet(p1: p1))); + await tester.pumpWidget(_mapWithPolylines({p1})); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polylinesToChange.length, 1); expect(platformGoogleMap.polylinesToChange.first, equals(p1)); @@ -152,16 +138,16 @@ void main() { testWidgets("Multi Update", (WidgetTester tester) async { Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); Polyline p2 = Polyline(polylineId: PolylineId("polyline_2")); - final Set prev = _toSet(p1: p1, p2: p2); + final Set prev = {p1, p2}; p1 = Polyline(polylineId: PolylineId("polyline_1"), visible: false); p2 = Polyline(polylineId: PolylineId("polyline_2"), geodesic: true); - final Set cur = _toSet(p1: p1, p2: p2); + final Set cur = {p1, p2}; await tester.pumpWidget(_mapWithPolylines(prev)); await tester.pumpWidget(_mapWithPolylines(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polylinesToChange, cur); expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); @@ -171,18 +157,18 @@ void main() { testWidgets("Multi Update", (WidgetTester tester) async { Polyline p2 = Polyline(polylineId: PolylineId("polyline_2")); final Polyline p3 = Polyline(polylineId: PolylineId("polyline_3")); - final Set prev = _toSet(p2: p2, p3: p3); + final Set prev = {p2, p3}; // p1 is added, p2 is updated, p3 is removed. final Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); p2 = Polyline(polylineId: PolylineId("polyline_2"), geodesic: true); - final Set cur = _toSet(p1: p1, p2: p2); + final Set cur = {p1, p2}; await tester.pumpWidget(_mapWithPolylines(prev)); await tester.pumpWidget(_mapWithPolylines(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polylinesToChange.length, 1); expect(platformGoogleMap.polylinesToAdd.length, 1); @@ -197,37 +183,33 @@ void main() { final Polyline p1 = Polyline(polylineId: PolylineId("polyline_1")); final Polyline p2 = Polyline(polylineId: PolylineId("polyline_2")); Polyline p3 = Polyline(polylineId: PolylineId("polyline_3")); - final Set prev = _toSet(p1: p1, p2: p2, p3: p3); + final Set prev = {p1, p2, p3}; p3 = Polyline(polylineId: PolylineId("polyline_3"), geodesic: true); - final Set cur = _toSet(p1: p1, p2: p2, p3: p3); + final Set cur = {p1, p2, p3}; await tester.pumpWidget(_mapWithPolylines(prev)); await tester.pumpWidget(_mapWithPolylines(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; - expect(platformGoogleMap.polylinesToChange, _toSet(p3: p3)); + expect(platformGoogleMap.polylinesToChange, {p3}); expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); expect(platformGoogleMap.polylinesToAdd.isEmpty, true); }); testWidgets("Update non platform related attr", (WidgetTester tester) async { Polyline p1 = Polyline(polylineId: PolylineId("polyline_1"), onTap: null); - final Set prev = _toSet( - p1: p1, - ); + final Set prev = {p1}; p1 = Polyline( polylineId: PolylineId("polyline_1"), onTap: () => print(2 + 2)); - final Set cur = _toSet( - p1: p1, - ); + final Set cur = {p1}; await tester.pumpWidget(_mapWithPolylines(prev)); await tester.pumpWidget(_mapWithPolylines(cur)); final FakePlatformGoogleMap platformGoogleMap = - fakePlatformViewsController.lastCreatedView; + fakePlatformViewsController.lastCreatedView!; expect(platformGoogleMap.polylinesToChange.isEmpty, true); expect(platformGoogleMap.polylineIdsToRemove.isEmpty, true); diff --git a/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_test.dart new file mode 100644 index 000000000000..35732da29726 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/test/tile_overlay_updates_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/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +import 'fake_maps_controllers.dart'; + +Widget _mapWithTileOverlays(Set tileOverlays) { + return Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + tileOverlays: tileOverlays, + ), + ); +} + +void main() { + final FakePlatformViewsController fakePlatformViewsController = + FakePlatformViewsController(); + + setUpAll(() { + SystemChannels.platform_views.setMockMethodCallHandler( + fakePlatformViewsController.fakePlatformViewsMethodHandler); + }); + + setUp(() { + fakePlatformViewsController.reset(); + }); + + testWidgets('Initializing a tile overlay', (WidgetTester tester) async { + final TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + await tester.pumpWidget(_mapWithTileOverlays({t1})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.tileOverlaysToAdd.length, 1); + + final TileOverlay initializedTileOverlay = + platformGoogleMap.tileOverlaysToAdd.first; + expect(initializedTileOverlay, equals(t1)); + expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); + expect(platformGoogleMap.tileOverlaysToChange.isEmpty, true); + }); + + testWidgets("Adding a tile overlay", (WidgetTester tester) async { + final TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + final TileOverlay t2 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2")); + + await tester.pumpWidget(_mapWithTileOverlays({t1})); + await tester.pumpWidget(_mapWithTileOverlays({t1, t2})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.tileOverlaysToAdd.length, 1); + + final TileOverlay addedTileOverlay = + platformGoogleMap.tileOverlaysToAdd.first; + expect(addedTileOverlay, equals(t2)); + expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); + + expect(platformGoogleMap.tileOverlaysToChange.isEmpty, true); + }); + + testWidgets("Removing a tile overlay", (WidgetTester tester) async { + final TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + + await tester.pumpWidget(_mapWithTileOverlays({t1})); + await tester.pumpWidget(_mapWithTileOverlays({})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.tileOverlayIdsToRemove.length, 1); + expect(platformGoogleMap.tileOverlayIdsToRemove.first, + equals(t1.tileOverlayId)); + + expect(platformGoogleMap.tileOverlaysToChange.isEmpty, true); + expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true); + }); + + testWidgets("Updating a tile overlay", (WidgetTester tester) async { + final TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + final TileOverlay t2 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1"), zIndex: 10); + + await tester.pumpWidget(_mapWithTileOverlays({t1})); + await tester.pumpWidget(_mapWithTileOverlays({t2})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.tileOverlaysToChange.length, 1); + expect(platformGoogleMap.tileOverlaysToChange.first, equals(t2)); + + expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); + expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true); + }); + + testWidgets("Updating a tile overlay", (WidgetTester tester) async { + final TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + final TileOverlay t2 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1"), zIndex: 10); + + await tester.pumpWidget(_mapWithTileOverlays({t1})); + await tester.pumpWidget(_mapWithTileOverlays({t2})); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + expect(platformGoogleMap.tileOverlaysToChange.length, 1); + + final TileOverlay update = platformGoogleMap.tileOverlaysToChange.first; + expect(update, equals(t2)); + expect(update.zIndex, 10); + }); + + testWidgets("Multi Update", (WidgetTester tester) async { + TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + TileOverlay t2 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2")); + final Set prev = {t1, t2}; + t1 = TileOverlay( + tileOverlayId: TileOverlayId("tile_overlay_1"), visible: false); + t2 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2"), zIndex: 10); + final Set cur = {t1, t2}; + + await tester.pumpWidget(_mapWithTileOverlays(prev)); + await tester.pumpWidget(_mapWithTileOverlays(cur)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + + expect(platformGoogleMap.tileOverlaysToChange, cur); + expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); + expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true); + }); + + testWidgets("Multi Update", (WidgetTester tester) async { + TileOverlay t2 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2")); + final TileOverlay t3 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_3")); + final Set prev = {t2, t3}; + + // t1 is added, t2 is updated, t3 is removed. + final TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + t2 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2"), zIndex: 10); + final Set cur = {t1, t2}; + + await tester.pumpWidget(_mapWithTileOverlays(prev)); + await tester.pumpWidget(_mapWithTileOverlays(cur)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + + expect(platformGoogleMap.tileOverlaysToChange.length, 1); + expect(platformGoogleMap.tileOverlaysToAdd.length, 1); + expect(platformGoogleMap.tileOverlayIdsToRemove.length, 1); + + expect(platformGoogleMap.tileOverlaysToChange.first, equals(t2)); + expect(platformGoogleMap.tileOverlaysToAdd.first, equals(t1)); + expect(platformGoogleMap.tileOverlayIdsToRemove.first, + equals(t3.tileOverlayId)); + }); + + testWidgets("Partial Update", (WidgetTester tester) async { + final TileOverlay t1 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_1")); + final TileOverlay t2 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_2")); + TileOverlay t3 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_3")); + final Set prev = {t1, t2, t3}; + t3 = + TileOverlay(tileOverlayId: TileOverlayId("tile_overlay_3"), zIndex: 10); + final Set cur = {t1, t2, t3}; + + await tester.pumpWidget(_mapWithTileOverlays(prev)); + await tester.pumpWidget(_mapWithTileOverlays(cur)); + + final FakePlatformGoogleMap platformGoogleMap = + fakePlatformViewsController.lastCreatedView!; + + expect(platformGoogleMap.tileOverlaysToChange, {t3}); + expect(platformGoogleMap.tileOverlayIdsToRemove.isEmpty, true); + expect(platformGoogleMap.tileOverlaysToAdd.isEmpty, true); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/AUTHORS b/packages/google_maps_flutter/google_maps_flutter_platform_interface/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_platform_interface/CHANGELOG.md index dc8eddf8b557..b6603d66fa89 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,46 @@ +## 2.0.4 + +* Preserve the `TileProvider` when copying `TileOverlay`, fixing a + regression with tile overlays introduced in the null safety migration. + +## 2.0.3 + +* Fix type issues in `isMarkerInfoWindowShown` and `getZoomLevel` introduced + in the null safety migration. + +## 2.0.2 + +* Mark constructors for CameraUpdate, CircleId, MapsObjectId, MarkerId, PolygonId, PolylineId and TileOverlayId as const + +## 2.0.1 + +* Update platform_plugin_interface version requirement. + +## 2.0.0 + +* Migrated to null-safety. +* BREAKING CHANGE: Removed deprecated APIs. +* BREAKING CHANGE: Many sets in APIs that used to treat null and empty set as + equivalent now require passing an empty set. +* BREAKING CHANGE: toJson now always returns an `Object`; the details of the + object type and structure should be treated as an implementation detail. + +## 1.2.0 + +* Add TileOverlay support. + +## 1.1.0 + +* Add support for holes in Polygons. + +## 1.0.6 + +* Update Flutter SDK constraint. + +## 1.0.5 + +* Temporarily add a `fromJson` constructor to `BitmapDescriptor` so serialized descriptors can be synchronously re-hydrated. This will be removed when a fix for [this issue](https://github.com/flutter/flutter/issues/70330) lands. + ## 1.0.4 * Add a `dispose` method to the interface, so implementations may cleanup resources acquired on `init`. diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/LICENSE b/packages/google_maps_flutter/google_maps_flutter_platform_interface/LICENSE index ad33cf3c3ed1..c6823b81eb84 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/LICENSE +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/LICENSE @@ -1,4 +1,4 @@ -Copyright 2018 The Chromium Authors. All rights reserved. +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: 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 cb28b40470fd..650a839cb676 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/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 c462b4b182e2..be426483193d 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart index 31392354d946..49029cc3d22d 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 @@ -1,18 +1,40 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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/material.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; import 'package:flutter/gestures.dart'; - +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'package:stream_transform/stream_transform.dart'; +import '../types/tile_overlay_updates.dart'; +import '../types/utils/tile_overlay.dart'; + +/// Error thrown when an unknown map ID is provided to a method channel API. +class UnknownMapIDError extends Error { + /// Creates an assertion error with the provided [mapId] and optional + /// [message]. + UnknownMapIDError(this.mapId, [this.message]); + + /// The unknown ID. + final int mapId; + + /// Message describing the assertion error. + final Object? message; + + String toString() { + if (message != null) { + return "Unknown map ID $mapId: ${Error.safeToString(message)}"; + } + return "Unknown map ID $mapId"; + } +} + /// An implementation of [GoogleMapsFlutterPlatform] that uses [MethodChannel] to communicate with the native code. /// /// The `google_maps_flutter` plugin code itself never talks to the native code directly. It delegates @@ -30,27 +52,37 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { /// Accesses the MethodChannel associated to the passed mapId. MethodChannel channel(int mapId) { - return _channels[mapId]; + MethodChannel? channel = _channels[mapId]; + if (channel == null) { + throw UnknownMapIDError(mapId); + } + return channel; } - /// Initializes the platform interface with [id]. - /// - /// This method is called when the plugin is first initialized. - @override - Future init(int mapId) { - MethodChannel channel; - if (!_channels.containsKey(mapId)) { + // Keep a collection of mapId to a map of TileOverlays. + final Map> _tileOverlays = {}; + + /// Returns the channel for [mapId], creating it if it doesn't already exist. + @visibleForTesting + MethodChannel ensureChannelInitialized(int mapId) { + MethodChannel? channel = _channels[mapId]; + if (channel == null) { channel = MethodChannel('plugins.flutter.io/google_maps_$mapId'); channel.setMethodCallHandler( (MethodCall call) => _handleMethodCall(call, mapId)); _channels[mapId] = channel; } + return channel; + } + + @override + Future init(int mapId) { + MethodChannel channel = ensureChannelInitialized(mapId); return channel.invokeMethod('map#waitForMap'); } - /// Dispose of the native resources. @override - void dispose({int mapId}) { + void dispose({required int mapId}) { // Noop! } @@ -67,57 +99,57 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { _mapEventStreamController.stream.where((event) => event.mapId == mapId); @override - Stream onCameraMoveStarted({@required int mapId}) { + Stream onCameraMoveStarted({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onCameraMove({@required int mapId}) { + Stream onCameraMove({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onCameraIdle({@required int mapId}) { + Stream onCameraIdle({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onMarkerTap({@required int mapId}) { + Stream onMarkerTap({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onInfoWindowTap({@required int mapId}) { + Stream onInfoWindowTap({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onMarkerDragEnd({@required int mapId}) { + Stream onMarkerDragEnd({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onPolylineTap({@required int mapId}) { + Stream onPolylineTap({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onPolygonTap({@required int mapId}) { + Stream onPolygonTap({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onCircleTap({@required int mapId}) { + Stream onCircleTap({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onTap({@required int mapId}) { + Stream onTap({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onLongPress({@required int mapId}) { + Stream onLongPress({required int mapId}) { return _events(mapId).whereType(); } @@ -129,7 +161,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { case 'camera#onMove': _mapEventStreamController.add(CameraMoveEvent( mapId, - CameraPosition.fromMap(call.arguments['position']), + CameraPosition.fromMap(call.arguments['position'])!, )); break; case 'camera#onIdle': @@ -144,7 +176,7 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { case 'marker#onDragEnd': _mapEventStreamController.add(MarkerDragEndEvent( mapId, - LatLng.fromJson(call.arguments['position']), + LatLng.fromJson(call.arguments['position'])!, MarkerId(call.arguments['markerId']), )); break; @@ -175,30 +207,40 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { case 'map#onTap': _mapEventStreamController.add(MapTapEvent( mapId, - LatLng.fromJson(call.arguments['position']), + LatLng.fromJson(call.arguments['position'])!, )); break; case 'map#onLongPress': _mapEventStreamController.add(MapLongPressEvent( mapId, - LatLng.fromJson(call.arguments['position']), + LatLng.fromJson(call.arguments['position'])!, )); break; + case 'tileOverlay#getTile': + final Map? tileOverlaysForThisMap = + _tileOverlays[mapId]; + final String tileOverlayId = call.arguments['tileOverlayId']; + final TileOverlay? tileOverlay = + tileOverlaysForThisMap?[TileOverlayId(tileOverlayId)]; + TileProvider? tileProvider = tileOverlay?.tileProvider; + if (tileProvider == null) { + return TileProvider.noTile.toJson(); + } + final Tile tile = await tileProvider.getTile( + call.arguments['x'], + call.arguments['y'], + call.arguments['zoom'], + ); + return tile.toJson(); default: throw MissingPluginException(); } } - /// Updates configuration options of the map user interface. - /// - /// Change listeners are notified once the update has been made on the - /// platform side. - /// - /// The returned [Future] completes after listeners have been notified. @override Future updateMapOptions( Map optionsUpdate, { - @required int mapId, + required int mapId, }) { assert(optionsUpdate != null); return channel(mapId).invokeMethod( @@ -209,16 +251,10 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { ); } - /// Updates marker configuration. - /// - /// Change listeners are notified once the update has been made on the - /// platform side. - /// - /// The returned [Future] completes after listeners have been notified. @override Future updateMarkers( MarkerUpdates markerUpdates, { - @required int mapId, + required int mapId, }) { assert(markerUpdates != null); return channel(mapId).invokeMethod( @@ -227,16 +263,10 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { ); } - /// Updates polygon configuration. - /// - /// Change listeners are notified once the update has been made on the - /// platform side. - /// - /// The returned [Future] completes after listeners have been notified. @override Future updatePolygons( PolygonUpdates polygonUpdates, { - @required int mapId, + required int mapId, }) { assert(polygonUpdates != null); return channel(mapId).invokeMethod( @@ -245,16 +275,10 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { ); } - /// Updates polyline configuration. - /// - /// Change listeners are notified once the update has been made on the - /// platform side. - /// - /// The returned [Future] completes after listeners have been notified. @override Future updatePolylines( PolylineUpdates polylineUpdates, { - @required int mapId, + required int mapId, }) { assert(polylineUpdates != null); return channel(mapId).invokeMethod( @@ -263,16 +287,10 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { ); } - /// Updates circle configuration. - /// - /// Change listeners are notified once the update has been made on the - /// platform side. - /// - /// The returned [Future] completes after listeners have been notified. @override Future updateCircles( CircleUpdates circleUpdates, { - @required int mapId, + required int mapId, }) { assert(circleUpdates != null); return channel(mapId).invokeMethod( @@ -281,185 +299,170 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { ); } - /// Starts an animated change of the map camera position. - /// - /// The returned [Future] completes after the change has been started on the - /// platform side. + @override + Future updateTileOverlays({ + required Set newTileOverlays, + required int mapId, + }) { + final Map? currentTileOverlays = + _tileOverlays[mapId]; + Set previousSet = currentTileOverlays != null + ? currentTileOverlays.values.toSet() + : {}; + final TileOverlayUpdates updates = + TileOverlayUpdates.from(previousSet, newTileOverlays); + _tileOverlays[mapId] = keyTileOverlayId(newTileOverlays); + return channel(mapId).invokeMethod( + 'tileOverlays#update', + updates.toJson(), + ); + } + + @override + Future clearTileCache( + TileOverlayId tileOverlayId, { + required int mapId, + }) { + return channel(mapId) + .invokeMethod('tileOverlays#clearTileCache', { + 'tileOverlayId': tileOverlayId.value, + }); + } + @override Future animateCamera( CameraUpdate cameraUpdate, { - @required int mapId, + required int mapId, }) { - return channel(mapId) - .invokeMethod('camera#animate', { + return channel(mapId).invokeMethod('camera#animate', { 'cameraUpdate': cameraUpdate.toJson(), }); } - /// Changes the map camera position. - /// - /// The returned [Future] completes after the change has been made on the - /// platform side. @override Future moveCamera( CameraUpdate cameraUpdate, { - @required int mapId, + required int mapId, }) { return channel(mapId).invokeMethod('camera#move', { 'cameraUpdate': cameraUpdate.toJson(), }); } - /// Sets the styling of the base map. - /// - /// Set to `null` to clear any previous custom styling. - /// - /// If problems were detected with the [mapStyle], including un-parsable - /// styling JSON, unrecognized feature type, unrecognized element type, or - /// invalid styler keys: [MapStyleException] is thrown and the current - /// style is left unchanged. - /// - /// The style string can be generated using [map style tool](https://mapstyle.withgoogle.com/). - /// Also, refer [iOS](https://developers.google.com/maps/documentation/ios-sdk/style-reference) - /// and [Android](https://developers.google.com/maps/documentation/android-sdk/style-reference) - /// style reference for more information regarding the supported styles. @override Future setMapStyle( - String mapStyle, { - @required int mapId, + String? mapStyle, { + required int mapId, }) async { - final List successAndError = await channel(mapId) - .invokeMethod>('map#setStyle', mapStyle); + final List successAndError = (await channel(mapId) + .invokeMethod>('map#setStyle', mapStyle))!; final bool success = successAndError[0]; if (!success) { throw MapStyleException(successAndError[1]); } } - /// Return the region that is visible in a map. @override Future getVisibleRegion({ - @required int mapId, + required int mapId, }) async { - final Map latLngBounds = await channel(mapId) - .invokeMapMethod('map#getVisibleRegion'); - final LatLng southwest = LatLng.fromJson(latLngBounds['southwest']); - final LatLng northeast = LatLng.fromJson(latLngBounds['northeast']); + final Map latLngBounds = (await channel(mapId) + .invokeMapMethod('map#getVisibleRegion'))!; + final LatLng southwest = LatLng.fromJson(latLngBounds['southwest'])!; + final LatLng northeast = LatLng.fromJson(latLngBounds['northeast'])!; return LatLngBounds(northeast: northeast, southwest: southwest); } - /// Return point [Map] of the [screenCoordinateInJson] in the current map view. - /// - /// A projection is used to translate between on screen location and geographic coordinates. - /// Screen location is in screen pixels (not display pixels) with respect to the top left corner - /// of the map, not necessarily of the whole screen. @override Future getScreenCoordinate( LatLng latLng, { - @required int mapId, + required int mapId, }) async { - final Map point = await channel(mapId) + final Map point = (await channel(mapId) .invokeMapMethod( - 'map#getScreenCoordinate', latLng.toJson()); + 'map#getScreenCoordinate', latLng.toJson()))!; - return ScreenCoordinate(x: point['x'], y: point['y']); + return ScreenCoordinate(x: point['x']!, y: point['y']!); } - /// Returns [LatLng] corresponding to the [ScreenCoordinate] in the current map view. - /// - /// Returned [LatLng] corresponds to a screen location. The screen location is specified in screen - /// pixels (not display pixels) relative to the top left of the map, not top left of the whole screen. @override Future getLatLng( ScreenCoordinate screenCoordinate, { - @required int mapId, + required int mapId, }) async { - final List latLng = await channel(mapId) + final List latLng = (await channel(mapId) .invokeMethod>( - 'map#getLatLng', screenCoordinate.toJson()); + 'map#getLatLng', screenCoordinate.toJson()))!; return LatLng(latLng[0], latLng[1]); } - /// Programmatically show the Info Window for a [Marker]. - /// - /// The `markerId` must match one of the markers on the map. - /// An invalid `markerId` triggers an "Invalid markerId" error. - /// - /// * See also: - /// * [hideMarkerInfoWindow] to hide the Info Window. - /// * [isMarkerInfoWindowShown] to check if the Info Window is showing. @override Future showMarkerInfoWindow( MarkerId markerId, { - @required int mapId, + required int mapId, }) { assert(markerId != null); return channel(mapId).invokeMethod( 'markers#showInfoWindow', {'markerId': markerId.value}); } - /// Programmatically hide the Info Window for a [Marker]. - /// - /// The `markerId` must match one of the markers on the map. - /// An invalid `markerId` triggers an "Invalid markerId" error. - /// - /// * See also: - /// * [showMarkerInfoWindow] to show the Info Window. - /// * [isMarkerInfoWindowShown] to check if the Info Window is showing. @override Future hideMarkerInfoWindow( MarkerId markerId, { - @required int mapId, + required int mapId, }) { assert(markerId != null); return channel(mapId).invokeMethod( 'markers#hideInfoWindow', {'markerId': markerId.value}); } - /// Returns `true` when the [InfoWindow] is showing, `false` otherwise. - /// - /// The `markerId` must match one of the markers on the map. - /// An invalid `markerId` triggers an "Invalid markerId" error. - /// - /// * See also: - /// * [showMarkerInfoWindow] to show the Info Window. - /// * [hideMarkerInfoWindow] to hide the Info Window. @override Future isMarkerInfoWindowShown( MarkerId markerId, { - @required int mapId, - }) { + required int mapId, + }) async { assert(markerId != null); - return channel(mapId).invokeMethod('markers#isInfoWindowShown', - {'markerId': markerId.value}); + return (await channel(mapId).invokeMethod('markers#isInfoWindowShown', + {'markerId': markerId.value}))!; } - /// Returns the current zoom level of the map @override Future getZoomLevel({ - @required int mapId, - }) { - return channel(mapId).invokeMethod('map#getZoomLevel'); + required int mapId, + }) async { + return (await channel(mapId).invokeMethod('map#getZoomLevel'))!; } - /// Returns the image bytes of the map @override - Future takeSnapshot({ - @required int mapId, + Future takeSnapshot({ + required int mapId, }) { return channel(mapId).invokeMethod('map#takeSnapshot'); } - /// This method builds the appropriate platform view where the map - /// can be rendered. - /// The `mapId` is passed as a parameter from the framework on the - /// `onPlatformViewCreated` callback. @override Widget buildView( - Map creationParams, - Set> gestureRecognizers, - PlatformViewCreatedCallback onPlatformViewCreated) { + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required CameraPosition initialCameraPosition, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set circles = const {}, + Set tileOverlays = const {}, + Set>? gestureRecognizers, + Map mapOptions = const {}, + }) { + final Map creationParams = { + 'initialCameraPosition': initialCameraPosition.toMap(), + 'options': mapOptions, + 'markersToAdd': serializeMarkerSet(markers), + 'polygonsToAdd': serializePolygonSet(polygons), + 'polylinesToAdd': serializePolylineSet(polylines), + 'circlesToAdd': serializeCircleSet(circles), + 'tileOverlaysToAdd': serializeTileOverlaySet(tileOverlays), + }; if (defaultTargetPlatform == TargetPlatform.android) { return AndroidView( viewType: 'plugins.flutter.io/google_maps', diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/platform_interface/google_maps_flutter_platform.dart index a4f487740811..425e040ee812 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -58,7 +58,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// The returned [Future] completes after listeners have been notified. Future updateMapOptions( Map optionsUpdate, { - @required int mapId, + required int mapId, }) { throw UnimplementedError('updateMapOptions() has not been implemented.'); } @@ -71,7 +71,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// The returned [Future] completes after listeners have been notified. Future updateMarkers( MarkerUpdates markerUpdates, { - @required int mapId, + required int mapId, }) { throw UnimplementedError('updateMarkers() has not been implemented.'); } @@ -84,7 +84,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// The returned [Future] completes after listeners have been notified. Future updatePolygons( PolygonUpdates polygonUpdates, { - @required int mapId, + required int mapId, }) { throw UnimplementedError('updatePolygons() has not been implemented.'); } @@ -97,7 +97,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// The returned [Future] completes after listeners have been notified. Future updatePolylines( PolylineUpdates polylineUpdates, { - @required int mapId, + required int mapId, }) { throw UnimplementedError('updatePolylines() has not been implemented.'); } @@ -110,18 +110,45 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// The returned [Future] completes after listeners have been notified. Future updateCircles( CircleUpdates circleUpdates, { - @required int mapId, + required int mapId, }) { throw UnimplementedError('updateCircles() has not been implemented.'); } + /// Updates tile overlay configuration. + /// + /// Change listeners are notified once the update has been made on the + /// platform side. + /// + /// The returned [Future] completes after listeners have been notified. + Future updateTileOverlays({ + required Set newTileOverlays, + required int mapId, + }) { + throw UnimplementedError('updateTileOverlays() has not been implemented.'); + } + + /// Clears the tile cache so that all tiles will be requested again from the + /// [TileProvider]. + /// + /// The current tiles from this tile overlay will also be + /// cleared from the map after calling this method. The Google Maps SDK maintains a small + /// in-memory cache of tiles. If you want to cache tiles for longer, you + /// should implement an on-disk cache. + Future clearTileCache( + TileOverlayId tileOverlayId, { + required int mapId, + }) { + throw UnimplementedError('clearTileCache() has not been implemented.'); + } + /// Starts an animated change of the map camera position. /// /// The returned [Future] completes after the change has been started on the /// platform side. Future animateCamera( CameraUpdate cameraUpdate, { - @required int mapId, + required int mapId, }) { throw UnimplementedError('animateCamera() has not been implemented.'); } @@ -132,7 +159,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// platform side. Future moveCamera( CameraUpdate cameraUpdate, { - @required int mapId, + required int mapId, }) { throw UnimplementedError('moveCamera() has not been implemented.'); } @@ -148,15 +175,15 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// /// The style string can be generated using [map style tool](https://mapstyle.withgoogle.com/). Future setMapStyle( - String mapStyle, { - @required int mapId, + String? mapStyle, { + required int mapId, }) { throw UnimplementedError('setMapStyle() has not been implemented.'); } /// Return the region that is visible in a map. Future getVisibleRegion({ - @required int mapId, + required int mapId, }) { throw UnimplementedError('getVisibleRegion() has not been implemented.'); } @@ -168,7 +195,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// of the map, not necessarily of the whole screen. Future getScreenCoordinate( LatLng latLng, { - @required int mapId, + required int mapId, }) { throw UnimplementedError('getScreenCoordinate() has not been implemented.'); } @@ -180,7 +207,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// of the map, not necessarily of the whole screen. Future getLatLng( ScreenCoordinate screenCoordinate, { - @required int mapId, + required int mapId, }) { throw UnimplementedError('getLatLng() has not been implemented.'); } @@ -195,7 +222,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// * [isMarkerInfoWindowShown] to check if the Info Window is showing. Future showMarkerInfoWindow( MarkerId markerId, { - @required int mapId, + required int mapId, }) { throw UnimplementedError( 'showMarkerInfoWindow() has not been implemented.'); @@ -211,7 +238,7 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// * [isMarkerInfoWindowShown] to check if the Info Window is showing. Future hideMarkerInfoWindow( MarkerId markerId, { - @required int mapId, + required int mapId, }) { throw UnimplementedError( 'hideMarkerInfoWindow() has not been implemented.'); @@ -227,21 +254,23 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { /// * [hideMarkerInfoWindow] to hide the Info Window. Future isMarkerInfoWindowShown( MarkerId markerId, { - @required int mapId, + required int mapId, }) { throw UnimplementedError('updateMapOptions() has not been implemented.'); } - /// Returns the current zoom level of the map + /// Returns the current zoom level of the map. Future getZoomLevel({ - @required int mapId, + required int mapId, }) { throw UnimplementedError('getZoomLevel() has not been implemented.'); } - /// Returns the image bytes of the map - Future takeSnapshot({ - @required int mapId, + /// Returns the image bytes of the map. + /// + /// Returns null if a snapshot cannot be created. + Future takeSnapshot({ + required int mapId, }) { throw UnimplementedError('takeSnapshot() has not been implemented.'); } @@ -250,70 +279,81 @@ abstract class GoogleMapsFlutterPlatform extends PlatformInterface { // into the plugin /// The Camera started moving. - Stream onCameraMoveStarted({@required int mapId}) { + Stream onCameraMoveStarted({required int mapId}) { throw UnimplementedError('onCameraMoveStarted() has not been implemented.'); } /// The Camera finished moving to a new [CameraPosition]. - Stream onCameraMove({@required int mapId}) { + Stream onCameraMove({required int mapId}) { throw UnimplementedError('onCameraMove() has not been implemented.'); } /// The Camera is now idle. - Stream onCameraIdle({@required int mapId}) { + Stream onCameraIdle({required int mapId}) { throw UnimplementedError('onCameraMove() has not been implemented.'); } /// A [Marker] has been tapped. - Stream onMarkerTap({@required int mapId}) { + Stream onMarkerTap({required int mapId}) { throw UnimplementedError('onMarkerTap() has not been implemented.'); } /// An [InfoWindow] has been tapped. - Stream onInfoWindowTap({@required int mapId}) { + Stream onInfoWindowTap({required int mapId}) { throw UnimplementedError('onInfoWindowTap() has not been implemented.'); } /// A [Marker] has been dragged to a different [LatLng] position. - Stream onMarkerDragEnd({@required int mapId}) { + Stream onMarkerDragEnd({required int mapId}) { throw UnimplementedError('onMarkerDragEnd() has not been implemented.'); } /// A [Polyline] has been tapped. - Stream onPolylineTap({@required int mapId}) { + Stream onPolylineTap({required int mapId}) { throw UnimplementedError('onPolylineTap() has not been implemented.'); } /// A [Polygon] has been tapped. - Stream onPolygonTap({@required int mapId}) { + Stream onPolygonTap({required int mapId}) { throw UnimplementedError('onPolygonTap() has not been implemented.'); } /// A [Circle] has been tapped. - Stream onCircleTap({@required int mapId}) { + Stream onCircleTap({required int mapId}) { throw UnimplementedError('onCircleTap() has not been implemented.'); } /// A Map has been tapped at a certain [LatLng]. - Stream onTap({@required int mapId}) { + Stream onTap({required int mapId}) { throw UnimplementedError('onTap() has not been implemented.'); } /// A Map has been long-pressed at a certain [LatLng]. - Stream onLongPress({@required int mapId}) { + Stream onLongPress({required int mapId}) { throw UnimplementedError('onLongPress() has not been implemented.'); } /// Dispose of whatever resources the `mapId` is holding on to. - void dispose({@required int mapId}) { + void dispose({required int mapId}) { throw UnimplementedError('dispose() has not been implemented.'); } /// Returns a widget displaying the map view Widget buildView( - Map creationParams, - Set> gestureRecognizers, - PlatformViewCreatedCallback onPlatformViewCreated) { + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required CameraPosition initialCameraPosition, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set circles = const {}, + Set tileOverlays = const {}, + Set>? gestureRecognizers = + const >{}, + // TODO: Replace with a structured type that's part of the interface. + // See https://github.com/flutter/flutter/issues/70330. + Map mapOptions = const {}, + }) { throw UnimplementedError('buildView() has not been implemented.'); } } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/bitmap.dart index a6fdcc1b7e33..d3dc37e327fe 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 @@ -1,9 +1,10 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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' show Future; import 'dart:typed_data' show Uint8List; +import 'dart:ui' show Size; import 'package:flutter/material.dart' show ImageConfiguration, AssetImage, AssetBundleImageKey; @@ -17,6 +18,18 @@ import 'package:flutter/foundation.dart' show kIsWeb; class BitmapDescriptor { const BitmapDescriptor._(this._json); + static const String _defaultMarker = 'defaultMarker'; + static const String _fromAsset = 'fromAsset'; + static const String _fromAssetImage = 'fromAssetImage'; + static const String _fromBytes = 'fromBytes'; + + static const Set _validTypes = { + _defaultMarker, + _fromAsset, + _fromAssetImage, + _fromBytes, + }; + /// Convenience hue value representing red. static const double hueRed = 0.0; @@ -49,28 +62,14 @@ class BitmapDescriptor { /// Creates a BitmapDescriptor that refers to the default marker image. static const BitmapDescriptor defaultMarker = - BitmapDescriptor._(['defaultMarker']); + BitmapDescriptor._([_defaultMarker]); /// Creates a BitmapDescriptor that refers to a colorization of the default /// marker image. For convenience, there is a predefined set of hue values. /// See e.g. [hueYellow]. static BitmapDescriptor defaultMarkerWithHue(double hue) { assert(0.0 <= hue && hue < 360.0); - return BitmapDescriptor._(['defaultMarker', hue]); - } - - /// Creates a BitmapDescriptor using the name of a bitmap image in the assets - /// directory. - /// - /// Use [fromAssetImage]. This method does not respect the screen dpi when - /// picking an asset image. - @Deprecated("Use fromAssetImage instead") - static BitmapDescriptor fromAsset(String assetName, {String package}) { - if (package == null) { - return BitmapDescriptor._(['fromAsset', assetName]); - } else { - return BitmapDescriptor._(['fromAsset', assetName, package]); - } + return BitmapDescriptor._([_defaultMarker, hue]); } /// Creates a [BitmapDescriptor] from an asset image. @@ -83,29 +82,31 @@ class BitmapDescriptor { static Future fromAssetImage( ImageConfiguration configuration, String assetName, { - AssetBundle bundle, - String package, + AssetBundle? bundle, + String? package, bool mipmaps = true, }) async { - if (!mipmaps && configuration.devicePixelRatio != null) { - return BitmapDescriptor._([ - 'fromAssetImage', + double? devicePixelRatio = configuration.devicePixelRatio; + if (!mipmaps && devicePixelRatio != null) { + return BitmapDescriptor._([ + _fromAssetImage, assetName, - configuration.devicePixelRatio, + devicePixelRatio, ]); } final AssetImage assetImage = AssetImage(assetName, package: package, bundle: bundle); final AssetBundleImageKey assetBundleImageKey = await assetImage.obtainKey(configuration); - return BitmapDescriptor._([ - 'fromAssetImage', + final Size? size = configuration.size; + return BitmapDescriptor._([ + _fromAssetImage, assetBundleImageKey.name, assetBundleImageKey.scale, - if (kIsWeb && configuration?.size != null) + if (kIsWeb && size != null) [ - configuration.size.width, - configuration.size.height, + size.width, + size.height, ], ]); } @@ -113,11 +114,56 @@ class BitmapDescriptor { /// Creates a BitmapDescriptor using an array of bytes that must be encoded /// as PNG. static BitmapDescriptor fromBytes(Uint8List byteData) { - return BitmapDescriptor._(['fromBytes', byteData]); + return BitmapDescriptor._([_fromBytes, byteData]); + } + + /// The inverse of .toJson. + // This is needed in Web to re-hydrate BitmapDescriptors that have been + // transformed to JSON for transport. + // TODO(https://github.com/flutter/flutter/issues/70330): Clean this up. + BitmapDescriptor.fromJson(Object json) : _json = json { + assert(_json is List); + final jsonList = json as List; + assert(_validTypes.contains(jsonList[0])); + switch (jsonList[0]) { + case _defaultMarker: + assert(jsonList.length <= 2); + if (jsonList.length == 2) { + assert(jsonList[1] is num); + assert(0 <= jsonList[1] && jsonList[1] < 360); + } + break; + case _fromBytes: + assert(jsonList.length == 2); + assert(jsonList[1] != null && jsonList[1] is List); + assert((jsonList[1] as List).isNotEmpty); + break; + case _fromAsset: + assert(jsonList.length <= 3); + assert(jsonList[1] != null && jsonList[1] is String); + assert((jsonList[1] as String).isNotEmpty); + if (jsonList.length == 3) { + assert(jsonList[2] != null && jsonList[2] is String); + assert((jsonList[2] as String).isNotEmpty); + } + break; + case _fromAssetImage: + assert(jsonList.length <= 4); + assert(jsonList[1] != null && jsonList[1] is String); + assert((jsonList[1] as String).isNotEmpty); + assert(jsonList[2] != null && jsonList[2] is double); + if (jsonList.length == 4) { + assert(jsonList[3] != null && jsonList[3] is List); + assert((jsonList[3] as List).length == 2); + } + break; + default: + break; + } } - final dynamic _json; + final Object _json; /// Convert the object to a Json format. - dynamic toJson() => _json; + Object toJson() => _json; } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/callbacks.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/callbacks.dart index c20ece5d6c7c..3b484c1feb05 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/camera.dart index 10ea1e98846a..7cb6369e7f59 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 @@ -1,11 +1,9 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:ui' show hashValues, Offset; -import 'package:meta/meta.dart' show required; - import 'types.dart'; /// The position of the map "camera", the view point from which the world is shown in the map view. @@ -19,7 +17,7 @@ class CameraPosition { /// null. const CameraPosition({ this.bearing = 0.0, - @required this.target, + required this.target, this.tilt = 0.0, this.zoom = 0.0, }) : assert(bearing != null), @@ -63,7 +61,7 @@ class CameraPosition { /// Serializes [CameraPosition]. /// /// Mainly for internal use when calling [CameraUpdate.newCameraPosition]. - dynamic toMap() => { + Object toMap() => { 'bearing': bearing, 'target': target.toJson(), 'tilt': tilt, @@ -73,23 +71,27 @@ class CameraPosition { /// Deserializes [CameraPosition] from a map. /// /// Mainly for internal use. - static CameraPosition fromMap(dynamic json) { - if (json == null) { + static CameraPosition? fromMap(Object? json) { + if (json == null || !(json is Map)) { + return null; + } + final LatLng? target = LatLng.fromJson(json['target']); + if (target == null) { return null; } return CameraPosition( bearing: json['bearing'], - target: LatLng.fromJson(json['target']), + target: target, tilt: json['tilt'], zoom: json['zoom'], ); } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { if (identical(this, other)) return true; if (runtimeType != other.runtimeType) return false; - final CameraPosition typedOther = other; + final CameraPosition typedOther = other as CameraPosition; return bearing == typedOther.bearing && target == typedOther.target && tilt == typedOther.tilt && @@ -107,19 +109,19 @@ class CameraPosition { /// Defines a camera move, supporting absolute moves as well as moves relative /// the current position. class CameraUpdate { - CameraUpdate._(this._json); + const CameraUpdate._(this._json); /// Returns a camera update that moves the camera to the specified position. static CameraUpdate newCameraPosition(CameraPosition cameraPosition) { return CameraUpdate._( - ['newCameraPosition', cameraPosition.toMap()], + ['newCameraPosition', cameraPosition.toMap()], ); } /// Returns a camera update that moves the camera target to the specified /// geographical location. static CameraUpdate newLatLng(LatLng latLng) { - return CameraUpdate._(['newLatLng', latLng.toJson()]); + return CameraUpdate._(['newLatLng', latLng.toJson()]); } /// Returns a camera update that transforms the camera so that the specified @@ -127,7 +129,7 @@ class CameraUpdate { /// possible zoom level. A non-zero [padding] insets the bounding box from the /// map view's edges. The camera's new tilt and bearing will both be 0.0. static CameraUpdate newLatLngBounds(LatLngBounds bounds, double padding) { - return CameraUpdate._([ + return CameraUpdate._([ 'newLatLngBounds', bounds.toJson(), padding, @@ -138,7 +140,7 @@ class CameraUpdate { /// geographical location and zoom level. static CameraUpdate newLatLngZoom(LatLng latLng, double zoom) { return CameraUpdate._( - ['newLatLngZoom', latLng.toJson(), zoom], + ['newLatLngZoom', latLng.toJson(), zoom], ); } @@ -150,18 +152,18 @@ class CameraUpdate { /// 75 to the south of the current location, measured in screen coordinates. static CameraUpdate scrollBy(double dx, double dy) { return CameraUpdate._( - ['scrollBy', dx, dy], + ['scrollBy', dx, dy], ); } /// Returns a camera update that modifies the camera zoom level by the /// specified amount. The optional [focus] is a screen point whose underlying /// geographical location should be invariant, if possible, by the movement. - static CameraUpdate zoomBy(double amount, [Offset focus]) { + static CameraUpdate zoomBy(double amount, [Offset? focus]) { if (focus == null) { - return CameraUpdate._(['zoomBy', amount]); + return CameraUpdate._(['zoomBy', amount]); } else { - return CameraUpdate._([ + return CameraUpdate._([ 'zoomBy', amount, [focus.dx, focus.dy], @@ -174,7 +176,7 @@ class CameraUpdate { /// /// Equivalent to the result of calling `zoomBy(1.0)`. static CameraUpdate zoomIn() { - return CameraUpdate._(['zoomIn']); + return const CameraUpdate._(['zoomIn']); } /// Returns a camera update that zooms the camera out, bringing the camera @@ -182,16 +184,16 @@ class CameraUpdate { /// /// Equivalent to the result of calling `zoomBy(-1.0)`. static CameraUpdate zoomOut() { - return CameraUpdate._(['zoomOut']); + return const CameraUpdate._(['zoomOut']); } /// Returns a camera update that sets the camera zoom level. static CameraUpdate zoomTo(double zoom) { - return CameraUpdate._(['zoomTo', zoom]); + return CameraUpdate._(['zoomTo', zoom]); } - final dynamic _json; + final Object _json; /// Converts this object to something serializable in JSON. - dynamic toJson() => _json; + Object toJson() => _json; } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/cap.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/cap.dart index 68bf14c36408..f5f43209d828 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -17,16 +17,16 @@ class Cap { /// /// This is the default cap type at start and end vertices of Polylines with /// solid stroke pattern. - static const Cap buttCap = Cap._(['buttCap']); + static const Cap buttCap = Cap._(['buttCap']); /// Cap that is a semicircle with radius equal to half the stroke width, /// centered at the start or end vertex of a [Polyline] with solid stroke /// pattern. - static const Cap roundCap = Cap._(['roundCap']); + static const Cap roundCap = Cap._(['roundCap']); /// Cap that is squared off after extending half the stroke width beyond the /// start or end vertex of a [Polyline] with solid stroke pattern. - static const Cap squareCap = Cap._(['squareCap']); + static const Cap squareCap = Cap._(['squareCap']); /// Constructs a new CustomCap with a bitmap overlay centered at the start or /// end vertex of a [Polyline], orientated according to the direction of the line's @@ -45,11 +45,11 @@ class Cap { }) { assert(bitmapDescriptor != null); assert(refWidth > 0.0); - return Cap._(['customCap', bitmapDescriptor.toJson(), refWidth]); + return Cap._(['customCap', bitmapDescriptor.toJson(), refWidth]); } - final dynamic _json; + final Object _json; /// Converts this object to something serializable in JSON. - dynamic toJson() => _json; + Object toJson() => _json; } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle.dart index d1418a4c30b1..1845195b31c6 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 @@ -1,10 +1,10 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/foundation.dart' show VoidCallback; import 'package:flutter/material.dart' show Color, Colors; -import 'package:meta/meta.dart' show immutable, required; +import 'package:meta/meta.dart' show immutable; import 'types.dart'; @@ -12,36 +12,17 @@ import 'types.dart'; /// /// This does not have to be globally unique, only unique among the list. @immutable -class CircleId { +class CircleId extends MapsObjectId { /// Creates an immutable identifier for a [Circle]. - CircleId(this.value) : assert(value != null); - - /// value of the [CircleId]. - final String value; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - final CircleId typedOther = other; - return value == typedOther.value; - } - - @override - int get hashCode => value.hashCode; - - @override - String toString() { - return 'CircleId{value: $value}'; - } + const CircleId(String value) : super(value); } /// Draws a circle on the map. @immutable -class Circle { +class Circle implements MapsObject { /// Creates an immutable representation of a [Circle] to draw on [GoogleMap]. const Circle({ - @required this.circleId, + required this.circleId, this.consumeTapEvents = false, this.fillColor = Colors.transparent, this.center = const LatLng(0.0, 0.0), @@ -56,6 +37,9 @@ class Circle { /// Uniquely identifies a [Circle]. final CircleId circleId; + @override + CircleId get mapsId => circleId; + /// True if the [Circle] consumes tap events. /// /// If this is false, [onTap] callback will not be triggered. @@ -91,20 +75,20 @@ class Circle { final int zIndex; /// Callbacks to receive tap events for circle placed on this map. - final VoidCallback onTap; + final VoidCallback? onTap; /// Creates a new [Circle] object whose values are the same as this instance, /// unless overwritten by the specified parameters. Circle copyWith({ - bool consumeTapEventsParam, - Color fillColorParam, - LatLng centerParam, - double radiusParam, - Color strokeColorParam, - int strokeWidthParam, - bool visibleParam, - int zIndexParam, - VoidCallback onTapParam, + bool? consumeTapEventsParam, + Color? fillColorParam, + LatLng? centerParam, + double? radiusParam, + Color? strokeColorParam, + int? strokeWidthParam, + bool? visibleParam, + int? zIndexParam, + VoidCallback? onTapParam, }) { return Circle( circleId: circleId, @@ -124,10 +108,10 @@ class Circle { Circle clone() => copyWith(); /// Converts this object to something serializable in JSON. - dynamic toJson() { - final Map json = {}; + Object toJson() { + final Map json = {}; - void addIfPresent(String fieldName, dynamic value) { + void addIfPresent(String fieldName, Object? value) { if (value != null) { json[fieldName] = value; } @@ -150,7 +134,7 @@ class Circle { bool operator ==(Object other) { if (identical(this, other)) return true; if (other.runtimeType != runtimeType) return false; - final Circle typedOther = other; + final Circle typedOther = other as Circle; return circleId == typedOther.circleId && consumeTapEvents == typedOther.consumeTapEvents && fillColor == typedOther.fillColor && diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle_updates.dart index 6f494423a38f..f3fdbb447c94 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle_updates.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/circle_updates.dart @@ -1,110 +1,24 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - -import 'package:flutter/foundation.dart' show setEquals; - import 'types.dart'; -import 'utils/circle.dart'; /// [Circle] update events to be applied to the [GoogleMap]. /// /// Used in [GoogleMapController] when the map is updated. // (Do not re-export) -class CircleUpdates { +class CircleUpdates extends MapsObjectUpdates { /// Computes [CircleUpdates] given previous and current [Circle]s. - CircleUpdates.from(Set previous, Set current) { - if (previous == null) { - previous = Set.identity(); - } - - if (current == null) { - current = Set.identity(); - } - - final Map previousCircles = keyByCircleId(previous); - final Map currentCircles = keyByCircleId(current); - - final Set prevCircleIds = previousCircles.keys.toSet(); - final Set currentCircleIds = currentCircles.keys.toSet(); - - Circle idToCurrentCircle(CircleId id) { - return currentCircles[id]; - } - - final Set _circleIdsToRemove = - prevCircleIds.difference(currentCircleIds); - - final Set _circlesToAdd = currentCircleIds - .difference(prevCircleIds) - .map(idToCurrentCircle) - .toSet(); - - /// Returns `true` if [current] is not equals to previous one with the - /// same id. - bool hasChanged(Circle current) { - final Circle previous = previousCircles[current.circleId]; - return current != previous; - } - - final Set _circlesToChange = currentCircleIds - .intersection(prevCircleIds) - .map(idToCurrentCircle) - .where(hasChanged) - .toSet(); - - circlesToAdd = _circlesToAdd; - circleIdsToRemove = _circleIdsToRemove; - circlesToChange = _circlesToChange; - } + CircleUpdates.from(Set previous, Set current) + : super.from(previous, current, objectName: 'circle'); /// Set of Circles to be added in this update. - Set circlesToAdd; + Set get circlesToAdd => objectsToAdd; /// Set of CircleIds to be removed in this update. - Set circleIdsToRemove; + Set get circleIdsToRemove => objectIdsToRemove.cast(); /// Set of Circles to be changed in this update. - Set circlesToChange; - - /// Converts this object to something serializable in JSON. - Map toJson() { - final Map updateMap = {}; - - void addIfNonNull(String fieldName, dynamic value) { - if (value != null) { - updateMap[fieldName] = value; - } - } - - addIfNonNull('circlesToAdd', serializeCircleSet(circlesToAdd)); - addIfNonNull('circlesToChange', serializeCircleSet(circlesToChange)); - addIfNonNull('circleIdsToRemove', - circleIdsToRemove.map((CircleId m) => m.value).toList()); - - return updateMap; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - final CircleUpdates typedOther = other; - return setEquals(circlesToAdd, typedOther.circlesToAdd) && - setEquals(circleIdsToRemove, typedOther.circleIdsToRemove) && - setEquals(circlesToChange, typedOther.circlesToChange); - } - - @override - int get hashCode => - hashValues(circlesToAdd, circleIdsToRemove, circlesToChange); - - @override - String toString() { - return '_CircleUpdates{circlesToAdd: $circlesToAdd, ' - 'circleIdsToRemove: $circleIdsToRemove, ' - 'circlesToChange: $circlesToChange}'; - } + Set get circlesToChange => objectsToChange; } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/joint_type.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/joint_type.dart index c7df0b298624..64e7a3d8cbdc 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/location.dart index 6b76a6d496ac..42c66e036fd7 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -29,16 +29,18 @@ class LatLng { final double longitude; /// Converts this object to something serializable in JSON. - dynamic toJson() { + Object toJson() { return [latitude, longitude]; } /// Initialize a LatLng from an \[lat, lng\] array. - static LatLng fromJson(dynamic json) { + static LatLng? fromJson(Object? json) { if (json == null) { return null; } - return LatLng(json[0], json[1]); + assert(json is List && json.length == 2); + final list = json as List; + return LatLng(list[0], list[1]); } @override @@ -66,7 +68,7 @@ class LatLngBounds { /// /// The latitude of the southwest corner cannot be larger than the /// latitude of the northeast corner. - LatLngBounds({@required this.southwest, @required this.northeast}) + LatLngBounds({required this.southwest, required this.northeast}) : assert(southwest != null), assert(northeast != null), assert(southwest.latitude <= northeast.latitude); @@ -78,8 +80,8 @@ class LatLngBounds { final LatLng northeast; /// Converts this object to something serializable in JSON. - dynamic toJson() { - return [southwest.toJson(), northeast.toJson()]; + Object toJson() { + return [southwest.toJson(), northeast.toJson()]; } /// Returns whether this rectangle contains the given [LatLng]. @@ -102,13 +104,15 @@ class LatLngBounds { /// Converts a list to [LatLngBounds]. @visibleForTesting - static LatLngBounds fromList(dynamic json) { + static LatLngBounds? fromList(Object? json) { if (json == null) { return null; } + assert(json is List && json.length == 2); + final list = json as List; return LatLngBounds( - southwest: LatLng.fromJson(json[0]), - northeast: LatLng.fromJson(json[1]), + southwest: LatLng.fromJson(list[0])!, + northeast: LatLng.fromJson(list[1])!, ); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object.dart new file mode 100644 index 000000000000..77d958be01e2 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object.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. + +import 'package:flutter/foundation.dart' show objectRuntimeType; +import 'package:meta/meta.dart' show immutable; + +/// Uniquely identifies object an among [GoogleMap] collections of a specific +/// type. +/// +/// This does not have to be globally unique, only unique among the collection. +@immutable +class MapsObjectId { + /// Creates an immutable object representing a [T] among [GoogleMap] Ts. + /// + /// An [AssertionError] will be thrown if [value] is null. + const MapsObjectId(this.value) : assert(value != null); + + /// The value of the id. + final String value; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + final MapsObjectId typedOther = other as MapsObjectId; + return value == typedOther.value; + } + + @override + int get hashCode => value.hashCode; + + @override + String toString() { + return '${objectRuntimeType(this, 'MapsObjectId')}($value)'; + } +} + +/// A common interface for maps types. +abstract class MapsObject { + /// A identifier for this object. + MapsObjectId get mapsId; + + /// Returns a duplicate of this object. + T clone(); + + /// Converts this object to something serializable in JSON. + Object toJson(); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object_updates.dart new file mode 100644 index 000000000000..2e2eefa3d32e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/maps_object_updates.dart @@ -0,0 +1,126 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui' show hashValues, hashList; + +import 'package:flutter/foundation.dart' show objectRuntimeType, setEquals; + +import 'maps_object.dart'; +import 'utils/maps_object.dart'; + +/// Update specification for a set of objects. +class MapsObjectUpdates { + /// Computes updates given previous and current object sets. + /// + /// [objectName] is the prefix to use when serializing the updates into a JSON + /// dictionary. E.g., 'circle' will give 'circlesToAdd', 'circlesToUpdate', + /// 'circleIdsToRemove'. + MapsObjectUpdates.from( + Set previous, + Set current, { + required this.objectName, + }) { + final Map, T> previousObjects = keyByMapsObjectId(previous); + final Map, T> currentObjects = keyByMapsObjectId(current); + + final Set> previousObjectIds = previousObjects.keys.toSet(); + final Set> currentObjectIds = currentObjects.keys.toSet(); + + /// Maps an ID back to a [T] in [currentObjects]. + /// + /// It is a programming error to call this with an ID that is not guaranteed + /// to be in [currentObjects]. + T _idToCurrentObject(MapsObjectId id) { + return currentObjects[id]!; + } + + _objectIdsToRemove = previousObjectIds.difference(currentObjectIds); + + _objectsToAdd = currentObjectIds + .difference(previousObjectIds) + .map(_idToCurrentObject) + .toSet(); + + // Returns `true` if [current] is not equals to previous one with the + // same id. + bool hasChanged(T current) { + final T? previous = previousObjects[current.mapsId as MapsObjectId]; + return current != previous; + } + + _objectsToChange = currentObjectIds + .intersection(previousObjectIds) + .map(_idToCurrentObject) + .where(hasChanged) + .toSet(); + } + + /// The name of the objects being updated, for use in serialization. + final String objectName; + + /// Set of objects to be added in this update. + Set get objectsToAdd { + return _objectsToAdd; + } + + late Set _objectsToAdd; + + /// Set of objects to be removed in this update. + Set> get objectIdsToRemove { + return _objectIdsToRemove; + } + + late Set> _objectIdsToRemove; + + /// Set of objects to be changed in this update. + Set get objectsToChange { + return _objectsToChange; + } + + late Set _objectsToChange; + + /// Converts this object to JSON. + Object toJson() { + final Map updateMap = {}; + + void addIfNonNull(String fieldName, Object? value) { + if (value != null) { + updateMap[fieldName] = value; + } + } + + addIfNonNull('${objectName}sToAdd', serializeMapsObjectSet(_objectsToAdd)); + addIfNonNull( + '${objectName}sToChange', serializeMapsObjectSet(_objectsToChange)); + addIfNonNull( + '${objectName}IdsToRemove', + _objectIdsToRemove + .map((MapsObjectId m) => m.value) + .toList()); + + return updateMap; + } + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is MapsObjectUpdates && + setEquals(_objectsToAdd, other._objectsToAdd) && + setEquals(_objectIdsToRemove, other._objectIdsToRemove) && + setEquals(_objectsToChange, other._objectsToChange); + } + + @override + int get hashCode => hashValues(hashList(_objectsToAdd), + hashList(_objectIdsToRemove), hashList(_objectsToChange)); + + @override + String toString() { + return '${objectRuntimeType(this, 'MapsObjectUpdates')}(add: $objectsToAdd, ' + 'remove: $objectIdsToRemove, ' + 'change: $objectsToChange)'; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker.dart index 9b57f9676334..0d1b780c24d2 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 @@ -1,19 +1,16 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:ui' show hashValues, Offset; import 'package:flutter/foundation.dart' show ValueChanged, VoidCallback; -import 'package:meta/meta.dart' show immutable, required; +import 'package:meta/meta.dart' show immutable; import 'types.dart'; -dynamic _offsetToJson(Offset offset) { - if (offset == null) { - return null; - } - return [offset.dx, offset.dy]; +Object _offsetToJson(Offset offset) { + return [offset.dx, offset.dy]; } /// Text labels for a [Marker] info window. @@ -32,12 +29,12 @@ class InfoWindow { /// Text displayed in an info window when the user taps the marker. /// /// A null value means no title. - final String title; + final String? title; /// Additional text displayed below the [title]. /// /// A null value means no additional text. - final String snippet; + final String? snippet; /// The icon image point that will be the anchor of the info window when /// displayed. @@ -48,15 +45,15 @@ class InfoWindow { final Offset anchor; /// onTap callback for this [InfoWindow]. - final VoidCallback onTap; + final VoidCallback? onTap; /// Creates a new [InfoWindow] object whose values are the same as this instance, /// unless overwritten by the specified parameters. InfoWindow copyWith({ - String titleParam, - String snippetParam, - Offset anchorParam, - VoidCallback onTapParam, + String? titleParam, + String? snippetParam, + Offset? anchorParam, + VoidCallback? onTapParam, }) { return InfoWindow( title: titleParam ?? title, @@ -66,10 +63,10 @@ class InfoWindow { ); } - dynamic _toJson() { - final Map json = {}; + Object _toJson() { + final Map json = {}; - void addIfPresent(String fieldName, dynamic value) { + void addIfPresent(String fieldName, Object? value) { if (value != null) { json[fieldName] = value; } @@ -86,7 +83,7 @@ class InfoWindow { bool operator ==(Object other) { if (identical(this, other)) return true; if (other.runtimeType != runtimeType) return false; - final InfoWindow typedOther = other; + final InfoWindow typedOther = other as InfoWindow; return title == typedOther.title && snippet == typedOther.snippet && anchor == typedOther.anchor; @@ -105,28 +102,9 @@ class InfoWindow { /// /// This does not have to be globally unique, only unique among the list. @immutable -class MarkerId { +class MarkerId extends MapsObjectId { /// Creates an immutable identifier for a [Marker]. - MarkerId(this.value) : assert(value != null); - - /// value of the [MarkerId]. - final String value; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - final MarkerId typedOther = other; - return value == typedOther.value; - } - - @override - int get hashCode => value.hashCode; - - @override - String toString() { - return 'MarkerId{value: $value}'; - } + const MarkerId(String value) : super(value); } /// Marks a geographical location on the map. @@ -135,7 +113,7 @@ class MarkerId { /// the map's surface; that is, it will not necessarily change orientation /// due to map rotations, tilting, or zooming. @immutable -class Marker { +class Marker implements MapsObject { /// Creates a set of marker configuration options. /// /// Default marker options. @@ -156,7 +134,7 @@ class Marker { /// * reports [onTap] events /// * reports [onDragEnd] events const Marker({ - @required this.markerId, + required this.markerId, this.alpha = 1.0, this.anchor = const Offset(0.5, 1.0), this.consumeTapEvents = false, @@ -175,6 +153,9 @@ class Marker { /// Uniquely identifies a [Marker]. final MarkerId markerId; + @override + MarkerId get mapsId => markerId; + /// The opacity of the marker, between 0.0 and 1.0 inclusive. /// /// 0.0 means fully transparent, 1.0 means fully opaque. @@ -224,27 +205,27 @@ class Marker { final double zIndex; /// Callbacks to receive tap events for markers placed on this map. - final VoidCallback onTap; + final VoidCallback? onTap; /// Signature reporting the new [LatLng] at the end of a drag event. - final ValueChanged onDragEnd; + final ValueChanged? onDragEnd; /// Creates a new [Marker] object whose values are the same as this instance, /// unless overwritten by the specified parameters. Marker copyWith({ - double alphaParam, - Offset anchorParam, - bool consumeTapEventsParam, - bool draggableParam, - bool flatParam, - BitmapDescriptor iconParam, - InfoWindow infoWindowParam, - LatLng positionParam, - double rotationParam, - bool visibleParam, - double zIndexParam, - VoidCallback onTapParam, - ValueChanged onDragEndParam, + double? alphaParam, + Offset? anchorParam, + bool? consumeTapEventsParam, + bool? draggableParam, + bool? flatParam, + BitmapDescriptor? iconParam, + InfoWindow? infoWindowParam, + LatLng? positionParam, + double? rotationParam, + bool? visibleParam, + double? zIndexParam, + VoidCallback? onTapParam, + ValueChanged? onDragEndParam, }) { return Marker( markerId: markerId, @@ -268,10 +249,10 @@ class Marker { Marker clone() => copyWith(); /// Converts this object to something serializable in JSON. - Map toJson() { - final Map json = {}; + Object toJson() { + final Map json = {}; - void addIfPresent(String fieldName, dynamic value) { + void addIfPresent(String fieldName, Object? value) { if (value != null) { json[fieldName] = value; } @@ -283,9 +264,9 @@ class Marker { addIfPresent('consumeTapEvents', consumeTapEvents); addIfPresent('draggable', draggable); addIfPresent('flat', flat); - addIfPresent('icon', icon?.toJson()); - addIfPresent('infoWindow', infoWindow?._toJson()); - addIfPresent('position', position?.toJson()); + addIfPresent('icon', icon.toJson()); + addIfPresent('infoWindow', infoWindow._toJson()); + addIfPresent('position', position.toJson()); addIfPresent('rotation', rotation); addIfPresent('visible', visible); addIfPresent('zIndex', zIndex); @@ -296,7 +277,7 @@ class Marker { bool operator ==(Object other) { if (identical(this, other)) return true; if (other.runtimeType != runtimeType) return false; - final Marker typedOther = other; + final Marker typedOther = other as Marker; return markerId == typedOther.markerId && alpha == typedOther.alpha && anchor == typedOther.anchor && diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker_updates.dart index bb6ea8813ea3..27257c628033 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker_updates.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/marker_updates.dart @@ -1,110 +1,24 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - -import 'package:flutter/foundation.dart' show setEquals; - import 'types.dart'; -import 'utils/marker.dart'; /// [Marker] update events to be applied to the [GoogleMap]. /// /// Used in [GoogleMapController] when the map is updated. // (Do not re-export) -class MarkerUpdates { +class MarkerUpdates extends MapsObjectUpdates { /// Computes [MarkerUpdates] given previous and current [Marker]s. - MarkerUpdates.from(Set previous, Set current) { - if (previous == null) { - previous = Set.identity(); - } - - if (current == null) { - current = Set.identity(); - } - - final Map previousMarkers = keyByMarkerId(previous); - final Map currentMarkers = keyByMarkerId(current); - - final Set prevMarkerIds = previousMarkers.keys.toSet(); - final Set currentMarkerIds = currentMarkers.keys.toSet(); - - Marker idToCurrentMarker(MarkerId id) { - return currentMarkers[id]; - } - - final Set _markerIdsToRemove = - prevMarkerIds.difference(currentMarkerIds); - - final Set _markersToAdd = currentMarkerIds - .difference(prevMarkerIds) - .map(idToCurrentMarker) - .toSet(); - - /// Returns `true` if [current] is not equals to previous one with the - /// same id. - bool hasChanged(Marker current) { - final Marker previous = previousMarkers[current.markerId]; - return current != previous; - } - - final Set _markersToChange = currentMarkerIds - .intersection(prevMarkerIds) - .map(idToCurrentMarker) - .where(hasChanged) - .toSet(); - - markersToAdd = _markersToAdd; - markerIdsToRemove = _markerIdsToRemove; - markersToChange = _markersToChange; - } + MarkerUpdates.from(Set previous, Set current) + : super.from(previous, current, objectName: 'marker'); /// Set of Markers to be added in this update. - Set markersToAdd; + Set get markersToAdd => objectsToAdd; /// Set of MarkerIds to be removed in this update. - Set markerIdsToRemove; + Set get markerIdsToRemove => objectIdsToRemove.cast(); /// Set of Markers to be changed in this update. - Set markersToChange; - - /// Converts this object to something serializable in JSON. - Map toJson() { - final Map updateMap = {}; - - void addIfNonNull(String fieldName, dynamic value) { - if (value != null) { - updateMap[fieldName] = value; - } - } - - addIfNonNull('markersToAdd', serializeMarkerSet(markersToAdd)); - addIfNonNull('markersToChange', serializeMarkerSet(markersToChange)); - addIfNonNull('markerIdsToRemove', - markerIdsToRemove.map((MarkerId m) => m.value).toList()); - - return updateMap; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - final MarkerUpdates typedOther = other; - return setEquals(markersToAdd, typedOther.markersToAdd) && - setEquals(markerIdsToRemove, typedOther.markerIdsToRemove) && - setEquals(markersToChange, typedOther.markersToChange); - } - - @override - int get hashCode => - hashValues(markersToAdd, markerIdsToRemove, markersToChange); - - @override - String toString() { - return '_MarkerUpdates{markersToAdd: $markersToAdd, ' - 'markerIdsToRemove: $markerIdsToRemove, ' - 'markersToChange: $markersToChange}'; - } + Set get markersToChange => objectsToChange; } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/pattern_item.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/pattern_item.dart index 28c7ce9d33dd..89f29d25e4cc 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -10,14 +10,14 @@ class PatternItem { const PatternItem._(this._json); /// A dot used in the stroke pattern for a [Polyline]. - static const PatternItem dot = PatternItem._(['dot']); + static const PatternItem dot = PatternItem._(['dot']); /// A dash used in the stroke pattern for a [Polyline]. /// /// [length] has to be non-negative. static PatternItem dash(double length) { assert(length >= 0.0); - return PatternItem._(['dash', length]); + return PatternItem._(['dash', length]); } /// A gap used in the stroke pattern for a [Polyline]. @@ -25,11 +25,11 @@ class PatternItem { /// [length] has to be non-negative. static PatternItem gap(double length) { assert(length >= 0.0); - return PatternItem._(['gap', length]); + return PatternItem._(['gap', length]); } - final dynamic _json; + final Object _json; /// Converts this object to something serializable in JSON. - dynamic toJson() => _json; + Object toJson() => _json; } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon.dart index 3b5e25060faf..569bd4c1f553 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 @@ -1,10 +1,11 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart' show listEquals, VoidCallback; import 'package:flutter/material.dart' show Color, Colors; -import 'package:meta/meta.dart' show immutable, required; +import 'package:meta/meta.dart' show immutable; import 'types.dart'; @@ -12,40 +13,22 @@ import 'types.dart'; /// /// This does not have to be globally unique, only unique among the list. @immutable -class PolygonId { +class PolygonId extends MapsObjectId { /// Creates an immutable identifier for a [Polygon]. - PolygonId(this.value) : assert(value != null); - - /// value of the [PolygonId]. - final String value; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - final PolygonId typedOther = other; - return value == typedOther.value; - } - - @override - int get hashCode => value.hashCode; - - @override - String toString() { - return 'PolygonId{value: $value}'; - } + const PolygonId(String value) : super(value); } /// Draws a polygon through geographical locations on the map. @immutable -class Polygon { +class Polygon implements MapsObject { /// Creates an immutable representation of a polygon through geographical locations on the map. const Polygon({ - @required this.polygonId, + required this.polygonId, this.consumeTapEvents = false, this.fillColor = Colors.black, this.geodesic = false, this.points = const [], + this.holes = const >[], this.strokeColor = Colors.black, this.strokeWidth = 10, this.visible = true, @@ -56,6 +39,9 @@ class Polygon { /// Uniquely identifies a [Polygon]. final PolygonId polygonId; + @override + PolygonId get mapsId => polygonId; + /// True if the [Polygon] consumes tap events. /// /// If this is false, [onTap] callback will not be triggered. @@ -77,6 +63,14 @@ class Polygon { /// default; to form a closed polygon, the start and end points must be the same. final List points; + /// To create an empty area within a polygon, you need to use holes. + /// To create the hole, the coordinates defining the hole path must be inside the polygon. + /// + /// The vertices of the holes to be cut out of polygon. + /// + /// Line segments of each points of hole are drawn inside polygon between consecutive hole points. + final List> holes; + /// True if the marker is visible. final bool visible; @@ -97,20 +91,21 @@ class Polygon { final int zIndex; /// Callbacks to receive tap events for polygon placed on this map. - final VoidCallback onTap; + final VoidCallback? onTap; /// Creates a new [Polygon] object whose values are the same as this instance, /// unless overwritten by the specified parameters. Polygon copyWith({ - bool consumeTapEventsParam, - Color fillColorParam, - bool geodesicParam, - List pointsParam, - Color strokeColorParam, - int strokeWidthParam, - bool visibleParam, - int zIndexParam, - VoidCallback onTapParam, + bool? consumeTapEventsParam, + Color? fillColorParam, + bool? geodesicParam, + List? pointsParam, + List>? holesParam, + Color? strokeColorParam, + int? strokeWidthParam, + bool? visibleParam, + int? zIndexParam, + VoidCallback? onTapParam, }) { return Polygon( polygonId: polygonId, @@ -118,6 +113,7 @@ class Polygon { fillColor: fillColorParam ?? fillColor, geodesic: geodesicParam ?? geodesic, points: pointsParam ?? points, + holes: holesParam ?? holes, strokeColor: strokeColorParam ?? strokeColor, strokeWidth: strokeWidthParam ?? strokeWidth, visible: visibleParam ?? visible, @@ -132,10 +128,10 @@ class Polygon { } /// Converts this object to something serializable in JSON. - dynamic toJson() { - final Map json = {}; + Object toJson() { + final Map json = {}; - void addIfPresent(String fieldName, dynamic value) { + void addIfPresent(String fieldName, Object? value) { if (value != null) { json[fieldName] = value; } @@ -154,6 +150,10 @@ class Polygon { json['points'] = _pointsToJson(); } + if (holes != null) { + json['holes'] = _holesToJson(); + } + return json; } @@ -161,12 +161,13 @@ class Polygon { bool operator ==(Object other) { if (identical(this, other)) return true; if (other.runtimeType != runtimeType) return false; - final Polygon typedOther = other; + final Polygon typedOther = other as Polygon; return polygonId == typedOther.polygonId && consumeTapEvents == typedOther.consumeTapEvents && fillColor == typedOther.fillColor && geodesic == typedOther.geodesic && listEquals(points, typedOther.points) && + const DeepCollectionEquality().equals(holes, typedOther.holes) && visible == typedOther.visible && strokeColor == typedOther.strokeColor && strokeWidth == typedOther.strokeWidth && @@ -176,11 +177,23 @@ class Polygon { @override int get hashCode => polygonId.hashCode; - dynamic _pointsToJson() { - final List result = []; + Object _pointsToJson() { + final List result = []; for (final LatLng point in points) { result.add(point.toJson()); } return result; } + + List> _holesToJson() { + final List> result = >[]; + for (final List hole in holes) { + final List jsonHole = []; + for (final LatLng point in hole) { + jsonHole.add(point.toJson()); + } + result.add(jsonHole); + } + return result; + } } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon_updates.dart index cc8b8e26c896..8b62141ce03c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon_updates.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polygon_updates.dart @@ -1,110 +1,24 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - -import 'package:flutter/foundation.dart' show setEquals; - import 'types.dart'; -import 'utils/polygon.dart'; /// [Polygon] update events to be applied to the [GoogleMap]. /// /// Used in [GoogleMapController] when the map is updated. // (Do not re-export) -class PolygonUpdates { +class PolygonUpdates extends MapsObjectUpdates { /// Computes [PolygonUpdates] given previous and current [Polygon]s. - PolygonUpdates.from(Set previous, Set current) { - if (previous == null) { - previous = Set.identity(); - } - - if (current == null) { - current = Set.identity(); - } - - final Map previousPolygons = keyByPolygonId(previous); - final Map currentPolygons = keyByPolygonId(current); - - final Set prevPolygonIds = previousPolygons.keys.toSet(); - final Set currentPolygonIds = currentPolygons.keys.toSet(); - - Polygon idToCurrentPolygon(PolygonId id) { - return currentPolygons[id]; - } - - final Set _polygonIdsToRemove = - prevPolygonIds.difference(currentPolygonIds); - - final Set _polygonsToAdd = currentPolygonIds - .difference(prevPolygonIds) - .map(idToCurrentPolygon) - .toSet(); - - /// Returns `true` if [current] is not equals to previous one with the - /// same id. - bool hasChanged(Polygon current) { - final Polygon previous = previousPolygons[current.polygonId]; - return current != previous; - } - - final Set _polygonsToChange = currentPolygonIds - .intersection(prevPolygonIds) - .map(idToCurrentPolygon) - .where(hasChanged) - .toSet(); - - polygonsToAdd = _polygonsToAdd; - polygonIdsToRemove = _polygonIdsToRemove; - polygonsToChange = _polygonsToChange; - } + PolygonUpdates.from(Set previous, Set current) + : super.from(previous, current, objectName: 'polygon'); /// Set of Polygons to be added in this update. - Set polygonsToAdd; + Set get polygonsToAdd => objectsToAdd; /// Set of PolygonIds to be removed in this update. - Set polygonIdsToRemove; + Set get polygonIdsToRemove => objectIdsToRemove.cast(); /// Set of Polygons to be changed in this update. - Set polygonsToChange; - - /// Converts this object to something serializable in JSON. - Map toJson() { - final Map updateMap = {}; - - void addIfNonNull(String fieldName, dynamic value) { - if (value != null) { - updateMap[fieldName] = value; - } - } - - addIfNonNull('polygonsToAdd', serializePolygonSet(polygonsToAdd)); - addIfNonNull('polygonsToChange', serializePolygonSet(polygonsToChange)); - addIfNonNull('polygonIdsToRemove', - polygonIdsToRemove.map((PolygonId m) => m.value).toList()); - - return updateMap; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - final PolygonUpdates typedOther = other; - return setEquals(polygonsToAdd, typedOther.polygonsToAdd) && - setEquals(polygonIdsToRemove, typedOther.polygonIdsToRemove) && - setEquals(polygonsToChange, typedOther.polygonsToChange); - } - - @override - int get hashCode => - hashValues(polygonsToAdd, polygonIdsToRemove, polygonsToChange); - - @override - String toString() { - return '_PolygonUpdates{polygonsToAdd: $polygonsToAdd, ' - 'polygonIdsToRemove: $polygonIdsToRemove, ' - 'polygonsToChange: $polygonsToChange}'; - } + Set get polygonsToChange => objectsToChange; } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline.dart index ae5c3b976352..c324aeb5f492 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 @@ -1,10 +1,10 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:flutter/foundation.dart' show listEquals, VoidCallback; import 'package:flutter/material.dart' show Color, Colors; -import 'package:meta/meta.dart' show immutable, required; +import 'package:meta/meta.dart' show immutable; import 'types.dart'; @@ -12,38 +12,19 @@ import 'types.dart'; /// /// This does not have to be globally unique, only unique among the list. @immutable -class PolylineId { +class PolylineId extends MapsObjectId { /// Creates an immutable object representing a [PolylineId] among [GoogleMap] polylines. /// /// An [AssertionError] will be thrown if [value] is null. - PolylineId(this.value) : assert(value != null); - - /// value of the [PolylineId]. - final String value; - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - final PolylineId typedOther = other; - return value == typedOther.value; - } - - @override - int get hashCode => value.hashCode; - - @override - String toString() { - return 'PolylineId{value: $value}'; - } + const PolylineId(String value) : super(value); } /// Draws a line through geographical locations on the map. @immutable -class Polyline { +class Polyline implements MapsObject { /// Creates an immutable object representing a line drawn through geographical locations on the map. const Polyline({ - @required this.polylineId, + required this.polylineId, this.consumeTapEvents = false, this.color = Colors.black, this.endCap = Cap.buttCap, @@ -61,6 +42,9 @@ class Polyline { /// Uniquely identifies a [Polyline]. final PolylineId polylineId; + @override + PolylineId get mapsId => polylineId; + /// True if the [Polyline] consumes tap events. /// /// If this is false, [onTap] callback will not be triggered. @@ -129,23 +113,23 @@ class Polyline { final int zIndex; /// Callbacks to receive tap events for polyline placed on this map. - final VoidCallback onTap; + final VoidCallback? onTap; /// Creates a new [Polyline] object whose values are the same as this instance, /// unless overwritten by the specified parameters. Polyline copyWith({ - Color colorParam, - bool consumeTapEventsParam, - Cap endCapParam, - bool geodesicParam, - JointType jointTypeParam, - List patternsParam, - List pointsParam, - Cap startCapParam, - bool visibleParam, - int widthParam, - int zIndexParam, - VoidCallback onTapParam, + Color? colorParam, + bool? consumeTapEventsParam, + Cap? endCapParam, + bool? geodesicParam, + JointType? jointTypeParam, + List? patternsParam, + List? pointsParam, + Cap? startCapParam, + bool? visibleParam, + int? widthParam, + int? zIndexParam, + VoidCallback? onTapParam, }) { return Polyline( polylineId: polylineId, @@ -174,10 +158,10 @@ class Polyline { } /// Converts this object to something serializable in JSON. - dynamic toJson() { - final Map json = {}; + Object toJson() { + final Map json = {}; - void addIfPresent(String fieldName, dynamic value) { + void addIfPresent(String fieldName, Object? value) { if (value != null) { json[fieldName] = value; } @@ -186,10 +170,10 @@ class Polyline { addIfPresent('polylineId', polylineId.value); addIfPresent('consumeTapEvents', consumeTapEvents); addIfPresent('color', color.value); - addIfPresent('endCap', endCap?.toJson()); + addIfPresent('endCap', endCap.toJson()); addIfPresent('geodesic', geodesic); - addIfPresent('jointType', jointType?.value); - addIfPresent('startCap', startCap?.toJson()); + addIfPresent('jointType', jointType.value); + addIfPresent('startCap', startCap.toJson()); addIfPresent('visible', visible); addIfPresent('width', width); addIfPresent('zIndex', zIndex); @@ -209,7 +193,7 @@ class Polyline { bool operator ==(Object other) { if (identical(this, other)) return true; if (other.runtimeType != runtimeType) return false; - final Polyline typedOther = other; + final Polyline typedOther = other as Polyline; return polylineId == typedOther.polylineId && consumeTapEvents == typedOther.consumeTapEvents && color == typedOther.color && @@ -227,16 +211,16 @@ class Polyline { @override int get hashCode => polylineId.hashCode; - dynamic _pointsToJson() { - final List result = []; + Object _pointsToJson() { + final List result = []; for (final LatLng point in points) { result.add(point.toJson()); } return result; } - dynamic _patternToJson() { - final List result = []; + Object _patternToJson() { + final List result = []; for (final PatternItem patternItem in patterns) { if (patternItem != null) { result.add(patternItem.toJson()); diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline_updates.dart index f871928c0ac4..30cd99f73229 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline_updates.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/polyline_updates.dart @@ -1,111 +1,25 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; - -import 'package:flutter/foundation.dart' show setEquals; - -import 'utils/polyline.dart'; import 'types.dart'; /// [Polyline] update events to be applied to the [GoogleMap]. /// /// Used in [GoogleMapController] when the map is updated. // (Do not re-export) -class PolylineUpdates { +class PolylineUpdates extends MapsObjectUpdates { /// Computes [PolylineUpdates] given previous and current [Polyline]s. - PolylineUpdates.from(Set previous, Set current) { - if (previous == null) { - previous = Set.identity(); - } - - if (current == null) { - current = Set.identity(); - } - - final Map previousPolylines = - keyByPolylineId(previous); - final Map currentPolylines = keyByPolylineId(current); - - final Set prevPolylineIds = previousPolylines.keys.toSet(); - final Set currentPolylineIds = currentPolylines.keys.toSet(); - - Polyline idToCurrentPolyline(PolylineId id) { - return currentPolylines[id]; - } - - final Set _polylineIdsToRemove = - prevPolylineIds.difference(currentPolylineIds); - - final Set _polylinesToAdd = currentPolylineIds - .difference(prevPolylineIds) - .map(idToCurrentPolyline) - .toSet(); - - /// Returns `true` if [current] is not equals to previous one with the - /// same id. - bool hasChanged(Polyline current) { - final Polyline previous = previousPolylines[current.polylineId]; - return current != previous; - } - - final Set _polylinesToChange = currentPolylineIds - .intersection(prevPolylineIds) - .map(idToCurrentPolyline) - .where(hasChanged) - .toSet(); - - polylinesToAdd = _polylinesToAdd; - polylineIdsToRemove = _polylineIdsToRemove; - polylinesToChange = _polylinesToChange; - } + PolylineUpdates.from(Set previous, Set current) + : super.from(previous, current, objectName: 'polyline'); /// Set of Polylines to be added in this update. - Set polylinesToAdd; + Set get polylinesToAdd => objectsToAdd; /// Set of PolylineIds to be removed in this update. - Set polylineIdsToRemove; + Set get polylineIdsToRemove => + objectIdsToRemove.cast(); /// Set of Polylines to be changed in this update. - Set polylinesToChange; - - /// Converts this object to something serializable in JSON. - Map toJson() { - final Map updateMap = {}; - - void addIfNonNull(String fieldName, dynamic value) { - if (value != null) { - updateMap[fieldName] = value; - } - } - - addIfNonNull('polylinesToAdd', serializePolylineSet(polylinesToAdd)); - addIfNonNull('polylinesToChange', serializePolylineSet(polylinesToChange)); - addIfNonNull('polylineIdsToRemove', - polylineIdsToRemove.map((PolylineId m) => m.value).toList()); - - return updateMap; - } - - @override - bool operator ==(Object other) { - if (identical(this, other)) return true; - if (other.runtimeType != runtimeType) return false; - final PolylineUpdates typedOther = other; - return setEquals(polylinesToAdd, typedOther.polylinesToAdd) && - setEquals(polylineIdsToRemove, typedOther.polylineIdsToRemove) && - setEquals(polylinesToChange, typedOther.polylinesToChange); - } - - @override - int get hashCode => - hashValues(polylinesToAdd, polylineIdsToRemove, polylinesToChange); - - @override - String toString() { - return '_PolylineUpdates{polylinesToAdd: $polylinesToAdd, ' - 'polylineIdsToRemove: $polylineIdsToRemove, ' - 'polylinesToChange: $polylinesToChange}'; - } + Set get polylinesToChange => objectsToChange; } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/screen_coordinate.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/screen_coordinate.dart index 965db7969bc2..8c9c083913ce 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 @@ -1,10 +1,10 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:ui' show hashValues; -import 'package:meta/meta.dart' show immutable, required; +import 'package:meta/meta.dart' show immutable; /// Represents a point coordinate in the [GoogleMap]'s view. /// @@ -15,8 +15,8 @@ import 'package:meta/meta.dart' show immutable, required; class ScreenCoordinate { /// Creates an immutable representation of a point coordinate in the [GoogleMap]'s view. const ScreenCoordinate({ - @required this.x, - @required this.y, + required this.x, + required this.y, }); /// Represents the number of pixels from the left of the [GoogleMap]. @@ -26,7 +26,7 @@ class ScreenCoordinate { final int y; /// Converts this object to something serializable in JSON. - dynamic toJson() { + Object toJson() { return { "x": x, "y": y, diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile.dart new file mode 100644 index 000000000000..d602b127f06c --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile.dart @@ -0,0 +1,42 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; +import 'package:meta/meta.dart' show immutable; + +/// Contains information about a Tile that is returned by a [TileProvider]. +@immutable +class Tile { + /// Creates an immutable representation of a [Tile] to draw by [TileProvider]. + const Tile(this.width, this.height, this.data); + + /// The width of the image encoded by data in logical pixels. + final int width; + + /// The height of the image encoded by data in logical pixels. + final int height; + + /// A byte array containing the image data. + /// + /// The image data format must be natively supported for decoding by the platform. + /// e.g on Android it can only be one of the [supported image formats for decoding](https://developer.android.com/guide/topics/media/media-formats#image-formats). + final Uint8List? data; + + /// Converts this object to JSON. + Object toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, Object? value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent('width', width); + addIfPresent('height', height); + addIfPresent('data', data); + + return json; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart new file mode 100644 index 000000000000..8cdd2c4699e1 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay.dart @@ -0,0 +1,152 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui' show hashValues; + +import 'package:flutter/foundation.dart'; +import 'package:meta/meta.dart' show immutable; + +import 'types.dart'; + +/// Uniquely identifies a [TileOverlay] among [GoogleMap] tile overlays. +@immutable +class TileOverlayId extends MapsObjectId { + /// Creates an immutable identifier for a [TileOverlay]. + const TileOverlayId(String value) : super(value); +} + +/// A set of images which are displayed on top of the base map tiles. +/// +/// These tiles may be transparent, allowing you to add features to existing maps. +/// +/// ## Tile Coordinates +/// +/// Note that the world is projected using the Mercator projection +/// (see [Wikipedia](https://en.wikipedia.org/wiki/Mercator_projection)) with the left (west) side +/// of the map corresponding to -180 degrees of longitude and the right (east) side of the map +/// corresponding to 180 degrees of longitude. To make the map square, the top (north) side of the +/// map corresponds to 85.0511 degrees of latitude and the bottom (south) side of the map +/// corresponds to -85.0511 degrees of latitude. Areas outside this latitude range are not rendered. +/// +/// At each zoom level, the map is divided into tiles and only the tiles that overlap the screen are +/// downloaded and rendered. Each tile is square and the map is divided into tiles as follows: +/// +/// * At zoom level 0, one tile represents the entire world. The coordinates of that tile are +/// (x, y) = (0, 0). +/// * At zoom level 1, the world is divided into 4 tiles arranged in a 2 x 2 grid. +/// * ... +/// * At zoom level N, the world is divided into 4N tiles arranged in a 2N x 2N grid. +/// +/// Note that the minimum zoom level that the camera supports (which can depend on various factors) +/// is GoogleMap.getMinZoomLevel and the maximum zoom level is GoogleMap.getMaxZoomLevel. +/// +/// The coordinates of the tiles are measured from the top left (northwest) corner of the map. +/// At zoom level N, the x values of the tile coordinates range from 0 to 2N - 1 and increase from +/// west to east and the y values range from 0 to 2N - 1 and increase from north to south. +/// +class TileOverlay implements MapsObject { + /// Creates an immutable representation of a [TileOverlay] to draw on [GoogleMap]. + const TileOverlay({ + required this.tileOverlayId, + this.fadeIn = true, + this.tileProvider, + this.transparency = 0.0, + this.zIndex = 0, + this.visible = true, + this.tileSize = 256, + }) : assert(transparency >= 0.0 && transparency <= 1.0); + + /// Uniquely identifies a [TileOverlay]. + final TileOverlayId tileOverlayId; + + @override + TileOverlayId get mapsId => tileOverlayId; + + /// Whether the tiles should fade in. The default is true. + final bool fadeIn; + + /// The tile provider to use for this tile overlay. + final TileProvider? tileProvider; + + /// The transparency of the tile overlay. The default transparency is 0 (opaque). + final double transparency; + + /// The tile overlay's zIndex, i.e., the order in which it will be drawn where + /// overlays with larger values are drawn above those with lower values + final int zIndex; + + /// The visibility for the tile overlay. The default visibility is true. + final bool visible; + + /// Specifies the number of logical pixels (not points) that the returned tile images will prefer + /// to display as. iOS only. + /// + /// Defaults to 256, which is the traditional size of Google Maps tiles. + /// As an example, an application developer may wish to provide retina tiles (512 pixel edge length) + /// on retina devices, to keep the same number of tiles per view as the default value of 256 + /// would give on a non-retina device. + final int tileSize; + + /// Creates a new [TileOverlay] object whose values are the same as this instance, + /// unless overwritten by the specified parameters. + TileOverlay copyWith({ + bool? fadeInParam, + TileProvider? tileProviderParam, + double? transparencyParam, + int? zIndexParam, + bool? visibleParam, + int? tileSizeParam, + }) { + return TileOverlay( + tileOverlayId: tileOverlayId, + fadeIn: fadeInParam ?? fadeIn, + tileProvider: tileProviderParam ?? tileProvider, + transparency: transparencyParam ?? transparency, + zIndex: zIndexParam ?? zIndex, + visible: visibleParam ?? visible, + tileSize: tileSizeParam ?? tileSize, + ); + } + + TileOverlay clone() => copyWith(); + + /// Converts this object to JSON. + Object toJson() { + final Map json = {}; + + void addIfPresent(String fieldName, Object? value) { + if (value != null) { + json[fieldName] = value; + } + } + + addIfPresent('tileOverlayId', tileOverlayId.value); + addIfPresent('fadeIn', fadeIn); + addIfPresent('transparency', transparency); + addIfPresent('zIndex', zIndex); + addIfPresent('visible', visible); + addIfPresent('tileSize', tileSize); + + return json; + } + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is TileOverlay && + tileOverlayId == other.tileOverlayId && + fadeIn == other.fadeIn && + tileProvider == other.tileProvider && + transparency == other.transparency && + zIndex == other.zIndex && + visible == other.visible && + tileSize == other.tileSize; + } + + @override + int get hashCode => hashValues(tileOverlayId, fadeIn, tileProvider, + transparency, zIndex, visible, tileSize); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay_updates.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay_updates.dart new file mode 100644 index 000000000000..e40db7da10fe --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_overlay_updates.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 'types.dart'; + +/// Update specification for a set of [TileOverlay]s. +class TileOverlayUpdates extends MapsObjectUpdates { + /// Computes [TileOverlayUpdates] given previous and current [TileOverlay]s. + TileOverlayUpdates.from(Set previous, Set current) + : super.from(previous, current, objectName: 'tileOverlay'); + + /// Set of TileOverlays to be added in this update. + Set get tileOverlaysToAdd => objectsToAdd; + + /// Set of TileOverlayIds to be removed in this update. + Set get tileOverlayIdsToRemove => + objectIdsToRemove.cast(); + + /// Set of TileOverlays to be changed in this update. + Set get tileOverlaysToChange => objectsToChange; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_provider.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_provider.dart new file mode 100644 index 000000000000..dfe6937e24a4 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/tile_provider.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 'types.dart'; + +/// An interface for a class that provides the tile images for a TileOverlay. +abstract class TileProvider { + /// Stub tile that is used to indicate that no tile exists for a specific tile coordinate. + static const Tile noTile = Tile(-1, -1, null); + + /// Returns the tile to be used for this tile coordinate. + /// + /// See [TileOverlay] for the specification of tile coordinates. + Future getTile(int x, int y, int? zoom); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/types.dart index e56c3a5dd646..5e2e4c234ccf 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -11,6 +11,8 @@ export 'circle_updates.dart'; export 'circle.dart'; export 'joint_type.dart'; export 'location.dart'; +export 'maps_object_updates.dart'; +export 'maps_object.dart'; export 'marker_updates.dart'; export 'marker.dart'; export 'pattern_item.dart'; @@ -19,6 +21,9 @@ export 'polygon.dart'; export 'polyline_updates.dart'; export 'polyline.dart'; export 'screen_coordinate.dart'; +export 'tile.dart'; +export 'tile_overlay.dart'; +export 'tile_provider.dart'; export 'ui.dart'; // Export the utils, they're used by the Widget @@ -26,3 +31,4 @@ export 'utils/circle.dart'; export 'utils/marker.dart'; export 'utils/polygon.dart'; export 'utils/polyline.dart'; +export 'utils/tile_overlay.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ui.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/ui.dart index 8d84171bac03..38c34fcfd27f 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 @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -39,19 +39,19 @@ class CameraTargetBounds { /// The geographical bounding box for the map camera target. /// /// A null value means the camera target is unbounded. - final LatLngBounds bounds; + final LatLngBounds? bounds; /// Unbounded camera target. static const CameraTargetBounds unbounded = CameraTargetBounds(null); /// Converts this object to something serializable in JSON. - dynamic toJson() => [bounds?.toJson()]; + Object toJson() => [bounds?.toJson()]; @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { if (identical(this, other)) return true; if (runtimeType != other.runtimeType) return false; - final CameraTargetBounds typedOther = other; + final CameraTargetBounds typedOther = other as CameraTargetBounds; return bounds == typedOther.bounds; } @@ -76,23 +76,23 @@ class MinMaxZoomPreference { : assert(minZoom == null || maxZoom == null || minZoom <= maxZoom); /// The preferred minimum zoom level or null, if unbounded from below. - final double minZoom; + final double? minZoom; /// The preferred maximum zoom level or null, if unbounded from above. - final double maxZoom; + final double? maxZoom; /// Unbounded zooming. static const MinMaxZoomPreference unbounded = MinMaxZoomPreference(null, null); /// Converts this object to something serializable in JSON. - dynamic toJson() => [minZoom, maxZoom]; + Object toJson() => [minZoom, maxZoom]; @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { if (identical(this, other)) return true; if (runtimeType != other.runtimeType) return false; - final MinMaxZoomPreference typedOther = other; + final MinMaxZoomPreference typedOther = other as MinMaxZoomPreference; return minZoom == typedOther.minZoom && maxZoom == typedOther.maxZoom; } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/circle.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/circle.dart index 5c3af96f8e02..bf1754fdf399 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/circle.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/circle.dart @@ -1,22 +1,16 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import '../types.dart'; +import 'maps_object.dart'; /// Converts an [Iterable] of Circles in a Map of CircleId -> Circle. Map keyByCircleId(Iterable circles) { - if (circles == null) { - return {}; - } - return Map.fromEntries(circles.map((Circle circle) => - MapEntry(circle.circleId, circle.clone()))); + return keyByMapsObjectId(circles).cast(); } /// Converts a Set of Circles into something serializable in JSON. -List> serializeCircleSet(Set circles) { - if (circles == null) { - return null; - } - return circles.map>((Circle p) => p.toJson()).toList(); +Object serializeCircleSet(Set circles) { + return serializeMapsObjectSet(circles); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/maps_object.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/maps_object.dart new file mode 100644 index 000000000000..da5a49825c7f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/maps_object.dart @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../maps_object.dart'; + +/// Converts an [Iterable] of [MapsObject]s in a Map of [MapObjectId] -> [MapObject]. +Map, T> keyByMapsObjectId( + Iterable objects) { + return Map, T>.fromEntries(objects.map((T object) => + MapEntry, T>( + object.mapsId as MapsObjectId, object.clone()))); +} + +/// Converts a Set of [MapsObject]s into something serializable in JSON. +Object serializeMapsObjectSet(Set mapsObjects) { + return mapsObjects.map((MapsObject p) => p.toJson()).toList(); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/marker.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/marker.dart index 7a2c76d8055b..4be3f2a2f9a4 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/marker.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/marker.dart @@ -1,22 +1,16 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import '../types.dart'; +import 'maps_object.dart'; /// Converts an [Iterable] of Markers in a Map of MarkerId -> Marker. Map keyByMarkerId(Iterable markers) { - if (markers == null) { - return {}; - } - return Map.fromEntries(markers.map((Marker marker) => - MapEntry(marker.markerId, marker.clone()))); + return keyByMapsObjectId(markers).cast(); } /// Converts a Set of Markers into something serializable in JSON. -List> serializeMarkerSet(Set markers) { - if (markers == null) { - return null; - } - return markers.map>((Marker m) => m.toJson()).toList(); +Object serializeMarkerSet(Set markers) { + return serializeMapsObjectSet(markers); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polygon.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polygon.dart index 9434ddaa077d..ba4ce7d6f55f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polygon.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polygon.dart @@ -1,22 +1,16 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import '../types.dart'; +import 'maps_object.dart'; /// Converts an [Iterable] of Polygons in a Map of PolygonId -> Polygon. Map keyByPolygonId(Iterable polygons) { - if (polygons == null) { - return {}; - } - return Map.fromEntries(polygons.map((Polygon polygon) => - MapEntry(polygon.polygonId, polygon.clone()))); + return keyByMapsObjectId(polygons).cast(); } /// Converts a Set of Polygons into something serializable in JSON. -List> serializePolygonSet(Set polygons) { - if (polygons == null) { - return null; - } - return polygons.map>((Polygon p) => p.toJson()).toList(); +Object serializePolygonSet(Set polygons) { + return serializeMapsObjectSet(polygons); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polyline.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polyline.dart index 9cef6319ddb5..8c188b021b2f 100644 --- a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polyline.dart +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/polyline.dart @@ -1,25 +1,16 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import '../types.dart'; +import 'maps_object.dart'; /// Converts an [Iterable] of Polylines in a Map of PolylineId -> Polyline. Map keyByPolylineId(Iterable polylines) { - if (polylines == null) { - return {}; - } - return Map.fromEntries(polylines.map( - (Polyline polyline) => MapEntry( - polyline.polylineId, polyline.clone()))); + return keyByMapsObjectId(polylines).cast(); } /// Converts a Set of Polylines into something serializable in JSON. -List> serializePolylineSet(Set polylines) { - if (polylines == null) { - return null; - } - return polylines - .map>((Polyline p) => p.toJson()) - .toList(); +Object serializePolylineSet(Set polylines) { + return serializeMapsObjectSet(polylines); } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/tile_overlay.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/tile_overlay.dart new file mode 100644 index 000000000000..fae61a4b4433 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/utils/tile_overlay.dart @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../types.dart'; +import 'maps_object.dart'; + +/// Converts an [Iterable] of TileOverlay in a Map of TileOverlayId -> TileOverlay. +Map keyTileOverlayId( + Iterable tileOverlays) { + return keyByMapsObjectId(tileOverlays) + .cast(); +} + +/// Converts a Set of TileOverlays into something serializable in JSON. +Object serializeTileOverlaySet(Set tileOverlays) { + return serializeMapsObjectSet(tileOverlays); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_platform_interface/pubspec.yaml index fd3a1c434960..5b278a812a8e 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,23 +1,25 @@ name: google_maps_flutter_platform_interface description: A common platform interface for the google_maps_flutter plugin. -homepage: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter_platform_interface +repository: https://github.com/flutter/plugins/tree/master/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: 1.0.4 +version: 2.0.4 + +environment: + sdk: '>=2.12.0 <3.0.0' + flutter: ">=2.0.0" dependencies: + collection: ^1.15.0 flutter: sdk: flutter - meta: ^1.0.5 - plugin_platform_interface: ^1.0.1 - stream_transform: ^1.2.0 + meta: ^1.3.0 + plugin_platform_interface: ^2.0.0 + stream_transform: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - mockito: ^4.1.1 - pedantic: ^1.8.0 - -environment: - sdk: ">=2.3.0 <3.0.0" - flutter: ">=1.9.1+hotfix.4 <2.0.0" + mockito: ^5.0.0 + pedantic: ^1.10.0 diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/method_channel/method_channel_google_maps_flutter_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/method_channel/method_channel_google_maps_flutter_test.dart new file mode 100644 index 000000000000..19e81c960839 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/method_channel/method_channel_google_maps_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 '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() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$MethodChannelGoogleMapsFlutter', () { + late List log; + + setUp(() async { + log = []; + }); + + /// Initializes a map with the given ID and canned responses, logging all + /// calls to [log]. + void configureMockMap( + MethodChannelGoogleMapsFlutter maps, { + required int mapId, + required Future? Function(MethodCall call) handler, + }) { + maps + .ensureChannelInitialized(mapId) + .setMockMethodCallHandler((MethodCall methodCall) { + log.add(methodCall.method); + return handler(methodCall); + }); + } + + // Calls each method that uses invokeMethod with a return type other than + // void to ensure that the casting/nullability handling succeeds. + // + // TODO(stuartmorgan): Remove this once there is real test coverage of + // each method, since that would cover this issue. + test('non-void invokeMethods handle types correctly', () async { + const int mapId = 0; + final MethodChannelGoogleMapsFlutter maps = + MethodChannelGoogleMapsFlutter(); + configureMockMap(maps, mapId: mapId, + handler: (MethodCall methodCall) async { + switch (methodCall.method) { + case 'map#getLatLng': + return [1.0, 2.0]; + case 'markers#isInfoWindowShown': + return true; + case 'map#getZoomLevel': + return 2.5; + case 'map#takeSnapshot': + return null; + } + }); + + await maps.getLatLng(ScreenCoordinate(x: 0, y: 0), mapId: mapId); + await maps.isMarkerInfoWindowShown(MarkerId(''), mapId: mapId); + await maps.getZoomLevel(mapId: mapId); + await maps.takeSnapshot(mapId: mapId); + // Check that all the invokeMethod calls happened. + expect(log, [ + 'map#getLatLng', + 'markers#isInfoWindowShown', + 'map#getZoomLevel', + 'map#takeSnapshot', + ]); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart index a003b94d544c..2c50313ab8a6 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 @@ -1,9 +1,8 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // 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:plugin_platform_interface/plugin_platform_interface.dart'; @@ -36,29 +35,6 @@ void main() { GoogleMapsFlutterPlatform.instance = ExtendsGoogleMapsFlutterPlatform(); }); }); - - group('$MethodChannelGoogleMapsFlutter', () { - const MethodChannel channel = - MethodChannel('plugins.flutter.io/google_maps_flutter'); - final List log = []; - channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - }); - -// final MethodChannelGoogleMapsFlutter map = MethodChannelGoogleMapsFlutter(0); - - tearDown(() { - log.clear(); - }); - - test('foo', () async { -// await map.foo(); - expect( - log, - [], - ); - }); - }); } class GoogleMapsFlutterPlatformMock extends Mock diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/bitmap_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/bitmap_test.dart new file mode 100644 index 000000000000..6d02b2c630df --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/bitmap_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 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; + +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$BitmapDescriptor', () { + test('toJson / fromJson', () { + final descriptor = + BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueCyan); + final json = descriptor.toJson(); + + // Rehydrate a new bitmap descriptor... + // ignore: deprecated_member_use_from_same_package + final descriptorFromJson = BitmapDescriptor.fromJson(json); + + expect(descriptorFromJson, isNot(descriptor)); // New instance + expect(identical(descriptorFromJson.toJson(), json), isTrue); // Same JSON + }); + + group('fromJson validation', () { + group('type validation', () { + test('correct type', () { + expect(BitmapDescriptor.fromJson(['defaultMarker']), + isA()); + }); + test('wrong type', () { + expect(() { + BitmapDescriptor.fromJson(['bogusType']); + }, throwsAssertionError); + }); + }); + group('defaultMarker', () { + test('hue is null', () { + expect(BitmapDescriptor.fromJson(['defaultMarker']), + isA()); + }); + test('hue is number', () { + expect(BitmapDescriptor.fromJson(['defaultMarker', 158]), + isA()); + }); + test('hue is not number', () { + expect(() { + BitmapDescriptor.fromJson(['defaultMarker', 'nope']); + }, throwsAssertionError); + }); + test('hue is out of range', () { + expect(() { + BitmapDescriptor.fromJson(['defaultMarker', -1]); + }, throwsAssertionError); + expect(() { + BitmapDescriptor.fromJson(['defaultMarker', 361]); + }, throwsAssertionError); + }); + }); + group('fromBytes', () { + test('with bytes', () { + expect( + BitmapDescriptor.fromJson([ + 'fromBytes', + Uint8List.fromList([1, 2, 3]) + ]), + isA()); + }); + test('without bytes', () { + expect(() { + BitmapDescriptor.fromJson(['fromBytes', null]); + }, throwsAssertionError); + expect(() { + BitmapDescriptor.fromJson(['fromBytes', []]); + }, throwsAssertionError); + }); + }); + group('fromAsset', () { + test('name is passed', () { + expect(BitmapDescriptor.fromJson(['fromAsset', 'some/path.png']), + isA()); + }); + test('name cannot be null or empty', () { + expect(() { + BitmapDescriptor.fromJson(['fromAsset', null]); + }, throwsAssertionError); + expect(() { + BitmapDescriptor.fromJson(['fromAsset', '']); + }, throwsAssertionError); + }); + test('package is passed', () { + expect( + BitmapDescriptor.fromJson( + ['fromAsset', 'some/path.png', 'some_package']), + isA()); + }); + test('package cannot be null or empty', () { + expect(() { + BitmapDescriptor.fromJson(['fromAsset', 'some/path.png', null]); + }, throwsAssertionError); + expect(() { + BitmapDescriptor.fromJson(['fromAsset', 'some/path.png', '']); + }, throwsAssertionError); + }); + }); + group('fromAssetImage', () { + test('name and dpi passed', () { + expect( + BitmapDescriptor.fromJson( + ['fromAssetImage', 'some/path.png', 1.0]), + isA()); + }); + test('name cannot be null or empty', () { + expect(() { + BitmapDescriptor.fromJson(['fromAssetImage', null, 1.0]); + }, throwsAssertionError); + expect(() { + BitmapDescriptor.fromJson(['fromAssetImage', '', 1.0]); + }, throwsAssertionError); + }); + test('dpi must be number', () { + expect(() { + BitmapDescriptor.fromJson( + ['fromAssetImage', 'some/path.png', null]); + }, throwsAssertionError); + expect(() { + BitmapDescriptor.fromJson( + ['fromAssetImage', 'some/path.png', 'one']); + }, throwsAssertionError); + }); + test('with optional [width, height] List', () { + expect( + BitmapDescriptor.fromJson([ + 'fromAssetImage', + 'some/path.png', + 1.0, + [640, 480] + ]), + isA()); + }); + test( + 'optional [width, height] List cannot be null or not contain 2 elements', + () { + expect(() { + BitmapDescriptor.fromJson( + ['fromAssetImage', 'some/path.png', 1.0, null]); + }, throwsAssertionError); + expect(() { + BitmapDescriptor.fromJson( + ['fromAssetImage', 'some/path.png', 1.0, []]); + }, throwsAssertionError); + expect(() { + BitmapDescriptor.fromJson([ + 'fromAssetImage', + 'some/path.png', + 1.0, + [640, 480, 1024] + ]); + }, throwsAssertionError); + }); + }); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/camera_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/camera_test.dart new file mode 100644 index 000000000000..11665d904556 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/camera_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_test/flutter_test.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('toMap / fromMap', () { + const cameraPosition = CameraPosition( + target: LatLng(10.0, 15.0), bearing: 0.5, tilt: 30.0, zoom: 1.5); + // Cast to to ensure that recreating from JSON, where + // type information will have likely been lost, still works. + final json = (cameraPosition.toMap() as Map) + .cast(); + final cameraPositionFromJson = CameraPosition.fromMap(json); + + expect(cameraPosition, cameraPositionFromJson); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_test.dart new file mode 100644 index 000000000000..c2ca2bdda5b7 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_test.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_test/flutter_test.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:google_maps_flutter_platform_interface/src/types/utils/maps_object.dart'; + +import 'test_maps_object.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + test('keyByMapsObjectId', () async { + const MapsObjectId id1 = MapsObjectId('1'); + const MapsObjectId id2 = MapsObjectId('2'); + const MapsObjectId id3 = MapsObjectId('3'); + const TestMapsObject object1 = TestMapsObject(id1); + const TestMapsObject object2 = TestMapsObject(id2, data: 2); + const TestMapsObject object3 = TestMapsObject(id3); + expect( + keyByMapsObjectId({object1, object2, object3}), + , TestMapsObject>{ + id1: object1, + id2: object2, + id3: object3, + }); + }); + + test('serializeMapsObjectSet', () async { + const MapsObjectId id1 = MapsObjectId('1'); + const MapsObjectId id2 = MapsObjectId('2'); + const MapsObjectId id3 = MapsObjectId('3'); + const TestMapsObject object1 = TestMapsObject(id1); + const TestMapsObject object2 = TestMapsObject(id2, data: 2); + const TestMapsObject object3 = TestMapsObject(id3); + expect( + serializeMapsObjectSet({object1, object2, object3}), + >[ + {'id': '1'}, + {'id': '2'}, + {'id': '3'} + ]); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_updates_test.dart new file mode 100644 index 000000000000..f09f70fd769e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/maps_object_updates_test.dart @@ -0,0 +1,162 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui' show hashValues, hashList; + +import 'package:flutter/rendering.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_platform_interface/src/types/maps_object.dart'; +import 'package:google_maps_flutter_platform_interface/src/types/maps_object_updates.dart'; +import 'package:google_maps_flutter_platform_interface/src/types/utils/maps_object.dart'; + +import 'test_maps_object.dart'; + +class TestMapsObjectUpdate extends MapsObjectUpdates { + TestMapsObjectUpdate.from( + Set previous, Set current) + : super.from(previous, current, objectName: 'testObject'); +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('tile overlay updates tests', () { + test('Correctly set toRemove, toAdd and toChange', () async { + const TestMapsObject to1 = + TestMapsObject(MapsObjectId('id1')); + const TestMapsObject to2 = + TestMapsObject(MapsObjectId('id2')); + const TestMapsObject to3 = + TestMapsObject(MapsObjectId('id3')); + const TestMapsObject to3Changed = + 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 TestMapsObjectUpdate updates = + TestMapsObjectUpdate.from(previous, current); + + final Set> toRemove = + Set.from(>[ + const MapsObjectId('id1') + ]); + expect(updates.objectIdsToRemove, toRemove); + + final Set toAdd = Set.from([to4]); + expect(updates.objectsToAdd, toAdd); + + final Set toChange = + Set.from([to3Changed]); + expect(updates.objectsToChange, toChange); + }); + + test('toJson', () async { + const TestMapsObject to1 = + TestMapsObject(MapsObjectId('id1')); + const TestMapsObject to2 = + TestMapsObject(MapsObjectId('id2')); + const TestMapsObject to3 = + TestMapsObject(MapsObjectId('id3')); + const TestMapsObject to3Changed = + 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 TestMapsObjectUpdate updates = + TestMapsObjectUpdate.from(previous, current); + + final Object json = updates.toJson(); + expect(json, { + 'testObjectsToAdd': serializeMapsObjectSet(updates.objectsToAdd), + 'testObjectsToChange': serializeMapsObjectSet(updates.objectsToChange), + 'testObjectIdsToRemove': updates.objectIdsToRemove + .map((MapsObjectId m) => m.value) + .toList() + }); + }); + + test('equality', () async { + const TestMapsObject to1 = + TestMapsObject(MapsObjectId('id1')); + const TestMapsObject to2 = + TestMapsObject(MapsObjectId('id2')); + const TestMapsObject to3 = + TestMapsObject(MapsObjectId('id3')); + const TestMapsObject to3Changed = + 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 TestMapsObjectUpdate updates1 = + TestMapsObjectUpdate.from(previous, current1); + final TestMapsObjectUpdate updates2 = + TestMapsObjectUpdate.from(previous, current2); + final TestMapsObjectUpdate updates3 = + TestMapsObjectUpdate.from(previous, current3); + expect(updates1, updates2); + expect(updates1, isNot(updates3)); + }); + + test('hashCode', () async { + const TestMapsObject to1 = + TestMapsObject(MapsObjectId('id1')); + const TestMapsObject to2 = + TestMapsObject(MapsObjectId('id2')); + const TestMapsObject to3 = + TestMapsObject(MapsObjectId('id3')); + const TestMapsObject to3Changed = + 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 TestMapsObjectUpdate updates = + TestMapsObjectUpdate.from(previous, current); + expect( + updates.hashCode, + hashValues( + hashList(updates.objectsToAdd), + hashList(updates.objectIdsToRemove), + hashList(updates.objectsToChange))); + }); + + test('toString', () async { + const TestMapsObject to1 = + TestMapsObject(MapsObjectId('id1')); + const TestMapsObject to2 = + TestMapsObject(MapsObjectId('id2')); + const TestMapsObject to3 = + TestMapsObject(MapsObjectId('id3')); + const TestMapsObject to3Changed = + 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 TestMapsObjectUpdate updates = + TestMapsObjectUpdate.from(previous, current); + expect( + updates.toString(), + 'TestMapsObjectUpdate(add: ${updates.objectsToAdd}, ' + 'remove: ${updates.objectIdsToRemove}, ' + 'change: ${updates.objectsToChange})'); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/test_maps_object.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/test_maps_object.dart new file mode 100644 index 000000000000..b95ae50a8f08 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/test_maps_object.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 'dart:ui' show hashValues; + +import 'package:flutter/rendering.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 { + const TestMapsObject(this.mapsId, {this.data = 1}); + + final MapsObjectId mapsId; + + final int data; + + @override + TestMapsObject clone() { + return TestMapsObject(mapsId, data: data); + } + + @override + Object toJson() { + return {'id': mapsId.value}; + } + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) { + return false; + } + return other is TestMapsObject && + mapsId == other.mapsId && + data == other.data; + } + + @override + int get hashCode => hashValues(mapsId, data); +} + +class TestMapsObjectUpdate extends MapsObjectUpdates { + TestMapsObjectUpdate.from( + Set previous, Set current) + : super.from(previous, current, objectName: 'testObject'); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_test.dart new file mode 100644 index 000000000000..3a4c34764ef7 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_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 '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); + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('tile overlay id tests', () { + test('equality', () async { + const TileOverlayId id1 = TileOverlayId('1'); + const TileOverlayId id2 = TileOverlayId('1'); + const TileOverlayId id3 = TileOverlayId('2'); + expect(id1, id2); + expect(id1, isNot(id3)); + }); + + test('toString', () async { + const TileOverlayId id1 = TileOverlayId('1'); + expect(id1.toString(), 'TileOverlayId(1)'); + }); + }); + + group('tile overlay tests', () { + test('toJson returns correct format', () async { + const TileOverlay tileOverlay = TileOverlay( + tileOverlayId: TileOverlayId('id'), + fadeIn: false, + tileProvider: null, + transparency: 0.1, + zIndex: 1, + visible: false, + tileSize: 128); + final Object json = tileOverlay.toJson(); + expect(json, { + 'tileOverlayId': 'id', + 'fadeIn': false, + 'transparency': moreOrLessEquals(0.1), + 'zIndex': 1, + 'visible': false, + 'tileSize': 128, + }); + }); + + test('invalid transparency throws', () async { + expect( + () => TileOverlay( + tileOverlayId: const TileOverlayId('id1'), transparency: -0.1), + throwsAssertionError); + expect( + () => TileOverlay( + tileOverlayId: const TileOverlayId('id2'), transparency: 1.2), + throwsAssertionError); + }); + + test('equality', () async { + final TileProvider tileProvider = _TestTileProvider(); + final TileOverlay tileOverlay1 = TileOverlay( + tileOverlayId: TileOverlayId('id1'), + fadeIn: false, + tileProvider: tileProvider, + transparency: 0.1, + zIndex: 1, + visible: false, + tileSize: 128); + final TileOverlay tileOverlaySameValues = TileOverlay( + tileOverlayId: TileOverlayId('id1'), + fadeIn: false, + tileProvider: tileProvider, + transparency: 0.1, + zIndex: 1, + visible: false, + tileSize: 128); + final TileOverlay tileOverlayDifferentId = TileOverlay( + tileOverlayId: TileOverlayId('id2'), + fadeIn: false, + tileProvider: tileProvider, + transparency: 0.1, + zIndex: 1, + visible: false, + tileSize: 128); + final TileOverlay tileOverlayDifferentProvider = TileOverlay( + tileOverlayId: TileOverlayId('id1'), + fadeIn: false, + tileProvider: null, + transparency: 0.1, + zIndex: 1, + visible: false, + tileSize: 128); + expect(tileOverlay1, tileOverlaySameValues); + expect(tileOverlay1, isNot(tileOverlayDifferentId)); + expect(tileOverlay1, isNot(tileOverlayDifferentProvider)); + }); + + test('clone', () async { + final TileProvider tileProvider = _TestTileProvider(); + // Set non-default values for every parameter. + final TileOverlay tileOverlay = TileOverlay( + tileOverlayId: TileOverlayId('id1'), + fadeIn: false, + tileProvider: tileProvider, + transparency: 0.1, + zIndex: 1, + visible: false, + tileSize: 128); + expect(tileOverlay, tileOverlay.clone()); + }); + + test('hashCode', () async { + final TileProvider tileProvider = _TestTileProvider(); + const TileOverlayId id = TileOverlayId('id1'); + final TileOverlay tileOverlay = TileOverlay( + tileOverlayId: id, + fadeIn: false, + tileProvider: tileProvider, + transparency: 0.1, + zIndex: 1, + visible: false, + tileSize: 128); + expect( + tileOverlay.hashCode, + hashValues( + tileOverlay.tileOverlayId, + tileOverlay.fadeIn, + tileOverlay.tileProvider, + tileOverlay.transparency, + tileOverlay.zIndex, + tileOverlay.visible, + tileOverlay.tileSize)); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_updates_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_updates_test.dart new file mode 100644 index 000000000000..05be14e1ba0b --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_overlay_updates_test.dart @@ -0,0 +1,126 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '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'; +import 'package:google_maps_flutter_platform_interface/src/types/utils/tile_overlay.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('tile overlay updates tests', () { + test('Correctly set toRemove, toAdd and toChange', () async { + const TileOverlay to1 = TileOverlay(tileOverlayId: TileOverlayId('id1')); + const TileOverlay to2 = TileOverlay(tileOverlayId: TileOverlayId('id2')); + const TileOverlay to3 = TileOverlay(tileOverlayId: TileOverlayId('id3')); + 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 TileOverlayUpdates updates = + TileOverlayUpdates.from(previous, current); + + final Set toRemove = + Set.from([const TileOverlayId('id1')]); + expect(updates.tileOverlayIdsToRemove, toRemove); + + final Set toAdd = Set.from([to4]); + expect(updates.tileOverlaysToAdd, toAdd); + + final Set toChange = Set.from([to3Changed]); + expect(updates.tileOverlaysToChange, toChange); + }); + + test('toJson', () async { + const TileOverlay to1 = TileOverlay(tileOverlayId: TileOverlayId('id1')); + const TileOverlay to2 = TileOverlay(tileOverlayId: TileOverlayId('id2')); + const TileOverlay to3 = TileOverlay(tileOverlayId: TileOverlayId('id3')); + 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 TileOverlayUpdates updates = + TileOverlayUpdates.from(previous, current); + + final Object json = updates.toJson(); + expect(json, { + 'tileOverlaysToAdd': serializeTileOverlaySet(updates.tileOverlaysToAdd), + 'tileOverlaysToChange': + serializeTileOverlaySet(updates.tileOverlaysToChange), + 'tileOverlayIdsToRemove': updates.tileOverlayIdsToRemove + .map((TileOverlayId m) => m.value) + .toList() + }); + }); + + test('equality', () async { + const TileOverlay to1 = TileOverlay(tileOverlayId: TileOverlayId('id1')); + const TileOverlay to2 = TileOverlay(tileOverlayId: TileOverlayId('id2')); + const TileOverlay to3 = TileOverlay(tileOverlayId: TileOverlayId('id3')); + 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 TileOverlayUpdates updates1 = + TileOverlayUpdates.from(previous, current1); + final TileOverlayUpdates updates2 = + TileOverlayUpdates.from(previous, current2); + final TileOverlayUpdates updates3 = + TileOverlayUpdates.from(previous, current3); + expect(updates1, updates2); + expect(updates1, isNot(updates3)); + }); + + test('hashCode', () async { + const TileOverlay to1 = TileOverlay(tileOverlayId: TileOverlayId('id1')); + const TileOverlay to2 = TileOverlay(tileOverlayId: TileOverlayId('id2')); + const TileOverlay to3 = TileOverlay(tileOverlayId: TileOverlayId('id3')); + 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 TileOverlayUpdates updates = + TileOverlayUpdates.from(previous, current); + expect( + updates.hashCode, + hashValues( + hashList(updates.tileOverlaysToAdd), + hashList(updates.tileOverlayIdsToRemove), + hashList(updates.tileOverlaysToChange))); + }); + + test('toString', () async { + const TileOverlay to1 = TileOverlay(tileOverlayId: TileOverlayId('id1')); + const TileOverlay to2 = TileOverlay(tileOverlayId: TileOverlayId('id2')); + const TileOverlay to3 = TileOverlay(tileOverlayId: TileOverlayId('id3')); + 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 TileOverlayUpdates updates = + TileOverlayUpdates.from(previous, current); + expect( + updates.toString(), + 'TileOverlayUpdates(add: ${updates.tileOverlaysToAdd}, ' + 'remove: ${updates.tileOverlayIdsToRemove}, ' + 'change: ${updates.tileOverlaysToChange})'); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_test.dart new file mode 100644 index 000000000000..653958474185 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/tile_test.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:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('tile tests', () { + test('toJson returns correct format', () async { + final Uint8List data = Uint8List.fromList([0, 1]); + final Tile tile = Tile(100, 200, data); + final Object json = tile.toJson(); + expect(json, { + 'width': 100, + 'height': 200, + 'data': data, + }); + }); + + test('toJson handles null data', () async { + const Tile tile = Tile(0, 0, null); + final Object json = tile.toJson(); + expect(json, { + 'width': 0, + 'height': 0, + }); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS b/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md index 0121042b1a02..36a4271cb95d 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,78 @@ +## 0.3.0+2 + +* Document `liteModeEnabled` is not available on the web. [#83737](https://github.com/flutter/flutter/issues/83737). + +## 0.3.0+1 + +* Change sizing code of `GoogleMap` widget's `HtmlElementView` so it works well when slotted. + +## 0.3.0 + +* Migrate package to null-safety. +* **Breaking changes:** + * The property `icon` of a `Marker` cannot be `null`. Defaults to `BitmapDescriptor.defaultMarker` + * The property `initialCameraPosition` of a `GoogleMapController` can't be `null`. It is also marked as `required`. + * The parameter `creationId` of the `buildView` method cannot be `null` (this should be handled internally for users of the plugin) + * Most of the Controller methods can't be called after `remove`/`dispose`. Calling these methods now will throw an Assertion error. Before it'd be a no-op, or a null-pointer exception. + +## 0.2.1 + +* Move integration tests to `example`. +* Tweak pubspec dependencies for main package. + +## 0.2.0 + +* Make this plugin compatible with the rest of null-safe plugins. +* Noop tile overlays methods, so they don't crash on web. + +**NOTE**: This plugin is **not** null-safe yet! + +## 0.1.2 + +* Update min Flutter SDK to 1.20.0. + +## 0.1.1 + +* Auto-reverse holes if they're the same direction as the polygon. [Issue](https://github.com/flutter/flutter/issues/74096). + +## 0.1.0+10 + +* Update `package:google_maps_flutter_platform_interface` to `^1.1.0`. +* Add support for Polygon Holes. + +## 0.1.0+9 + +* Update Flutter SDK constraint. + +## 0.1.0+8 + +* Update `package:google_maps_flutter_platform_interface` to `^1.0.5`. +* Add support for `fromBitmap` BitmapDescriptors. [Issue](https://github.com/flutter/flutter/issues/66622). + +## 0.1.0+7 + +* Substitute `undefined_prefixed_name: ignore` analyzer setting by a `dart:ui` shim with conditional exports. [Issue](https://github.com/flutter/flutter/issues/69309). + +## 0.1.0+6 + +* Ensure a single `InfoWindow` is shown at a time. [Issue](https://github.com/flutter/flutter/issues/67380). + +## 0.1.0+5 + +* Update `package:google_maps` to `^3.4.5`. +* Fix `GoogleMapController.getLatLng()`. [Issue](https://github.com/flutter/flutter/issues/67606). +* Make `InfoWindow` contents clickable so `onTap` works as advertised. [Issue](https://github.com/flutter/flutter/issues/67289). +* Fix `InfoWindow` snippets when converting initial markers. [Issue](https://github.com/flutter/flutter/issues/67854). + +## 0.1.0+4 + +* Update `package:sanitize_html` to `^1.4.1` to prevent [a crash](https://github.com/flutter/flutter/issues/67854) when InfoWindow title/snippet have links. + +## 0.1.0+3 + +* Fix crash when converting initial polylines and polygons. [Issue](https://github.com/flutter/flutter/issues/65152). +* Correctly convert Colors when rendering polylines, polygons and circles. [Issue](https://github.com/flutter/flutter/issues/67032). + ## 0.1.0+2 * Fix crash when converting Markers with icon explicitly set to null. [Issue](https://github.com/flutter/flutter/issues/64938). diff --git a/packages/google_maps_flutter/google_maps_flutter_web/LICENSE b/packages/google_maps_flutter/google_maps_flutter_web/LICENSE index 447867e0637e..c6823b81eb84 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/LICENSE +++ b/packages/google_maps_flutter/google_maps_flutter_web/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017, the Flutter project authors. All rights reserved. +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: 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 e1c1a5330c56..cfd5f6d8271e 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/README.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/README.md @@ -49,3 +49,5 @@ There's no "My Location" widget in web ([tracking issue](https://github.com/flut There's no `defaultMarkerWithHue` in web. If you need colored pins/markers, you may need to use your own asset images. 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. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/analysis_options.yaml b/packages/google_maps_flutter/google_maps_flutter_web/analysis_options.yaml deleted file mode 100644 index 443b16551ec9..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter_web/analysis_options.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# This is a temporary file to allow us to unblock the flutter/plugins repo CI. -# It disables some of lints that were disabled inline. Disabling lints inline -# is no longer possible, so this file is required. -# TODO(ditman) https://github.com/flutter/flutter/issues/55000 (clean this up) - -include: ../../../analysis_options.yaml - -analyzer: - errors: - undefined_prefixed_name: ignore diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/README.md b/packages/google_maps_flutter/google_maps_flutter_web/example/README.md new file mode 100644 index 000000000000..582288a561a4 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/README.md @@ -0,0 +1,31 @@ +# Testing + +This package utilizes the `integration_test` package to run its tests in a web browser. + +See [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) for more info. + +## Running the tests + +Make sure you have updated to the latest Flutter master. + +1. Check what version of Chrome is running on the machine you're running tests on. + +2. Download and install driver for that version from here: + * + +3. Start the driver using `chromedriver --port=4444` + +4. Run tests: `flutter drive -d web-server --browser-name=chrome --driver=test_driver/integration_driver.dart --target=integration_test/TEST_NAME.dart`, or (in Linux): + + * Single: `./run_test.sh integration_test/TEST_NAME.dart` + * All: `./run_test.sh` + +## Mocks + +There's new `.mocks.dart` files next to the test files that use them. + +Mock files are [generated by `package:mockito`](https://github.com/dart-lang/mockito/blob/master/NULL_SAFETY_README.md#code-generation). The contents of these files can change with how the mocks are used within the tests, in addition to actual changes in the APIs they're mocking. + +Mock files can be updated either manually by running the following command: `flutter pub run build_runner build` (or the `regen_mocks.sh` script), or automatically on each call to the `run_test.sh` script. + +Please, add whatever changes show up in mock files to your PRs, or CI will fail. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/build.yaml b/packages/google_maps_flutter/google_maps_flutter_web/example/build.yaml new file mode 100644 index 000000000000..db3104bb04c6 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/build.yaml @@ -0,0 +1,6 @@ +targets: + $default: + sources: + - integration_test/*.dart + - lib/$lib$ + - $package$ 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 new file mode 100644 index 000000000000..1d33eea4c7f3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart @@ -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 'dart:async'; +import 'dart:html' as html; + +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:integration_test/integration_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'google_maps_controller_test.mocks.dart'; + +// This value is used when comparing long~num, like +// LatLng values. +const _acceptableDelta = 0.0000000001; + +@GenerateMocks([], customMocks: [ + MockSpec(returnNullOnMissingStub: true), + MockSpec(returnNullOnMissingStub: true), + MockSpec(returnNullOnMissingStub: true), + MockSpec(returnNullOnMissingStub: true), +]) + +/// Test Google Map Controller +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('GoogleMapController', () { + final int mapId = 33930; + late GoogleMapController controller; + 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 {}, + }) { + return GoogleMapController( + mapId: mapId, + streamController: stream, + initialCameraPosition: initialCameraPosition, + markers: markers, + polygons: polygons, + polylines: polylines, + circles: circles, + mapOptions: options, + ); + } + + setUp(() { + stream = StreamController.broadcast(); + }); + + group('construct/dispose', () { + setUp(() { + controller = _createController(); + }); + + testWidgets('constructor creates widget', (WidgetTester tester) async { + expect(controller.widget, isNotNull); + expect(controller.widget, isA()); + 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; + expect(identical(first, again), isTrue); + }); + + group('dispose', () { + testWidgets('closes the stream and removes the widget', + (WidgetTester tester) async { + controller.dispose(); + + expect(stream.isClosed, isTrue); + expect(controller.widget, isNull); + }); + + testWidgets('cannot call getVisibleRegion after dispose', + (WidgetTester tester) async { + controller.dispose(); + + expect(() async { + await controller.getVisibleRegion(); + }, throwsAssertionError); + }); + + testWidgets('cannot call getScreenCoordinate after dispose', + (WidgetTester tester) async { + controller.dispose(); + + expect(() async { + await controller.getScreenCoordinate( + LatLng(43.3072465, -5.6918241), + ); + }, throwsAssertionError); + }); + + testWidgets('cannot call getLatLng after dispose', + (WidgetTester tester) async { + controller.dispose(); + + expect(() async { + await controller.getLatLng( + ScreenCoordinate(x: 640, y: 480), + ); + }, throwsAssertionError); + }); + + testWidgets('cannot call moveCamera after dispose', + (WidgetTester tester) async { + controller.dispose(); + + expect(() async { + await controller.moveCamera(CameraUpdate.zoomIn()); + }, throwsAssertionError); + }); + + testWidgets('cannot call getZoomLevel after dispose', + (WidgetTester tester) async { + controller.dispose(); + + expect(() async { + await controller.getZoomLevel(); + }, throwsAssertionError); + }); + + testWidgets('cannot updateCircles after dispose', + (WidgetTester tester) async { + controller.dispose(); + + expect(() { + controller.updateCircles(CircleUpdates.from({}, {})); + }, throwsAssertionError); + }); + + testWidgets('cannot updatePolygons after dispose', + (WidgetTester tester) async { + controller.dispose(); + + expect(() { + controller.updatePolygons(PolygonUpdates.from({}, {})); + }, throwsAssertionError); + }); + + testWidgets('cannot updatePolylines after dispose', + (WidgetTester tester) async { + controller.dispose(); + + expect(() { + controller.updatePolylines(PolylineUpdates.from({}, {})); + }, throwsAssertionError); + }); + + testWidgets('cannot updateMarkers after dispose', + (WidgetTester tester) async { + controller.dispose(); + + expect(() { + controller.updateMarkers(MarkerUpdates.from({}, {})); + }, throwsAssertionError); + + expect(() { + controller.showInfoWindow(MarkerId('any')); + }, throwsAssertionError); + + expect(() { + controller.hideInfoWindow(MarkerId('any')); + }, throwsAssertionError); + }); + + testWidgets('isInfoWindowShown defaults to false', + (WidgetTester tester) async { + controller.dispose(); + + expect(controller.isInfoWindowShown(MarkerId('any')), false); + }); + }); + }); + + group('init', () { + late MockCirclesController circles; + late MockMarkersController markers; + late MockPolygonsController polygons; + late MockPolylinesController polylines; + late gmaps.GMap map; + + setUp(() { + circles = MockCirclesController(); + markers = MockMarkersController(); + polygons = MockPolygonsController(); + polylines = MockPolylinesController(); + map = gmaps.GMap(html.DivElement()); + }); + + testWidgets('listens to map events', (WidgetTester tester) async { + controller = _createController(); + controller.debugSetOverrides( + createMap: (_, __) => map, + circles: circles, + markers: markers, + polygons: polygons, + polylines: polylines, + ); + + controller.init(); + + // Trigger events on the map, and verify they've been broadcast to the stream + final 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', []); + + final events = await capturedEvents.toList(); + + expect(events[0], isA()); + expect(events[1], isA()); + expect(events[2], isA()); + expect(events[3], isA()); + expect(events[4], isA()); + }); + + testWidgets('binds geometry controllers to map\'s', + (WidgetTester tester) async { + controller = _createController(); + controller.debugSetOverrides( + createMap: (_, __) => map, + circles: circles, + markers: markers, + polygons: polygons, + polylines: polylines, + ); + + controller.init(); + + verify(circles.bindToMap(mapId, map)); + verify(markers.bindToMap(mapId, map)); + verify(polygons.bindToMap(mapId, map)); + verify(polylines.bindToMap(mapId, map)); + }); + + testWidgets('renders initial geometry', (WidgetTester tester) async { + controller = _createController(circles: { + Circle(circleId: CircleId('circle-1')) + }, markers: { + Marker( + markerId: MarkerId('marker-1'), + infoWindow: InfoWindow( + title: 'title for test', snippet: 'snippet for test')) + }, polygons: { + 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( + polygonId: PolygonId('polygon-2-with-holes'), + points: [ + LatLng(43.355114, -5.851333), + LatLng(43.354797, -5.851860), + LatLng(43.354469, -5.851318), + LatLng(43.354762, -5.850824), + ], + holes: [ + [ + LatLng(41.354797, -6.851860), + LatLng(41.354469, -6.851318), + LatLng(41.354762, -6.850824), + ] + ], + ), + }, polylines: { + 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, + markers: markers, + polygons: polygons, + polylines: polylines, + ); + + controller.init(); + + final capturedCircles = + verify(circles.addCircles(captureAny)).captured[0] as Set; + final 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; + + expect(capturedCircles.first.circleId.value, 'circle-1'); + expect(capturedMarkers.first.markerId.value, 'marker-1'); + expect(capturedMarkers.first.infoWindow.snippet, 'snippet for test'); + expect(capturedMarkers.first.infoWindow.title, 'title for test'); + expect(capturedPolygons.first.polygonId.value, 'polygon-1'); + expect(capturedPolygons.elementAt(1).polygonId.value, + 'polygon-2-with-holes'); + expect(capturedPolygons.elementAt(1).holes, isNot(null)); + expect(capturedPolylines.first.polylineId.value, 'polyline-1'); + }); + + testWidgets('empty infoWindow does not create InfoWindow instance.', + (WidgetTester tester) async { + controller = _createController(markers: { + Marker(markerId: MarkerId('marker-1')), + }); + + controller.debugSetOverrides( + markers: markers, + ); + + controller.init(); + + final capturedMarkers = + verify(markers.addMarkers(captureAny)).captured[0] as Set; + + expect(capturedMarkers.first.infoWindow, InfoWindow.noText); + }); + + group('Initialization options', () { + gmaps.MapOptions? capturedOptions; + setUp(() { + capturedOptions = null; + }); + testWidgets('translates initial options', (WidgetTester tester) async { + controller = _createController(options: { + 'mapType': 2, + 'zoomControlsEnabled': true, + }); + controller.debugSetOverrides(createMap: (_, options) { + capturedOptions = options; + return map; + }); + + controller.init(); + + expect(capturedOptions, isNotNull); + expect(capturedOptions!.mapTypeId, gmaps.MapTypeId.SATELLITE); + expect(capturedOptions!.zoomControl, true); + expect(capturedOptions!.gestureHandling, 'auto', + reason: + 'by default the map handles zoom/pan gestures internally'); + }); + + testWidgets('disables gestureHandling with scrollGesturesEnabled false', + (WidgetTester tester) async { + controller = _createController(options: { + 'scrollGesturesEnabled': false, + }); + controller.debugSetOverrides(createMap: (_, options) { + capturedOptions = options; + return map; + }); + + controller.init(); + + expect(capturedOptions, isNotNull); + expect(capturedOptions!.gestureHandling, 'none', + reason: + 'disabling scroll gestures disables all gesture handling'); + }); + + testWidgets('disables gestureHandling with zoomGesturesEnabled false', + (WidgetTester tester) async { + controller = _createController(options: { + 'zoomGesturesEnabled': false, + }); + controller.debugSetOverrides(createMap: (_, options) { + capturedOptions = options; + return map; + }); + + controller.init(); + + expect(capturedOptions, isNotNull); + expect(capturedOptions!.gestureHandling, 'none', + reason: + 'disabling scroll gestures disables all gesture handling'); + }); + + testWidgets('sets initial position when passed', + (WidgetTester tester) async { + controller = _createController( + initialCameraPosition: CameraPosition( + target: LatLng(43.308, -5.6910), + zoom: 12, + bearing: 0, + tilt: 0, + ), + ); + + controller.debugSetOverrides(createMap: (_, options) { + capturedOptions = options; + return map; + }); + + controller.init(); + + expect(capturedOptions, isNotNull); + expect(capturedOptions!.zoom, 12); + expect(capturedOptions!.center, isNotNull); + }); + }); + + group('Traffic Layer', () { + testWidgets('by default is disabled', (WidgetTester tester) async { + controller = _createController(); + controller.init(); + expect(controller.trafficLayer, isNull); + }); + + testWidgets('initializes with traffic layer', + (WidgetTester tester) async { + controller = _createController(options: { + 'trafficEnabled': true, + }); + controller.debugSetOverrides(createMap: (_, __) => map); + controller.init(); + expect(controller.trafficLayer, isNotNull); + }); + }); + }); + + // These are the methods that are delegated to the gmaps.GMap object, that we can mock... + group('Map control methods', () { + late gmaps.GMap map; + + setUp(() { + map = gmaps.GMap( + html.DivElement(), + gmaps.MapOptions() + ..zoom = 10 + ..center = gmaps.LatLng(0, 0), + ); + controller = _createController(); + controller.debugSetOverrides(createMap: (_, __) => map); + controller.init(); + }); + + group('updateRawOptions', () { + testWidgets('can update `options`', (WidgetTester tester) async { + controller.updateRawOptions({ + 'mapType': 2, + }); + + expect(map.mapTypeId, gmaps.MapTypeId.SATELLITE); + }); + + testWidgets('can turn on/off traffic', (WidgetTester tester) async { + expect(controller.trafficLayer, isNull); + + controller.updateRawOptions({ + 'trafficEnabled': true, + }); + + expect(controller.trafficLayer, isNotNull); + + controller.updateRawOptions({ + 'trafficEnabled': false, + }); + + expect(controller.trafficLayer, isNull); + }); + }); + + group('viewport getters', () { + testWidgets('getVisibleRegion', (WidgetTester tester) async { + final gmCenter = map.center!; + final center = + LatLng(gmCenter.lat.toDouble(), gmCenter.lng.toDouble()); + + final bounds = await controller.getVisibleRegion(); + + expect(bounds.contains(center), isTrue, + reason: + 'The computed visible region must contain the center of the created map.'); + }); + + testWidgets('getZoomLevel', (WidgetTester tester) async { + expect(await controller.getZoomLevel(), map.zoom); + }); + }); + + group('moveCamera', () { + testWidgets('newLatLngZoom', (WidgetTester tester) async { + await (controller + .moveCamera(CameraUpdate.newLatLngZoom(LatLng(19, 26), 12))); + + final gmCenter = map.center!; + + expect(map.zoom, 12); + expect(gmCenter.lat, closeTo(19, _acceptableDelta)); + expect(gmCenter.lng, closeTo(26, _acceptableDelta)); + }); + }); + + 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 + }); + }); + + // These are the methods that get forwarded to other controllers, so we just verify calls. + group('Pass-through methods', () { + setUp(() { + controller = _createController(); + }); + + testWidgets('updateCircles', (WidgetTester tester) async { + final mock = MockCirclesController(); + controller.debugSetOverrides(circles: mock); + + final previous = { + Circle(circleId: CircleId('to-be-updated')), + Circle(circleId: CircleId('to-be-removed')), + }; + + final current = { + Circle(circleId: CircleId('to-be-updated'), visible: false), + Circle(circleId: CircleId('to-be-added')), + }; + + controller.updateCircles(CircleUpdates.from(previous, current)); + + verify(mock.removeCircles({ + CircleId('to-be-removed'), + })); + verify(mock.addCircles({ + Circle(circleId: CircleId('to-be-added')), + })); + verify(mock.changeCircles({ + Circle(circleId: CircleId('to-be-updated'), visible: false), + })); + }); + + testWidgets('updateMarkers', (WidgetTester tester) async { + final mock = MockMarkersController(); + controller.debugSetOverrides(markers: mock); + + final previous = { + Marker(markerId: MarkerId('to-be-updated')), + Marker(markerId: MarkerId('to-be-removed')), + }; + + final current = { + Marker(markerId: MarkerId('to-be-updated'), visible: false), + Marker(markerId: MarkerId('to-be-added')), + }; + + controller.updateMarkers(MarkerUpdates.from(previous, current)); + + verify(mock.removeMarkers({ + MarkerId('to-be-removed'), + })); + verify(mock.addMarkers({ + Marker(markerId: MarkerId('to-be-added')), + })); + verify(mock.changeMarkers({ + Marker(markerId: MarkerId('to-be-updated'), visible: false), + })); + }); + + testWidgets('updatePolygons', (WidgetTester tester) async { + final mock = MockPolygonsController(); + controller.debugSetOverrides(polygons: mock); + + final previous = { + Polygon(polygonId: PolygonId('to-be-updated')), + Polygon(polygonId: PolygonId('to-be-removed')), + }; + + final current = { + Polygon(polygonId: PolygonId('to-be-updated'), visible: false), + Polygon(polygonId: PolygonId('to-be-added')), + }; + + controller.updatePolygons(PolygonUpdates.from(previous, current)); + + verify(mock.removePolygons({ + PolygonId('to-be-removed'), + })); + verify(mock.addPolygons({ + Polygon(polygonId: PolygonId('to-be-added')), + })); + verify(mock.changePolygons({ + Polygon(polygonId: PolygonId('to-be-updated'), visible: false), + })); + }); + + testWidgets('updatePolylines', (WidgetTester tester) async { + final mock = MockPolylinesController(); + controller.debugSetOverrides(polylines: mock); + + final previous = { + Polyline(polylineId: PolylineId('to-be-updated')), + Polyline(polylineId: PolylineId('to-be-removed')), + }; + + final current = { + Polyline(polylineId: PolylineId('to-be-updated'), visible: false), + Polyline(polylineId: PolylineId('to-be-added')), + }; + + controller.updatePolylines(PolylineUpdates.from(previous, current)); + + verify(mock.removePolylines({ + PolylineId('to-be-removed'), + })); + verify(mock.addPolylines({ + Polyline(polylineId: PolylineId('to-be-added')), + })); + verify(mock.changePolylines({ + Polyline(polylineId: PolylineId('to-be-updated'), visible: false), + })); + }); + + testWidgets('infoWindow visibility', (WidgetTester tester) async { + final mock = MockMarkersController(); + final markerId = MarkerId('marker-with-infowindow'); + when(mock.isInfoWindowShown(markerId)).thenReturn(true); + controller.debugSetOverrides(markers: mock); + + controller.showInfoWindow(markerId); + + verify(mock.showMarkerInfoWindow(markerId)); + + controller.hideInfoWindow(markerId); + + verify(mock.hideMarkerInfoWindow(markerId)); + + controller.isInfoWindowShown(markerId); + + verify(mock.isInfoWindowShown(markerId)); + }); + }); + }); +} 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 new file mode 100644 index 000000000000..47933285b208 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart @@ -0,0 +1,202 @@ +// Copyright 2013 The Flutter 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 +// in google_maps_flutter_web_integration_tests/integration_test/google_maps_controller_test.dart. +// Do not manually edit this file. + +import 'package:google_maps/src/generated/google_maps_core.js.g.dart' as _i2; +import 'package:google_maps_flutter_platform_interface/src/types/circle.dart' + as _i4; +import 'package:google_maps_flutter_platform_interface/src/types/marker.dart' + as _i7; +import 'package:google_maps_flutter_platform_interface/src/types/polygon.dart' + as _i5; +import 'package:google_maps_flutter_platform_interface/src/types/polyline.dart' + as _i6; +import 'package:google_maps_flutter_web/google_maps_flutter_web.dart' as _i3; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: comment_references +// ignore_for_file: unnecessary_parenthesis + +class _FakeGMap extends _i1.Fake implements _i2.GMap {} + +/// A class which mocks [CirclesController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockCirclesController extends _i1.Mock implements _i3.CirclesController { + @override + Map<_i4.CircleId, _i3.CircleController> get circles => + (super.noSuchMethod(Invocation.getter(#circles), + returnValue: <_i4.CircleId, _i3.CircleController>{}) + as Map<_i4.CircleId, _i3.CircleController>); + @override + _i2.GMap get googleMap => (super.noSuchMethod(Invocation.getter(#googleMap), + returnValue: _FakeGMap()) as _i2.GMap); + @override + set googleMap(_i2.GMap? _googleMap) => + super.noSuchMethod(Invocation.setter(#googleMap, _googleMap), + returnValueForMissingStub: null); + @override + int get mapId => + (super.noSuchMethod(Invocation.getter(#mapId), returnValue: 0) as int); + @override + set mapId(int? _mapId) => + super.noSuchMethod(Invocation.setter(#mapId, _mapId), + returnValueForMissingStub: null); + @override + void addCircles(Set<_i4.Circle>? circlesToAdd) => + super.noSuchMethod(Invocation.method(#addCircles, [circlesToAdd]), + returnValueForMissingStub: null); + @override + void changeCircles(Set<_i4.Circle>? circlesToChange) => + super.noSuchMethod(Invocation.method(#changeCircles, [circlesToChange]), + returnValueForMissingStub: null); + @override + void removeCircles(Set<_i4.CircleId>? circleIdsToRemove) => + super.noSuchMethod(Invocation.method(#removeCircles, [circleIdsToRemove]), + returnValueForMissingStub: null); + @override + void bindToMap(int? mapId, _i2.GMap? googleMap) => + super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]), + returnValueForMissingStub: null); +} + +/// A class which mocks [PolygonsController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPolygonsController extends _i1.Mock + implements _i3.PolygonsController { + @override + Map<_i5.PolygonId, _i3.PolygonController> get polygons => + (super.noSuchMethod(Invocation.getter(#polygons), + returnValue: <_i5.PolygonId, _i3.PolygonController>{}) + as Map<_i5.PolygonId, _i3.PolygonController>); + @override + _i2.GMap get googleMap => (super.noSuchMethod(Invocation.getter(#googleMap), + returnValue: _FakeGMap()) as _i2.GMap); + @override + set googleMap(_i2.GMap? _googleMap) => + super.noSuchMethod(Invocation.setter(#googleMap, _googleMap), + returnValueForMissingStub: null); + @override + int get mapId => + (super.noSuchMethod(Invocation.getter(#mapId), returnValue: 0) as int); + @override + set mapId(int? _mapId) => + super.noSuchMethod(Invocation.setter(#mapId, _mapId), + returnValueForMissingStub: null); + @override + void addPolygons(Set<_i5.Polygon>? polygonsToAdd) => + super.noSuchMethod(Invocation.method(#addPolygons, [polygonsToAdd]), + returnValueForMissingStub: null); + @override + void changePolygons(Set<_i5.Polygon>? polygonsToChange) => + super.noSuchMethod(Invocation.method(#changePolygons, [polygonsToChange]), + returnValueForMissingStub: null); + @override + void removePolygons(Set<_i5.PolygonId>? polygonIdsToRemove) => super + .noSuchMethod(Invocation.method(#removePolygons, [polygonIdsToRemove]), + returnValueForMissingStub: null); + @override + void bindToMap(int? mapId, _i2.GMap? googleMap) => + super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]), + returnValueForMissingStub: null); +} + +/// A class which mocks [PolylinesController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockPolylinesController extends _i1.Mock + implements _i3.PolylinesController { + @override + Map<_i6.PolylineId, _i3.PolylineController> get lines => + (super.noSuchMethod(Invocation.getter(#lines), + returnValue: <_i6.PolylineId, _i3.PolylineController>{}) + as Map<_i6.PolylineId, _i3.PolylineController>); + @override + _i2.GMap get googleMap => (super.noSuchMethod(Invocation.getter(#googleMap), + returnValue: _FakeGMap()) as _i2.GMap); + @override + set googleMap(_i2.GMap? _googleMap) => + super.noSuchMethod(Invocation.setter(#googleMap, _googleMap), + returnValueForMissingStub: null); + @override + int get mapId => + (super.noSuchMethod(Invocation.getter(#mapId), returnValue: 0) as int); + @override + set mapId(int? _mapId) => + super.noSuchMethod(Invocation.setter(#mapId, _mapId), + returnValueForMissingStub: null); + @override + void addPolylines(Set<_i6.Polyline>? polylinesToAdd) => + super.noSuchMethod(Invocation.method(#addPolylines, [polylinesToAdd]), + returnValueForMissingStub: null); + @override + void changePolylines(Set<_i6.Polyline>? polylinesToChange) => super + .noSuchMethod(Invocation.method(#changePolylines, [polylinesToChange]), + returnValueForMissingStub: null); + @override + void removePolylines(Set<_i6.PolylineId>? polylineIdsToRemove) => super + .noSuchMethod(Invocation.method(#removePolylines, [polylineIdsToRemove]), + returnValueForMissingStub: null); + @override + void bindToMap(int? mapId, _i2.GMap? googleMap) => + super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]), + returnValueForMissingStub: null); +} + +/// A class which mocks [MarkersController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockMarkersController extends _i1.Mock implements _i3.MarkersController { + @override + Map<_i7.MarkerId, _i3.MarkerController> get markers => + (super.noSuchMethod(Invocation.getter(#markers), + returnValue: <_i7.MarkerId, _i3.MarkerController>{}) + as Map<_i7.MarkerId, _i3.MarkerController>); + @override + _i2.GMap get googleMap => (super.noSuchMethod(Invocation.getter(#googleMap), + returnValue: _FakeGMap()) as _i2.GMap); + @override + set googleMap(_i2.GMap? _googleMap) => + super.noSuchMethod(Invocation.setter(#googleMap, _googleMap), + returnValueForMissingStub: null); + @override + int get mapId => + (super.noSuchMethod(Invocation.getter(#mapId), returnValue: 0) as int); + @override + set mapId(int? _mapId) => + super.noSuchMethod(Invocation.setter(#mapId, _mapId), + returnValueForMissingStub: null); + @override + void addMarkers(Set<_i7.Marker>? markersToAdd) => + super.noSuchMethod(Invocation.method(#addMarkers, [markersToAdd]), + returnValueForMissingStub: null); + @override + void changeMarkers(Set<_i7.Marker>? markersToChange) => + super.noSuchMethod(Invocation.method(#changeMarkers, [markersToChange]), + returnValueForMissingStub: null); + @override + void removeMarkers(Set<_i7.MarkerId>? markerIdsToRemove) => + super.noSuchMethod(Invocation.method(#removeMarkers, [markerIdsToRemove]), + returnValueForMissingStub: null); + @override + void showMarkerInfoWindow(_i7.MarkerId? markerId) => + super.noSuchMethod(Invocation.method(#showMarkerInfoWindow, [markerId]), + returnValueForMissingStub: null); + @override + void hideMarkerInfoWindow(_i7.MarkerId? markerId) => + super.noSuchMethod(Invocation.method(#hideMarkerInfoWindow, [markerId]), + returnValueForMissingStub: null); + @override + bool isInfoWindowShown(_i7.MarkerId? markerId) => + (super.noSuchMethod(Invocation.method(#isInfoWindowShown, [markerId]), + returnValue: false) as bool); + @override + void bindToMap(int? mapId, _i2.GMap? googleMap) => + super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]), + returnValueForMissingStub: null); +} 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 new file mode 100644 index 000000000000..2de431a5445e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart @@ -0,0 +1,435 @@ +// Copyright 2013 The Flutter Authors. All 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:js_util' show getProperty; + +import 'package:integration_test/integration_test.dart'; +import 'package:flutter/widgets.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:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +import 'google_maps_plugin_test.mocks.dart'; + +@GenerateMocks([], customMocks: [ + MockSpec(returnNullOnMissingStub: true), +]) + +/// Test GoogleMapsPlugin +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('GoogleMapsPlugin', () { + late MockGoogleMapController controller; + late GoogleMapsPlugin plugin; + int? reportedMapId; + + void onPlatformViewCreated(int id) { + reportedMapId = id; + } + + setUp(() { + controller = MockGoogleMapController(); + plugin = GoogleMapsPlugin(); + reportedMapId = null; + }); + + group('init/dispose', () { + group('before buildWidget', () { + testWidgets('init throws assertion', (WidgetTester tester) async { + expect(() => plugin.init(0), throwsAssertionError); + }); + }); + + group('after buildWidget', () { + setUp(() { + plugin.debugSetMapById({0: controller}); + }); + + testWidgets('init initializes controller', (WidgetTester tester) async { + await plugin.init(0); + + verify(controller.init()); + }); + + testWidgets('cannot call methods after dispose', + (WidgetTester tester) async { + plugin.dispose(mapId: 0); + + verify(controller.dispose()); + expect( + () => plugin.init(0), + throwsAssertionError, + reason: 'Method calls should fail after dispose.', + ); + }); + }); + }); + + group('buildView', () { + final testMapId = 33930; + final initialCameraPosition = CameraPosition(target: LatLng(0, 0)); + + testWidgets( + 'returns an HtmlElementView and caches the controller for later', + (WidgetTester tester) async { + final Map cache = {}; + plugin.debugSetMapById(cache); + + final Widget widget = plugin.buildView( + testMapId, + onPlatformViewCreated, + initialCameraPosition: initialCameraPosition, + ); + + expect(widget, isA()); + expect( + (widget as HtmlElementView).viewType, + contains('$testMapId'), + reason: + 'view type should contain the mapId passed when creating the map.', + ); + expect( + reportedMapId, + testMapId, + reason: 'Should call onPlatformViewCreated with the mapId', + ); + expect(cache, contains(testMapId)); + expect( + cache[testMapId], + isNotNull, + reason: 'cached controller cannot be null.', + ); + }); + + testWidgets('returns cached instance if it already exists', + (WidgetTester tester) async { + final expected = HtmlElementView(viewType: 'only-for-testing'); + when(controller.widget).thenReturn(expected); + plugin.debugSetMapById({testMapId: controller}); + + final widget = plugin.buildView( + testMapId, + onPlatformViewCreated, + initialCameraPosition: initialCameraPosition, + ); + + expect(widget, equals(expected)); + expect( + reportedMapId, + isNull, + reason: + 'onPlatformViewCreated should not be called when returning a cached controller', + ); + }); + }); + + group('setMapStyles', () { + String mapStyle = '''[{ + "featureType": "poi.park", + "elementType": "labels.text.fill", + "stylers": [{"color": "#6b9a76"}] + }]'''; + + testWidgets('translates styles for controller', + (WidgetTester tester) async { + plugin.debugSetMapById({0: controller}); + + await plugin.setMapStyle(mapStyle, mapId: 0); + + var captured = + verify(controller.updateRawOptions(captureThat(isMap))).captured[0]; + + expect(captured, contains('styles')); + var styles = captured['styles']; + expect(styles.length, 1); + // Let's peek inside the styles... + var style = styles[0] as gmaps.MapTypeStyle; + expect(style.featureType, 'poi.park'); + expect(style.elementType, 'labels.text.fill'); + expect(style.stylers?.length, 1); + expect(getProperty(style.stylers![0]!, 'color'), '#6b9a76'); + }); + }); + + group('Noop methods:', () { + int mapId = 0; + setUp(() { + plugin.debugSetMapById({mapId: controller}); + }); + // Options + testWidgets('updateTileOverlays', (WidgetTester tester) async { + final update = + plugin.updateTileOverlays(mapId: mapId, newTileOverlays: {}); + expect(update, completion(null)); + }); + testWidgets('updateTileOverlays', (WidgetTester tester) async { + final update = + plugin.clearTileCache(TileOverlayId('any'), mapId: mapId); + expect(update, completion(null)); + }); + }); + + // 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; + setUp(() { + plugin.debugSetMapById({mapId: controller}); + }); + // Options + testWidgets('updateMapOptions', (WidgetTester tester) async { + final expectedMapOptions = {'someOption': 12345}; + + await plugin.updateMapOptions(expectedMapOptions, mapId: mapId); + + verify(controller.updateRawOptions(expectedMapOptions)); + }); + // Geometry + testWidgets('updateMarkers', (WidgetTester tester) async { + final expectedUpdates = MarkerUpdates.from({}, {}); + + await plugin.updateMarkers(expectedUpdates, mapId: mapId); + + verify(controller.updateMarkers(expectedUpdates)); + }); + testWidgets('updatePolygons', (WidgetTester tester) async { + final expectedUpdates = PolygonUpdates.from({}, {}); + + await plugin.updatePolygons(expectedUpdates, mapId: mapId); + + verify(controller.updatePolygons(expectedUpdates)); + }); + testWidgets('updatePolylines', (WidgetTester tester) async { + final expectedUpdates = PolylineUpdates.from({}, {}); + + await plugin.updatePolylines(expectedUpdates, mapId: mapId); + + verify(controller.updatePolylines(expectedUpdates)); + }); + testWidgets('updateCircles', (WidgetTester tester) async { + final expectedUpdates = CircleUpdates.from({}, {}); + + await plugin.updateCircles(expectedUpdates, mapId: mapId); + + verify(controller.updateCircles(expectedUpdates)); + }); + // Camera + testWidgets('animateCamera', (WidgetTester tester) async { + final expectedUpdates = + CameraUpdate.newLatLng(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)); + + await plugin.moveCamera(expectedUpdates, mapId: mapId); + + verify(controller.moveCamera(expectedUpdates)); + }); + + // Viewport + testWidgets('getVisibleRegion', (WidgetTester tester) async { + when(controller.getVisibleRegion()) + .thenAnswer((_) async => LatLngBounds( + northeast: LatLng(47.2359634, -68.0192019), + southwest: LatLng(34.5019594, -120.4974629), + )); + await plugin.getVisibleRegion(mapId: mapId); + + verify(controller.getVisibleRegion()); + }); + + testWidgets('getZoomLevel', (WidgetTester tester) async { + when(controller.getZoomLevel()).thenAnswer((_) async => 10); + await plugin.getZoomLevel(mapId: mapId); + + verify(controller.getZoomLevel()); + }); + + testWidgets('getScreenCoordinate', (WidgetTester tester) async { + when(controller.getScreenCoordinate(any)).thenAnswer( + (_) async => ScreenCoordinate(x: 320, y: 240) // fake return + ); + + final latLng = LatLng(43.3613, -5.8499); + + await plugin.getScreenCoordinate(latLng, mapId: mapId); + + verify(controller.getScreenCoordinate(latLng)); + }); + + testWidgets('getLatLng', (WidgetTester tester) async { + when(controller.getLatLng(any)) + .thenAnswer((_) async => LatLng(43.3613, -5.8499) // fake return + ); + + final coordinates = ScreenCoordinate(x: 19, y: 26); + + await plugin.getLatLng(coordinates, mapId: mapId); + + verify(controller.getLatLng(coordinates)); + }); + + // InfoWindows + testWidgets('showMarkerInfoWindow', (WidgetTester tester) async { + final markerId = MarkerId('testing-123'); + + await plugin.showMarkerInfoWindow(markerId, mapId: mapId); + + verify(controller.showInfoWindow(markerId)); + }); + + testWidgets('hideMarkerInfoWindow', (WidgetTester tester) async { + final markerId = MarkerId('testing-123'); + + await plugin.hideMarkerInfoWindow(markerId, mapId: mapId); + + verify(controller.hideInfoWindow(markerId)); + }); + + testWidgets('isMarkerInfoWindowShown', (WidgetTester tester) async { + when(controller.isInfoWindowShown(any)).thenReturn(true); + + final markerId = MarkerId('testing-123'); + + await plugin.isMarkerInfoWindowShown(markerId, mapId: mapId); + + verify(controller.isInfoWindowShown(markerId)); + }); + }); + + // Verify all event streams are filtered correctly from the main one... + group('Event Streams', () { + int mapId = 0; + late StreamController streamController; + setUp(() { + streamController = StreamController.broadcast(); + when(controller.events) + .thenAnswer((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 { + Timer.run(() { + streamController.add(_OtherMapEvent(mapId)); + streamController.add(event); + streamController.add(_OtherMapEvent(mapId)); + streamController.close(); + }); + + final events = await stream.toList(); + + expect(events.length, 1); + expect(events[0], event); + } + + // Camera events + testWidgets('onCameraMoveStarted', (WidgetTester tester) async { + final event = CameraMoveStartedEvent(mapId); + + final stream = plugin.onCameraMoveStarted(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + testWidgets('onCameraMoveStarted', (WidgetTester tester) async { + final event = CameraMoveEvent( + mapId, + CameraPosition( + target: LatLng(43.3790, -5.8660), + ), + ); + + final stream = plugin.onCameraMove(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + testWidgets('onCameraIdle', (WidgetTester tester) async { + final event = CameraIdleEvent(mapId); + + final stream = plugin.onCameraIdle(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + // Marker events + testWidgets('onMarkerTap', (WidgetTester tester) async { + final event = MarkerTapEvent(mapId, MarkerId('test-123')); + + final stream = plugin.onMarkerTap(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + testWidgets('onInfoWindowTap', (WidgetTester tester) async { + final event = InfoWindowTapEvent(mapId, MarkerId('test-123')); + + final stream = plugin.onInfoWindowTap(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + testWidgets('onMarkerDragEnd', (WidgetTester tester) async { + final event = MarkerDragEndEvent( + mapId, + LatLng(43.3677, -5.8372), + MarkerId('test-123'), + ); + + final stream = plugin.onMarkerDragEnd(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + // Geometry + testWidgets('onPolygonTap', (WidgetTester tester) async { + final event = PolygonTapEvent(mapId, PolygonId('test-123')); + + final stream = plugin.onPolygonTap(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + testWidgets('onPolylineTap', (WidgetTester tester) async { + final event = PolylineTapEvent(mapId, PolylineId('test-123')); + + final stream = plugin.onPolylineTap(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + testWidgets('onCircleTap', (WidgetTester tester) async { + final event = CircleTapEvent(mapId, CircleId('test-123')); + + final 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 stream = plugin.onTap(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + testWidgets('onLongPress', (WidgetTester tester) async { + final event = MapLongPressEvent(mapId, LatLng(43.3608, -5.8425)); + + final stream = plugin.onLongPress(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + }); + }); +} + +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 new file mode 100644 index 000000000000..43150f63ef93 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.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. + +// Mocks generated by Mockito 5.0.2 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 _i5; + +import 'package:google_maps_flutter_platform_interface/src/events/map_event.dart' + as _i6; +import 'package:google_maps_flutter_platform_interface/src/types/camera.dart' + as _i7; +import 'package:google_maps_flutter_platform_interface/src/types/circle_updates.dart' + as _i8; +import 'package:google_maps_flutter_platform_interface/src/types/location.dart' + as _i2; +import 'package:google_maps_flutter_platform_interface/src/types/marker.dart' + as _i12; +import 'package:google_maps_flutter_platform_interface/src/types/marker_updates.dart' + as _i11; +import 'package:google_maps_flutter_platform_interface/src/types/polygon_updates.dart' + as _i9; +import 'package:google_maps_flutter_platform_interface/src/types/polyline_updates.dart' + as _i10; +import 'package:google_maps_flutter_platform_interface/src/types/screen_coordinate.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: comment_references +// ignore_for_file: unnecessary_parenthesis + +class _FakeLatLngBounds extends _i1.Fake implements _i2.LatLngBounds {} + +class _FakeScreenCoordinate extends _i1.Fake implements _i3.ScreenCoordinate {} + +class _FakeLatLng extends _i1.Fake implements _i2.LatLng {} + +/// A class which mocks [GoogleMapController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockGoogleMapController extends _i1.Mock + implements _i4.GoogleMapController { + @override + _i5.Stream<_i6.MapEvent> get events => + (super.noSuchMethod(Invocation.getter(#events), + returnValue: Stream<_i6.MapEvent>.empty()) + as _i5.Stream<_i6.MapEvent>); + @override + void updateRawOptions(Map? optionsUpdate) => + super.noSuchMethod(Invocation.method(#updateRawOptions, [optionsUpdate]), + returnValueForMissingStub: null); + @override + _i5.Future<_i2.LatLngBounds> getVisibleRegion() => + (super.noSuchMethod(Invocation.method(#getVisibleRegion, []), + returnValue: Future.value(_FakeLatLngBounds())) + as _i5.Future<_i2.LatLngBounds>); + @override + _i5.Future<_i3.ScreenCoordinate> getScreenCoordinate(_i2.LatLng? latLng) => + (super.noSuchMethod(Invocation.method(#getScreenCoordinate, [latLng]), + returnValue: Future.value(_FakeScreenCoordinate())) + as _i5.Future<_i3.ScreenCoordinate>); + @override + _i5.Future<_i2.LatLng> getLatLng(_i3.ScreenCoordinate? screenCoordinate) => + (super.noSuchMethod(Invocation.method(#getLatLng, [screenCoordinate]), + returnValue: Future.value(_FakeLatLng())) as _i5.Future<_i2.LatLng>); + @override + _i5.Future moveCamera(_i7.CameraUpdate? cameraUpdate) => + (super.noSuchMethod(Invocation.method(#moveCamera, [cameraUpdate]), + returnValue: Future.value(null), + returnValueForMissingStub: Future.value()) as _i5.Future); + @override + _i5.Future getZoomLevel() => + (super.noSuchMethod(Invocation.method(#getZoomLevel, []), + returnValue: Future.value(0.0)) as _i5.Future); + @override + void updateCircles(_i8.CircleUpdates? updates) => + super.noSuchMethod(Invocation.method(#updateCircles, [updates]), + returnValueForMissingStub: null); + @override + void updatePolygons(_i9.PolygonUpdates? updates) => + super.noSuchMethod(Invocation.method(#updatePolygons, [updates]), + returnValueForMissingStub: null); + @override + void updatePolylines(_i10.PolylineUpdates? updates) => + super.noSuchMethod(Invocation.method(#updatePolylines, [updates]), + returnValueForMissingStub: null); + @override + void updateMarkers(_i11.MarkerUpdates? updates) => + super.noSuchMethod(Invocation.method(#updateMarkers, [updates]), + returnValueForMissingStub: null); + @override + void showInfoWindow(_i12.MarkerId? markerId) => + super.noSuchMethod(Invocation.method(#showInfoWindow, [markerId]), + returnValueForMissingStub: null); + @override + void hideInfoWindow(_i12.MarkerId? markerId) => + super.noSuchMethod(Invocation.method(#hideInfoWindow, [markerId]), + returnValueForMissingStub: null); + @override + bool isInfoWindowShown(_i12.MarkerId? markerId) => + (super.noSuchMethod(Invocation.method(#isInfoWindowShown, [markerId]), + returnValue: false) as bool); +} 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 new file mode 100644 index 000000000000..2bfa27b73a77 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart @@ -0,0 +1,158 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:html' as html; + +import 'package:integration_test/integration_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'; + +/// Test Markers +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // Since onTap/DragEnd events happen asynchronously, we need to store when the event + // is fired. We use a completer so the test can wait for the future to be completed. + late Completer _methodCalledCompleter; + + /// This is the future value of the [_methodCalledCompleter]. Reinitialized + /// in the [setUp] method, and completed (as `true`) by [onTap] and [onDragEnd] + /// when those methods are called from the MarkerController. + late Future methodCalled; + + void onTap() { + _methodCalledCompleter.complete(true); + } + + void onDragEnd(gmaps.LatLng _) { + _methodCalledCompleter.complete(true); + } + + setUp(() { + _methodCalledCompleter = Completer(); + methodCalled = _methodCalledCompleter.future; + }); + + group('MarkerController', () { + late gmaps.Marker marker; + + setUp(() { + marker = gmaps.Marker(); + }); + + testWidgets('onTap gets called', (WidgetTester tester) async { + MarkerController(marker: marker, onTap: onTap); + + // Trigger a click event... + gmaps.Event.trigger(marker, 'click', [gmaps.MapMouseEvent()]); + + // The event handling is now truly async. Wait for it... + 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)]); + + expect(await methodCalled, isTrue); + }); + + testWidgets('update', (WidgetTester tester) async { + final controller = MarkerController(marker: marker); + final options = gmaps.MarkerOptions()..draggable = true; + + expect(marker.draggable, isNull); + + controller.update(options); + + expect(marker.draggable, isTrue); + }); + + testWidgets('infoWindow null, showInfoWindow.', + (WidgetTester tester) async { + final controller = MarkerController(marker: marker); + + controller.showInfoWindow(); + + expect(controller.infoWindowShown, isFalse); + }); + + testWidgets('showInfoWindow', (WidgetTester tester) async { + final infoWindow = gmaps.InfoWindow(); + final map = gmaps.GMap(html.DivElement()); + marker.set('map', map); + final controller = + MarkerController(marker: marker, infoWindow: infoWindow); + + controller.showInfoWindow(); + + expect(infoWindow.get('map'), map); + expect(controller.infoWindowShown, isTrue); + }); + + testWidgets('hideInfoWindow', (WidgetTester tester) async { + final infoWindow = gmaps.InfoWindow(); + final map = gmaps.GMap(html.DivElement()); + marker.set('map', map); + final controller = + MarkerController(marker: marker, infoWindow: infoWindow); + + controller.hideInfoWindow(); + + expect(infoWindow.get('map'), isNull); + expect(controller.infoWindowShown, isFalse); + }); + + group('remove', () { + late MarkerController controller; + + setUp(() { + final infoWindow = gmaps.InfoWindow(); + final map = gmaps.GMap(html.DivElement()); + marker.set('map', map); + controller = MarkerController(marker: marker, infoWindow: infoWindow); + }); + + testWidgets('drops gmaps instance', (WidgetTester tester) async { + controller.remove(); + + expect(controller.marker, isNull); + }); + + testWidgets('cannot call update after remove', + (WidgetTester tester) async { + final options = gmaps.MarkerOptions()..draggable = true; + + controller.remove(); + + expect(() { + controller.update(options); + }, throwsAssertionError); + }); + + testWidgets('cannot call showInfoWindow after remove', + (WidgetTester tester) async { + controller.remove(); + + expect(() { + controller.showInfoWindow(); + }, throwsAssertionError); + }); + + testWidgets('cannot call hideInfoWindow after remove', + (WidgetTester tester) async { + controller.remove(); + + expect(() { + controller.hideInfoWindow(); + }, throwsAssertionError); + }); + }); + }); +} 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 new file mode 100644 index 000000000000..6f2bf610f77d --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/markers_test.dart @@ -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 'dart:async'; +import 'dart:convert'; +import 'dart:html' as html; +import 'dart:js_util' show getProperty; + +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:http/http.dart' as http; +import 'package:integration_test/integration_test.dart'; + +import 'resources/icon_image_base64.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('MarkersController', () { + late StreamController events; + late MarkersController controller; + late gmaps.GMap map; + + setUp(() { + 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')), + }; + + 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')))); + }); + + testWidgets('changeMarkers', (WidgetTester tester) async { + final markers = { + Marker(markerId: MarkerId('1')), + }; + controller.addMarkers(markers); + + expect(controller.markers[MarkerId('1')]?.marker?.draggable, isFalse); + + // Update the marker with radius 10 + final updatedMarkers = { + Marker(markerId: MarkerId('1'), draggable: true), + }; + controller.changeMarkers(updatedMarkers); + + expect(controller.markers.length, 1); + expect(controller.markers[MarkerId('1')]?.marker?.draggable, isTrue); + }); + + testWidgets('removeMarkers', (WidgetTester tester) async { + final markers = { + Marker(markerId: MarkerId('1')), + Marker(markerId: MarkerId('2')), + Marker(markerId: MarkerId('3')), + }; + + controller.addMarkers(markers); + + expect(controller.markers.length, 3); + + // Remove some markers... + final markerIdsToRemove = { + MarkerId('1'), + 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')))); + }); + + testWidgets('InfoWindow show/hide', (WidgetTester tester) async { + final markers = { + Marker( + markerId: MarkerId('1'), + infoWindow: InfoWindow(title: "Title", snippet: "Snippet"), + ), + }; + + controller.addMarkers(markers); + + expect(controller.markers[MarkerId('1')]?.infoWindowShown, isFalse); + + controller.showMarkerInfoWindow(MarkerId('1')); + + expect(controller.markers[MarkerId('1')]?.infoWindowShown, isTrue); + + controller.hideMarkerInfoWindow(MarkerId('1')); + + expect(controller.markers[MarkerId('1')]?.infoWindowShown, isFalse); + }); + + // https://github.com/flutter/flutter/issues/67380 + testWidgets('only single InfoWindow is visible', + (WidgetTester tester) async { + final markers = { + Marker( + markerId: MarkerId('1'), + infoWindow: InfoWindow(title: "Title", snippet: "Snippet"), + ), + Marker( + markerId: MarkerId('2'), + infoWindow: InfoWindow(title: "Title", snippet: "Snippet"), + ), + }; + controller.addMarkers(markers); + + expect(controller.markers[MarkerId('1')]?.infoWindowShown, isFalse); + expect(controller.markers[MarkerId('2')]?.infoWindowShown, isFalse); + + controller.showMarkerInfoWindow(MarkerId('1')); + + expect(controller.markers[MarkerId('1')]?.infoWindowShown, isTrue); + expect(controller.markers[MarkerId('2')]?.infoWindowShown, isFalse); + + controller.showMarkerInfoWindow(MarkerId('2')); + + expect(controller.markers[MarkerId('1')]?.infoWindowShown, isFalse); + expect(controller.markers[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 = { + Marker( + markerId: MarkerId('1'), icon: BitmapDescriptor.fromBytes(bytes)), + }; + + controller.addMarkers(markers); + + expect(controller.markers.length, 1); + expect(controller.markers[MarkerId('1')]?.marker?.icon, isNotNull); + + final blobUrl = getProperty( + controller.markers[MarkerId('1')]!.marker!.icon!, + 'url', + ); + + expect(blobUrl, startsWith('blob:')); + + final response = await http.get(Uri.parse(blobUrl)); + + expect(response.bodyBytes, bytes, + reason: + 'Bytes from the Icon blob must match bytes used to create Marker'); + }); + + // https://github.com/flutter/flutter/issues/67854 + testWidgets('InfoWindow snippet can have links', + (WidgetTester tester) async { + final markers = { + Marker( + markerId: MarkerId('1'), + infoWindow: InfoWindow( + title: 'title for test', + snippet: 'Go to Google >>>', + ), + ), + }; + + 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')); + expect( + content.innerHtml, + contains( + 'Go to Google >>>')); + }); + + // https://github.com/flutter/flutter/issues/67289 + testWidgets('InfoWindow content is clickable', (WidgetTester tester) async { + final markers = { + Marker( + markerId: MarkerId('1'), + infoWindow: InfoWindow( + title: 'title for test', + snippet: 'some snippet', + ), + ), + }; + + controller.addMarkers(markers); + + expect(controller.markers.length, 1); + final content = controller.markers[MarkerId('1')]?.infoWindow?.content + as html.HtmlElement; + + content.click(); + + final event = await events.stream.first; + + expect(event, isA()); + expect((event as InfoWindowTapEvent).value, equals(MarkerId('1'))); + }); + }); +} 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 new file mode 100644 index 000000000000..6010f0107031 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/resources/icon_image_base64.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. + +final iconImageBase64 = + 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAIRlWElmTU' + '0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIA' + 'AIdpAAQAAAABAAAAWgAAAAAAAABIAAAAAQAAAEgAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQ' + 'AAABCgAwAEAAAAAQAAABAAAAAAx28c8QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlpVFh0WE1M' + 'OmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIH' + 'g6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8v' + 'd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcm' + 'lwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFk' + 'b2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk' + '9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6' + 'eG1wbWV0YT4KTMInWQAAAplJREFUOBF1k01ME1EQx2fe7tIPoGgTE6AJgQQSPaiH9oAtkFbsgX' + 'jygFcT0XjSkxcTDxtPJh6MR28ePMHBBA8cNLSIony0oBhEMVETP058tE132+7uG3cW24DAXN57' + '2fn9/zPz3iIcEdEl0nIxtNLr1IlVeoMadkubKmoL+u2SzAV8IjV5Ekt4GN+A8+VOUPwLarOI2G' + 'Vpqq0i4JQorwQxPtWHVZ1IKP8LNGDXGaSyqARFxDGo7MJBy4XVf3AyQ+qTHnTEXoF9cFUy3OkY' + '0oWxmWFtD5xNoc1sQ6AOn1+hCNTkkhKow8KFZV77tVs2O9dhFvBm0IA/U0RhZ7/ocEx23oUDlh' + 'h8HkNjZIN8Lb3gOU8gOp7AKJHCB2/aNZkTftHumNzzbtl2CBPZHqxw8mHhVZBeoz6w5DvhE2FZ' + 'lQYPjKdd2/qRyKZ6KsPv7TEk7EYEk0A0EUmJduHRy1i4oLKqgmC59ZggAdwrC9pFuWy1iUT2rA' + 'uv0h2UdNtNqxCBBkgqorjOMOgksN7CxQ90vEb00U3c3LIwyo9o8FXxQVNr8Coqyk+S5EPBXnjt' + 'xRmc4TegI7qWbvBkeeUbGMnTCd4nZnYeDOWIEtlC6cKK/JJepY3hZSvN33jovO6L0XFqPKqBTO' + 'FuapUoPr1lxDM7cmC2TAOz25cYSGa++feBew/cjpc0V+mNT29/HZp3KDFTNLvuTRPEHy5065lj' + 'Xn4y41XM+wP/AlcycRmdc3MUhvLm/J/ceu/3qUVT62oP2EZpjSylHybHSpDUVcjq9gEBVo0+Xt' + 'JyN2IWRO+3QUforRoKnZLVsglaMECW+YmMSj9M3SrC6Lg71CMiqWfUrJ6ywzefhnZ+G69BaKdB' + 'WhXQAn6wzDUpfUPw7MrmX/WhbfmKblw+AAAAAElFTkSuQmCC'; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shape_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shape_test.dart new file mode 100644 index 000000000000..547aaec6dc0a --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shape_test.dart @@ -0,0 +1,196 @@ +// Copyright 2013 The Flutter Authors. All 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:integration_test/integration_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'; + +/// Test Shapes (Circle, Polygon, Polyline) +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // Since onTap events happen asynchronously, we need to store when the event + // is fired. We use a completer so the test can wait for the future to be completed. + late Completer _methodCalledCompleter; + + /// This is the future value of the [_methodCalledCompleter]. Reinitialized + /// in the [setUp] method, and completed (as `true`) by [onTap], when it gets + /// called by the corresponding Shape Controller. + late Future methodCalled; + + void onTap() { + _methodCalledCompleter.complete(true); + } + + setUp(() { + _methodCalledCompleter = Completer(); + methodCalled = _methodCalledCompleter.future; + }); + + group('CircleController', () { + late gmaps.Circle circle; + + setUp(() { + circle = gmaps.Circle(); + }); + + testWidgets('onTap gets called', (WidgetTester tester) async { + CircleController(circle: circle, consumeTapEvents: true, onTap: onTap); + + // Trigger a click event... + 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; + + expect(circle.draggable, isNull); + + controller.update(options); + + expect(circle.draggable, isTrue); + }); + + group('remove', () { + late CircleController controller; + + setUp(() { + controller = CircleController(circle: circle); + }); + + testWidgets('drops gmaps instance', (WidgetTester tester) async { + controller.remove(); + + expect(controller.circle, isNull); + }); + + testWidgets('cannot call update after remove', + (WidgetTester tester) async { + final options = gmaps.CircleOptions()..draggable = true; + + controller.remove(); + + expect(() { + controller.update(options); + }, throwsAssertionError); + }); + }); + }); + + group('PolygonController', () { + late gmaps.Polygon polygon; + + setUp(() { + polygon = gmaps.Polygon(); + }); + + testWidgets('onTap gets called', (WidgetTester tester) async { + PolygonController(polygon: polygon, consumeTapEvents: true, onTap: onTap); + + // Trigger a click event... + 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; + + expect(polygon.draggable, isNull); + + controller.update(options); + + expect(polygon.draggable, isTrue); + }); + + group('remove', () { + late PolygonController controller; + + setUp(() { + controller = PolygonController(polygon: polygon); + }); + + testWidgets('drops gmaps instance', (WidgetTester tester) async { + controller.remove(); + + expect(controller.polygon, isNull); + }); + + testWidgets('cannot call update after remove', + (WidgetTester tester) async { + final options = gmaps.PolygonOptions()..draggable = true; + + controller.remove(); + + expect(() { + controller.update(options); + }, throwsAssertionError); + }); + }); + }); + + group('PolylineController', () { + late gmaps.Polyline polyline; + + setUp(() { + polyline = gmaps.Polyline(); + }); + + testWidgets('onTap gets called', (WidgetTester tester) async { + PolylineController( + polyline: polyline, consumeTapEvents: true, onTap: onTap); + + // Trigger a click event... + 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; + + expect(polyline.draggable, isNull); + + controller.update(options); + + expect(polyline.draggable, isTrue); + }); + + group('remove', () { + late PolylineController controller; + + setUp(() { + controller = PolylineController(polyline: polyline); + }); + + testWidgets('drops gmaps instance', (WidgetTester tester) async { + controller.remove(); + + expect(controller.line, isNull); + }); + + testWidgets('cannot call update after remove', + (WidgetTester tester) async { + final options = gmaps.PolylineOptions()..draggable = true; + + controller.remove(); + + expect(() { + controller.update(options); + }, throwsAssertionError); + }); + }); + }); +} 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 new file mode 100644 index 000000000000..80b4e0823bb5 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/shapes_test.dart @@ -0,0 +1,368 @@ +// Copyright 2013 The Flutter Authors. All 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 'dart:html' as html; + +import 'package:integration_test/integration_test.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; +import 'package:google_maps/google_maps.dart' as gmaps; +import 'package:google_maps/google_maps_geometry.dart' as geometry; +import 'package:flutter_test/flutter_test.dart'; + +// This value is used when comparing the results of +// converting from a byte value to a double between 0 and 1. +// (For Color opacity values, for example) +const _acceptableDelta = 0.01; + +/// Test Shapes (Circle, Polygon, Polyline) +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + late gmaps.GMap map; + + setUp(() { + map = gmaps.GMap(html.DivElement()); + }); + + group('CirclesController', () { + late StreamController events; + late CirclesController controller; + + setUp(() { + 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')), + }; + + 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')))); + }); + + testWidgets('changeCircles', (WidgetTester tester) async { + final circles = { + Circle(circleId: CircleId('1')), + }; + controller.addCircles(circles); + + expect(controller.circles[CircleId('1')]?.circle?.visible, isTrue); + + final updatedCircles = { + Circle(circleId: CircleId('1'), visible: false), + }; + controller.changeCircles(updatedCircles); + + expect(controller.circles.length, 1); + expect(controller.circles[CircleId('1')]?.circle?.visible, isFalse); + }); + + testWidgets('removeCircles', (WidgetTester tester) async { + final circles = { + Circle(circleId: CircleId('1')), + Circle(circleId: CircleId('2')), + Circle(circleId: CircleId('3')), + }; + + controller.addCircles(circles); + + expect(controller.circles.length, 3); + + // Remove some circles... + final circleIdsToRemove = { + CircleId('1'), + 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')))); + }); + + testWidgets('Converts colors to CSS', (WidgetTester tester) async { + final circles = { + Circle( + circleId: CircleId('1'), + fillColor: Color(0x7FFABADA), + strokeColor: Color(0xFFC0FFEE), + ), + }; + + controller.addCircles(circles); + + final circle = controller.circles.values.first.circle!; + + expect(circle.get('fillColor'), '#fabada'); + expect(circle.get('fillOpacity'), closeTo(0.5, _acceptableDelta)); + expect(circle.get('strokeColor'), '#c0ffee'); + expect(circle.get('strokeOpacity'), closeTo(1, _acceptableDelta)); + }); + }); + + group('PolygonsController', () { + late StreamController events; + late PolygonsController controller; + + setUp(() { + 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')), + }; + + 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')))); + }); + + testWidgets('changePolygons', (WidgetTester tester) async { + final polygons = { + Polygon(polygonId: PolygonId('1')), + }; + controller.addPolygons(polygons); + + expect(controller.polygons[PolygonId('1')]?.polygon?.visible, isTrue); + + // Update the polygon + final updatedPolygons = { + Polygon(polygonId: PolygonId('1'), visible: false), + }; + controller.changePolygons(updatedPolygons); + + expect(controller.polygons.length, 1); + expect(controller.polygons[PolygonId('1')]?.polygon?.visible, isFalse); + }); + + testWidgets('removePolygons', (WidgetTester tester) async { + final polygons = { + Polygon(polygonId: PolygonId('1')), + Polygon(polygonId: PolygonId('2')), + Polygon(polygonId: PolygonId('3')), + }; + + controller.addPolygons(polygons); + + expect(controller.polygons.length, 3); + + // Remove some polygons... + final polygonIdsToRemove = { + PolygonId('1'), + 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')))); + }); + + testWidgets('Converts colors to CSS', (WidgetTester tester) async { + final polygons = { + Polygon( + polygonId: PolygonId('1'), + fillColor: Color(0x7FFABADA), + strokeColor: Color(0xFFC0FFEE), + ), + }; + + controller.addPolygons(polygons); + + final polygon = controller.polygons.values.first.polygon!; + + expect(polygon.get('fillColor'), '#fabada'); + expect(polygon.get('fillOpacity'), closeTo(0.5, _acceptableDelta)); + expect(polygon.get('strokeColor'), '#c0ffee'); + expect(polygon.get('strokeOpacity'), closeTo(1, _acceptableDelta)); + }); + + testWidgets('Handle Polygons with holes', (WidgetTester tester) async { + final polygons = { + Polygon( + polygonId: PolygonId('BermudaTriangle'), + points: [ + LatLng(25.774, -80.19), + LatLng(18.466, -66.118), + LatLng(32.321, -64.757), + ], + holes: [ + [ + LatLng(28.745, -70.579), + LatLng(29.57, -67.514), + LatLng(27.339, -66.668), + ], + ], + ), + }; + + controller.addPolygons(polygons); + + expect(controller.polygons.length, 1); + expect(controller.polygons, contains(PolygonId('BermudaTriangle'))); + expect(controller.polygons, isNot(contains(PolygonId('66')))); + }); + + testWidgets('Polygon with hole has a hole', (WidgetTester tester) async { + final polygons = { + Polygon( + polygonId: PolygonId('BermudaTriangle'), + points: [ + LatLng(25.774, -80.19), + LatLng(18.466, -66.118), + LatLng(32.321, -64.757), + ], + holes: [ + [ + LatLng(28.745, -70.579), + LatLng(29.57, -67.514), + LatLng(27.339, -66.668), + ], + ], + ), + }; + + controller.addPolygons(polygons); + + final polygon = controller.polygons.values.first.polygon; + final pointInHole = gmaps.LatLng(28.632, -68.401); + + expect(geometry.Poly.containsLocation(pointInHole, polygon), false); + }); + + testWidgets('Hole Path gets reversed to display correctly', + (WidgetTester tester) async { + final polygons = { + Polygon( + polygonId: PolygonId('BermudaTriangle'), + points: [ + LatLng(25.774, -80.19), + LatLng(18.466, -66.118), + LatLng(32.321, -64.757), + ], + holes: [ + [ + LatLng(27.339, -66.668), + LatLng(29.57, -67.514), + LatLng(28.745, -70.579), + ], + ], + ), + }; + + controller.addPolygons(polygons); + + final 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); + expect(paths.getAt(1)?.getAt(2)?.lat, 27.339); + }); + }); + + group('PolylinesController', () { + late StreamController events; + late PolylinesController controller; + + setUp(() { + 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')), + }; + + 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')))); + }); + + testWidgets('changePolylines', (WidgetTester tester) async { + final polylines = { + Polyline(polylineId: PolylineId('1')), + }; + controller.addPolylines(polylines); + + expect(controller.lines[PolylineId('1')]?.line?.visible, isTrue); + + final updatedPolylines = { + Polyline(polylineId: PolylineId('1'), visible: false), + }; + controller.changePolylines(updatedPolylines); + + expect(controller.lines.length, 1); + expect(controller.lines[PolylineId('1')]?.line?.visible, isFalse); + }); + + testWidgets('removePolylines', (WidgetTester tester) async { + final polylines = { + Polyline(polylineId: PolylineId('1')), + Polyline(polylineId: PolylineId('2')), + Polyline(polylineId: PolylineId('3')), + }; + + controller.addPolylines(polylines); + + expect(controller.lines.length, 3); + + // Remove some polylines... + final polylineIdsToRemove = { + PolylineId('1'), + 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')))); + }); + + testWidgets('Converts colors to CSS', (WidgetTester tester) async { + final lines = { + Polyline( + polylineId: PolylineId('1'), + color: Color(0x7FFABADA), + ), + }; + + controller.addPolylines(lines); + + final 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/test/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/lib/main.dart similarity index 100% rename from packages/google_maps_flutter/google_maps_flutter_web/test/lib/main.dart rename to packages/google_maps_flutter/google_maps_flutter_web/example/lib/main.dart 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 new file mode 100644 index 000000000000..b0ac9910afc9 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml @@ -0,0 +1,25 @@ +name: google_maps_flutter_web_integration_tests +publish_to: none + +# Tests require flutter beta or greater to run. +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.1.0" + +dependencies: + google_maps_flutter_web: + path: ../ + flutter: + sdk: flutter + +dev_dependencies: + build_runner: ^1.11.0 + google_maps: ^5.1.0 + http: ^0.13.0 + mockito: ^5.0.0 + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/regen_mocks.sh b/packages/google_maps_flutter/google_maps_flutter_web/example/regen_mocks.sh new file mode 100755 index 000000000000..78bcdc0f9e28 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/regen_mocks.sh @@ -0,0 +1,10 @@ +#!/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. + +flutter pub get + +echo "(Re)generating mocks." + +flutter pub run build_runner build --delete-conflicting-outputs diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/run_test.sh b/packages/google_maps_flutter/google_maps_flutter_web/example/run_test.sh new file mode 100755 index 000000000000..fcac5f600acb --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/run_test.sh @@ -0,0 +1,24 @@ +#!/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." + + ./regen_mocks.sh + + if [ $# -eq 0 ]; then + echo "No target specified, running all tests..." + find integration_test/ -iname *_test.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target='{}' + 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/google_maps_flutter/google_maps_flutter_web/example/test_driver/integration_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_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/google_maps_flutter/google_maps_flutter_web/example/web/index.html b/packages/google_maps_flutter/google_maps_flutter_web/example/web/index.html new file mode 100644 index 000000000000..3121d189b913 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/web/index.html @@ -0,0 +1,14 @@ + + + + + Browser Tests + + + + + + + diff --git a/packages/google_maps_flutter/google_maps_flutter_web/ios/google_maps_flutter_web.podspec b/packages/google_maps_flutter/google_maps_flutter_web/ios/google_maps_flutter_web.podspec deleted file mode 100644 index 18db6ced01b6..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter_web/ios/google_maps_flutter_web.podspec +++ /dev/null @@ -1,23 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint google_maps_flutter_web.podspec' to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'google_maps_flutter_web' - s.version = '0.1.0' - s.summary = 'No-op implementation of google maps flutter web plugin to avoid build issues on iOS' - s.description = <<-DESC -temp fake google_maps_flutter_web plugin - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter_web' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.dependency 'Flutter' - s.platform = :ios, '8.0' - - # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } - s.swift_version = '5.0' -end diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart index cf133fb9e533..6dc2dab572a6 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -6,7 +6,8 @@ library google_maps_flutter_web; import 'dart:async'; import 'dart:html'; -import 'dart:ui' as ui; +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'; 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 96f9be7aa001..65057d8c869e 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -6,15 +6,15 @@ part of google_maps_flutter_web; /// The `CircleController` class wraps a [gmaps.Circle] and its `onTap` behavior. class CircleController { - gmaps.Circle _circle; + gmaps.Circle? _circle; final bool _consumeTapEvents; /// Creates a `CircleController`, which wraps a [gmaps.Circle] object and its `onTap` behavior. CircleController({ - @required gmaps.Circle circle, + required gmaps.Circle circle, bool consumeTapEvents = false, - ui.VoidCallback onTap, + ui.VoidCallback? onTap, }) : _circle = circle, _consumeTapEvents = consumeTapEvents { if (onTap != null) { @@ -26,21 +26,26 @@ class CircleController { /// Returns the wrapped [gmaps.Circle]. Only used for testing. @visibleForTesting - gmaps.Circle get circle => _circle; + gmaps.Circle? get circle => _circle; /// Returns `true` if this Controller will use its own `onTap` handler to consume events. bool get consumeTapEvents => _consumeTapEvents; /// Updates the options of the wrapped [gmaps.Circle] object. + /// + /// This cannot be called after [remove]. void update(gmaps.CircleOptions options) { - _circle.options = options; + assert(_circle != null, 'Cannot `update` Circle after calling `remove`.'); + _circle!.options = options; } /// Disposes of the currently wrapped [gmaps.Circle]. void remove() { - _circle.visible = false; - _circle.radius = 0; - _circle.map = null; - _circle = null; + if (_circle != null) { + _circle!.visible = false; + _circle!.radius = 0; + _circle!.map = null; + _circle = null; + } } } 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 c7c33ed1811f..ae8faa038ea6 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -14,7 +14,7 @@ class CirclesController extends GeometryController { /// Initialize the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. CirclesController({ - @required StreamController stream, + required StreamController stream, }) : _streamController = stream, _circleIdToController = Map(); @@ -26,7 +26,7 @@ class CirclesController extends GeometryController { /// /// Wraps each [Circle] into its corresponding [CircleController]. void addCircles(Set circlesToAdd) { - circlesToAdd?.forEach((circle) { + circlesToAdd.forEach((circle) { _addCircle(circle); }); } @@ -50,20 +50,21 @@ class CirclesController extends GeometryController { /// Updates a set of [Circle] objects with new options. void changeCircles(Set circlesToChange) { - circlesToChange?.forEach((circleToChange) { + circlesToChange.forEach((circleToChange) { _changeCircle(circleToChange); }); } void _changeCircle(Circle circle) { - final circleController = _circleIdToController[circle?.circleId]; + final 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]; + circleIdsToRemove.forEach((circleId) { + final CircleController? circleController = + _circleIdToController[circleId]; circleController?.remove(); _circleIdToController.remove(circleId); }); 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 0ec00871e676..2e71c795ff0e 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 @@ -1,18 +1,17 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. part of google_maps_flutter_web; -final _nullLatLng = LatLng(0, 0); -final _nullLatLngBounds = LatLngBounds( - northeast: _nullLatLng, - southwest: _nullLatLng, -); +// Default values for when the gmaps objects return null/undefined values. +final _nullGmapsLatLng = gmaps.LatLng(0, 0); +final _nullGmapsLatLngBounds = + gmaps.LatLngBounds(_nullGmapsLatLng, _nullGmapsLatLng); // Defaults taken from the Google Maps Platform SDK documentation. -final _defaultStrokeColor = Colors.black.value; -final _defaultFillColor = Colors.transparent.value; +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. @@ -24,6 +23,22 @@ final _mapTypeToMapTypeId = { 4: gmaps.MapTypeId.HYBRID, }; +// 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); +} + +// Extracts the opacity from a [Color]. +double _getCssOpacity(Color color) { + if (color == null) { + return _defaultCssOpacity; + } + return color.opacity; +} + // Converts options from the plugin into gmaps.MapOptions that can be used by the JS SDK. // The following options are not handled here, for various reasons: // The following are not available in web, because the map doesn't rotate there: @@ -41,41 +56,39 @@ final _mapTypeToMapTypeId = { // 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) { - Map optionsUpdate = rawOptions['options'] ?? {}; - gmaps.MapOptions options = gmaps.MapOptions(); - if (_mapTypeToMapTypeId.containsKey(optionsUpdate['mapType'])) { - options.mapTypeId = _mapTypeToMapTypeId[optionsUpdate['mapType']]; + if (_mapTypeToMapTypeId.containsKey(rawOptions['mapType'])) { + options.mapTypeId = _mapTypeToMapTypeId[rawOptions['mapType']]; } - if (optionsUpdate['minMaxZoomPreference'] != null) { + if (rawOptions['minMaxZoomPreference'] != null) { options - ..minZoom = optionsUpdate['minMaxZoomPreference'][0] - ..maxZoom = optionsUpdate['minMaxZoomPreference'][1]; + ..minZoom = rawOptions['minMaxZoomPreference'][0] + ..maxZoom = rawOptions['minMaxZoomPreference'][1]; } - if (optionsUpdate['cameraTargetBounds'] != null) { + if (rawOptions['cameraTargetBounds'] != null) { // Needs gmaps.MapOptions.restriction and gmaps.MapRestriction // see: https://developers.google.com/maps/documentation/javascript/reference/map#MapOptions.restriction } - if (optionsUpdate['zoomControlsEnabled'] != null) { - options.zoomControl = optionsUpdate['zoomControlsEnabled']; + if (rawOptions['zoomControlsEnabled'] != null) { + options.zoomControl = rawOptions['zoomControlsEnabled']; } - if (optionsUpdate['styles'] != null) { - options.styles = optionsUpdate['styles']; + if (rawOptions['styles'] != null) { + options.styles = rawOptions['styles']; } - if (optionsUpdate['scrollGesturesEnabled'] == false || - optionsUpdate['zoomGesturesEnabled'] == false) { + if (rawOptions['scrollGesturesEnabled'] == false || + rawOptions['zoomGesturesEnabled'] == false) { options.gestureHandling = 'none'; } else { options.gestureHandling = 'auto'; } - // These don't have any optionUpdate entry, but they seem to be off in the native maps. + // These don't have any rawOptions entry, but they seem to be off in the native maps. options.mapTypeControl = false; options.fullscreenControl = false; options.streetViewControl = false; @@ -84,102 +97,23 @@ gmaps.MapOptions _rawOptionsToGmapsOptions(Map rawOptions) { } gmaps.MapOptions _applyInitialPosition( - Map rawOptions, + CameraPosition initialPosition, gmaps.MapOptions options, ) { // Adjust the initial position, if passed... - Map initialPosition = rawOptions['initialCameraPosition']; if (initialPosition != null) { - final position = CameraPosition.fromMap(initialPosition); - options.zoom = position.zoom; - options.center = - gmaps.LatLng(position.target.latitude, position.target.longitude); + options.zoom = initialPosition.zoom; + options.center = gmaps.LatLng( + initialPosition.target.latitude, initialPosition.target.longitude); } return options; } // Extracts the status of the traffic layer from the rawOptions map. bool _isTrafficLayerEnabled(Map rawOptions) { - if (rawOptions['options'] == null) { - return false; - } - return rawOptions['options']['trafficEnabled'] ?? false; + return rawOptions['trafficEnabled'] ?? false; } -// Coverts the incoming JSON object into a List of MapTypeStyler objects. -List _parseStylers(List stylerJsons) { - return stylerJsons?.map((styler) { - return gmaps.MapTypeStyler() - ..color = styler['color'] - ..gamma = styler['gamma'] - ..hue = styler['hue'] - ..invertLightness = styler['invertLightness'] - ..lightness = styler['lightness'] - ..saturation = styler['saturation'] - ..visibility = styler['visibility'] - ..weight = styler['weight']; - })?.toList(); -} - -// Converts a String to its corresponding MapTypeStyleElementType enum value. -final _elementTypeToEnum = { - 'all': gmaps.MapTypeStyleElementType.ALL, - 'geometry': gmaps.MapTypeStyleElementType.GEOMETRY, - 'geometry.fill': gmaps.MapTypeStyleElementType.GEOMETRY_FILL, - 'geometry.stroke': gmaps.MapTypeStyleElementType.GEOMETRY_STROKE, - 'labels': gmaps.MapTypeStyleElementType.LABELS, - 'labels.icon': gmaps.MapTypeStyleElementType.LABELS_ICON, - 'labels.text': gmaps.MapTypeStyleElementType.LABELS_TEXT, - 'labels.text.fill': gmaps.MapTypeStyleElementType.LABELS_TEXT_FILL, - 'labels.text.stroke': gmaps.MapTypeStyleElementType.LABELS_TEXT_STROKE, -}; - -// Converts a String to its corresponding MapTypeStyleFeatureType enum value. -final _featureTypeToEnum = { - 'administrative': gmaps.MapTypeStyleFeatureType.ADMINISTRATIVE, - 'administrative.country': - gmaps.MapTypeStyleFeatureType.ADMINISTRATIVE_COUNTRY, - 'administrative.land_parcel': - gmaps.MapTypeStyleFeatureType.ADMINISTRATIVE_LAND_PARCEL, - 'administrative.locality': - gmaps.MapTypeStyleFeatureType.ADMINISTRATIVE_LOCALITY, - 'administrative.neighborhood': - gmaps.MapTypeStyleFeatureType.ADMINISTRATIVE_NEIGHBORHOOD, - 'administrative.province': - gmaps.MapTypeStyleFeatureType.ADMINISTRATIVE_PROVINCE, - 'all': gmaps.MapTypeStyleFeatureType.ALL, - 'landscape': gmaps.MapTypeStyleFeatureType.LANDSCAPE, - 'landscape.man_made': gmaps.MapTypeStyleFeatureType.LANDSCAPE_MAN_MADE, - 'landscape.natural': gmaps.MapTypeStyleFeatureType.LANDSCAPE_NATURAL, - 'landscape.natural.landcover': - gmaps.MapTypeStyleFeatureType.LANDSCAPE_NATURAL_LANDCOVER, - 'landscape.natural.terrain': - gmaps.MapTypeStyleFeatureType.LANDSCAPE_NATURAL_TERRAIN, - 'poi': gmaps.MapTypeStyleFeatureType.POI, - 'poi.attraction': gmaps.MapTypeStyleFeatureType.POI_ATTRACTION, - 'poi.business': gmaps.MapTypeStyleFeatureType.POI_BUSINESS, - 'poi.government': gmaps.MapTypeStyleFeatureType.POI_GOVERNMENT, - 'poi.medical': gmaps.MapTypeStyleFeatureType.POI_MEDICAL, - 'poi.park': gmaps.MapTypeStyleFeatureType.POI_PARK, - 'poi.place_of_worship': gmaps.MapTypeStyleFeatureType.POI_PLACE_OF_WORSHIP, - 'poi.school': gmaps.MapTypeStyleFeatureType.POI_SCHOOL, - 'poi.sports_complex': gmaps.MapTypeStyleFeatureType.POI_SPORTS_COMPLEX, - 'road': gmaps.MapTypeStyleFeatureType.ROAD, - 'road.arterial': gmaps.MapTypeStyleFeatureType.ROAD_ARTERIAL, - 'road.highway': gmaps.MapTypeStyleFeatureType.ROAD_HIGHWAY, - 'road.highway.controlled_access': - gmaps.MapTypeStyleFeatureType.ROAD_HIGHWAY_CONTROLLED_ACCESS, - 'road.local': gmaps.MapTypeStyleFeatureType.ROAD_LOCAL, - 'transit': gmaps.MapTypeStyleFeatureType.TRANSIT, - 'transit.line': gmaps.MapTypeStyleFeatureType.TRANSIT_LINE, - 'transit.station': gmaps.MapTypeStyleFeatureType.TRANSIT_STATION, - 'transit.station.airport': - gmaps.MapTypeStyleFeatureType.TRANSIT_STATION_AIRPORT, - 'transit.station.bus': gmaps.MapTypeStyleFeatureType.TRANSIT_STATION_BUS, - 'transit.station.rail': gmaps.MapTypeStyleFeatureType.TRANSIT_STATION_RAIL, - 'water': gmaps.MapTypeStyleFeatureType.WATER, -}; - // The keys we'd expect to see in a serialized MapTypeStyle JSON object. final _mapStyleKeys = { 'elementType', @@ -193,37 +127,36 @@ bool _isJsonMapStyle(Map value) { } // Converts an incoming JSON-encoded Style info, into the correct gmaps array. -List _mapStyles(String mapStyleJson) { +List _mapStyles(String? mapStyleJson) { List styles = []; if (mapStyleJson != null) { - styles = json.decode(mapStyleJson, reviver: (key, value) { - if (value is Map && _isJsonMapStyle(value)) { - return gmaps.MapTypeStyle() - ..elementType = _elementTypeToEnum[value['elementType']] - ..featureType = _featureTypeToEnum[value['featureType']] - ..stylers = _parseStylers(value['stylers']); - } - return value; - }).cast(); + 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; + }) + .cast() + .toList(); + // .toList calls are required so the JS API understands the underlying data structure. } return styles; } gmaps.LatLng _latLngToGmLatLng(LatLng latLng) { - if (latLng == null) return null; return gmaps.LatLng(latLng.latitude, latLng.longitude); } LatLng _gmLatLngToLatLng(gmaps.LatLng latLng) { - if (latLng == null) return _nullLatLng; - return LatLng(latLng.lat, latLng.lng); + return LatLng(latLng.lat.toDouble(), latLng.lng.toDouble()); } LatLngBounds _gmLatLngBoundsTolatLngBounds(gmaps.LatLngBounds latLngBounds) { - if (latLngBounds == null) { - return _nullLatLngBounds; - } - return LatLngBounds( southwest: _gmLatLngToLatLng(latLngBounds.southWest), northeast: _gmLatLngToLatLng(latLngBounds.northEast), @@ -232,141 +165,50 @@ LatLngBounds _gmLatLngBoundsTolatLngBounds(gmaps.LatLngBounds latLngBounds) { CameraPosition _gmViewportToCameraPosition(gmaps.GMap map) { return CameraPosition( - target: _gmLatLngToLatLng(map.center), - bearing: map.heading ?? 0, - tilt: map.tilt ?? 0, - zoom: map.zoom?.toDouble() ?? 10, + target: _gmLatLngToLatLng(map.center ?? _nullGmapsLatLng), + bearing: map.heading?.toDouble() ?? 0, + tilt: map.tilt?.toDouble() ?? 0, + zoom: map.zoom?.toDouble() ?? 0, ); } -Set _rawOptionsToInitialMarkers(Map rawOptions) { - final List> list = rawOptions['markersToAdd']; - Set markers = {}; - markers.addAll(list?.map((rawMarker) { - Offset offset; - LatLng position; - InfoWindow infoWindow; - if (rawMarker['anchor'] != null) { - offset = Offset((rawMarker['anchor'][0]), (rawMarker['anchor'][1])); - } - if (rawMarker['position'] != null) { - position = LatLng.fromJson(rawMarker['position']); - } - if (rawMarker['infoWindow'] != null || rawMarker['snippet'] != null) { - String title = rawMarker['infoWindow'] != null - ? rawMarker['infoWindow']['title'] - : null; - infoWindow = InfoWindow( - title: title ?? '', - snippet: rawMarker['snippet'] ?? '', - ); - } - return Marker( - markerId: MarkerId(rawMarker['markerId']), - alpha: rawMarker['alpha'], - anchor: offset, - consumeTapEvents: rawMarker['consumeTapEvents'], - draggable: rawMarker['draggable'], - flat: rawMarker['flat'], - // TODO: Doesn't this support custom icons? - icon: BitmapDescriptor.defaultMarker, - infoWindow: infoWindow, - position: position ?? _nullLatLng, - rotation: rawMarker['rotation'], - visible: rawMarker['visible'], - zIndex: rawMarker['zIndex'], - ); - }) ?? - []); - return markers; -} - -Set _rawOptionsToInitialCircles(Map rawOptions) { - final List> list = rawOptions['circlesToAdd']; - Set circles = {}; - circles.addAll(list?.map((rawCircle) { - LatLng center; - if (rawCircle['center'] != null) { - center = LatLng.fromJson(rawCircle['center']); - } - return Circle( - circleId: CircleId(rawCircle['circleId']), - consumeTapEvents: rawCircle['consumeTapEvents'], - fillColor: Color(rawCircle['fillColor'] ?? _defaultFillColor), - center: center ?? _nullLatLng, - radius: rawCircle['radius'], - strokeColor: Color(rawCircle['strokeColor'] ?? _defaultStrokeColor), - strokeWidth: rawCircle['strokeWidth'], - visible: rawCircle['visible'], - zIndex: rawCircle['zIndex'], - ); - }) ?? - []); - return circles; -} - -// Unsupported on the web: endCap, jointType, patterns and startCap. -Set _rawOptionsToInitialPolylines(Map rawOptions) { - final List> list = rawOptions['polylinesToAdd']; - Set polylines = {}; - polylines.addAll(list?.map((rawPolyline) { - return Polyline( - polylineId: PolylineId(rawPolyline['polylineId']), - consumeTapEvents: rawPolyline['consumeTapEvents'], - color: Color(rawPolyline['color'] ?? _defaultStrokeColor), - geodesic: rawPolyline['geodesic'], - visible: rawPolyline['visible'], - zIndex: rawPolyline['zIndex'], - width: rawPolyline['width'], - points: rawPolyline['points'] - ?.map((rawPoint) => LatLng.fromJson(rawPoint)) - ?.toList(), - ); - }) ?? - []); - return polylines; -} - -Set _rawOptionsToInitialPolygons(Map rawOptions) { - final List> list = rawOptions['polygonsToAdd']; - Set polygons = {}; - - polygons.addAll(list?.map((rawPolygon) { - return Polygon( - polygonId: PolygonId(rawPolygon['polygonId']), - consumeTapEvents: rawPolygon['consumeTapEvents'], - fillColor: Color(rawPolygon['fillColor'] ?? _defaultFillColor), - geodesic: rawPolygon['geodesic'], - strokeColor: Color(rawPolygon['strokeColor'] ?? _defaultStrokeColor), - strokeWidth: rawPolygon['strokeWidth'], - visible: rawPolygon['visible'], - zIndex: rawPolygon['zIndex'], - points: rawPolygon['points'] - ?.map((rawPoint) => LatLng.fromJson(rawPoint)) - ?.toList(), - ); - }) ?? - []); - return polygons; -} - // Convert plugin objects to gmaps.Options objects // TODO: Move to their appropriate objects, maybe make these copy constructors: // Marker.fromMarker(anotherMarker, moreOptions); -gmaps.InfoWindowOptions _infoWindowOptionsFromMarker(Marker marker) { - if ((marker.infoWindow?.title?.isEmpty ?? true) && - (marker.infoWindow?.snippet?.isEmpty ?? true)) { +gmaps.InfoWindowOptions? _infoWindowOptionsFromMarker(Marker marker) { + final markerTitle = marker.infoWindow.title ?? ''; + final markerSnippet = marker.infoWindow.snippet ?? ''; + + // If both the title and snippet of an infowindow are empty, we don't really + // want an infowindow... + if ((markerTitle.isEmpty) && (markerSnippet.isEmpty)) { return null; } - final content = '

' + - sanitizeHtml(marker.infoWindow.title ?? "") + - '

' + - sanitizeHtml(marker.infoWindow.snippet ?? ""); + // Add an outer wrapper to the contents of the infowindow, we need it to listen + // to click events... + final HtmlElement container = DivElement() + ..id = 'gmaps-marker-${marker.markerId.value}-infowindow'; + + if (markerTitle.isNotEmpty) { + final HtmlElement title = HeadingElement.h3() + ..className = 'infowindow-title' + ..innerText = markerTitle; + container.children.add(title); + } + if (markerSnippet.isNotEmpty) { + final HtmlElement snippet = DivElement() + ..className = 'infowindow-snippet' + ..setInnerHtml( + sanitizeHtml(markerSnippet), + treeSanitizer: NodeTreeSanitizer.trusted, + ); + container.children.add(snippet); + } return gmaps.InfoWindowOptions() - ..content = content + ..content = container ..zIndex = marker.zIndex; // TODO: Compute the pixelOffset of the infoWindow, from the size of the Marker, // and the marker.infoWindow.anchor property. @@ -377,10 +219,10 @@ gmaps.InfoWindowOptions _infoWindowOptionsFromMarker(Marker marker) { // Preserves the position from the [currentMarker], if set. gmaps.MarkerOptions _markerOptionsFromMarker( Marker marker, - gmaps.Marker currentMarker, + gmaps.Marker? currentMarker, ) { - final iconConfig = marker.icon?.toJson() as List; - gmaps.Icon icon; + final iconConfig = marker.icon.toJson() as List; + gmaps.Icon? icon; if (iconConfig != null) { if (iconConfig[0] == 'fromAssetImage') { @@ -398,6 +240,11 @@ gmaps.MarkerOptions _markerOptionsFromMarker( ..size = size ..scaledSize = size; } + } else if (iconConfig[0] == 'fromBytes') { + // Grab the bytes, and put them into a blob + List bytes = iconConfig[1]; + final blob = Blob([bytes]); // Let the browser figure out the encoding + icon = gmaps.Icon()..url = Url.createObjectUrlFromBlob(blob); } } return gmaps.MarkerOptions() @@ -406,7 +253,7 @@ 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 @@ -418,11 +265,11 @@ gmaps.MarkerOptions _markerOptionsFromMarker( gmaps.CircleOptions _circleOptionsFromCircle(Circle circle) { final populationOptions = gmaps.CircleOptions() - ..strokeColor = '#' + circle.strokeColor.value.toRadixString(16) - ..strokeOpacity = 0.8 + ..strokeColor = _getCssColor(circle.strokeColor) + ..strokeOpacity = _getCssOpacity(circle.strokeColor) ..strokeWeight = circle.strokeWidth - ..fillColor = '#' + circle.fillColor.value.toRadixString(16) - ..fillOpacity = 0.6 + ..fillColor = _getCssColor(circle.fillColor) + ..fillOpacity = _getCssOpacity(circle.fillColor) ..center = gmaps.LatLng(circle.center.latitude, circle.center.longitude) ..radius = circle.radius ..visible = circle.visible; @@ -431,22 +278,61 @@ gmaps.CircleOptions _circleOptionsFromCircle(Circle circle) { gmaps.PolygonOptions _polygonOptionsFromPolygon( gmaps.GMap googleMap, Polygon polygon) { - List paths = []; + List path = []; polygon.points.forEach((point) { - paths.add(_latLngToGmLatLng(point)); + path.add(_latLngToGmLatLng(point)); + }); + final polygonDirection = _isPolygonClockwise(path); + List> paths = [path]; + int holeIndex = 0; + polygon.holes.forEach((hole) { + List holePath = + hole.map((point) => _latLngToGmLatLng(point)).toList(); + if (_isPolygonClockwise(holePath) == polygonDirection) { + holePath = holePath.reversed.toList(); + if (kDebugMode) { + print( + 'Hole [$holeIndex] in Polygon [${polygon.polygonId.value}] has been reversed.' + ' Ensure holes in polygons are "wound in the opposite direction to the outer path."' + ' More info: https://github.com/flutter/flutter/issues/74096'); + } + } + paths.add(holePath); + holeIndex++; }); return gmaps.PolygonOptions() ..paths = paths - ..strokeColor = '#' + polygon.strokeColor.value.toRadixString(16) - ..strokeOpacity = 0.8 + ..strokeColor = _getCssColor(polygon.strokeColor) + ..strokeOpacity = _getCssOpacity(polygon.strokeColor) ..strokeWeight = polygon.strokeWidth - ..fillColor = '#' + polygon.fillColor.value.toRadixString(16) - ..fillOpacity = 0.35 + ..fillColor = _getCssColor(polygon.fillColor) + ..fillOpacity = _getCssOpacity(polygon.fillColor) ..visible = polygon.visible ..zIndex = polygon.zIndex ..geodesic = polygon.geodesic; } +/// Calculates the direction of a given Polygon +/// based on: https://stackoverflow.com/a/1165943 +/// +/// returns [true] if clockwise [false] if counterclockwise +/// +/// This method expects that the incoming [path] is a `List` of well-formed, +/// non-null [gmaps.LatLng] objects. +/// +/// Currently, this method is only called from [_polygonOptionsFromPolygon], and +/// 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++) { + direction = direction + + ((path[(i + 1) % path.length].lat - path[i].lat) * + (path[(i + 1) % path.length].lng + path[i].lng)); + } + return direction >= 0; +} + gmaps.PolylineOptions _polylineOptionsFromPolyline( gmaps.GMap googleMap, Polyline polyline) { List paths = []; @@ -456,9 +342,9 @@ gmaps.PolylineOptions _polylineOptionsFromPolyline( return gmaps.PolylineOptions() ..path = paths - ..strokeOpacity = 1.0 ..strokeWeight = polyline.width - ..strokeColor = '#' + polyline.color.value.toRadixString(16).substring(0, 6) + ..strokeColor = _getCssColor(polyline.color) + ..strokeOpacity = _getCssOpacity(polyline.color) ..visible = polyline.visible ..zIndex = polyline.zIndex ..geodesic = polyline.geodesic; @@ -471,7 +357,7 @@ gmaps.PolylineOptions _polylineOptionsFromPolyline( // Translates a [CameraUpdate] into operations on a [gmaps.GMap]. void _applyCameraUpdate(gmaps.GMap map, CameraUpdate update) { - final json = update.toJson(); + final json = update.toJson() as List; switch (json[0]) { case 'newCameraPosition': map.heading = json[1]['bearing']; @@ -497,7 +383,7 @@ void _applyCameraUpdate(gmaps.GMap map, CameraUpdate update) { map.panBy(json[1], json[2]); break; case 'zoomBy': - gmaps.LatLng focusLatLng; + gmaps.LatLng? focusLatLng; double zoomDelta = json[1] ?? 0; // Web only supports integer changes... int newZoomDelta = zoomDelta < 0 ? zoomDelta.floor() : zoomDelta.ceil(); @@ -510,16 +396,16 @@ void _applyCameraUpdate(gmaps.GMap map, CameraUpdate update) { // print('Error computing new focus LatLng. JS Error: ' + e.toString()); } } - map.zoom = map.zoom + newZoomDelta; + map.zoom = (map.zoom ?? 0) + newZoomDelta; if (focusLatLng != null) { map.panTo(focusLatLng); } break; case 'zoomIn': - map.zoom++; + map.zoom = (map.zoom ?? 0) + 1; break; case 'zoomOut': - map.zoom--; + map.zoom = (map.zoom ?? 0) - 1; break; case 'zoomTo': map.zoom = json[1]; @@ -531,17 +417,27 @@ 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 ne = map.bounds.northEast; - final sw = map.bounds.southWest; + final bounds = map.bounds; final projection = map.projection; + final zoom = map.zoom; + + assert( + bounds != null, 'Map Bounds required to compute LatLng of screen x/y.'); + assert(projection != null, + 'Map Projection required to compute LatLng of screen x/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 topRight = projection.fromLatLngToPoint(ne); - final bottomLeft = projection.fromLatLngToPoint(sw); + final topRight = projection!.fromLatLngToPoint!(ne)!; + final bottomLeft = projection.fromLatLngToPoint!(sw)!; - final scale = 1 << map.zoom; // 2 ^ zoom + final scale = 1 << (zoom!.toInt()); // 2 ^ zoom final point = - gmaps.Point((x / scale) + bottomLeft.x, (y / scale) + topRight.y); + gmaps.Point((x / scale) + bottomLeft.x!, (y / scale) + topRight.y!); - return projection.fromPointToLatLng(point); + 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 707af828e2c6..226268270579 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -14,21 +14,24 @@ class GoogleMapController { // The internal ID of the map. Used to broadcast events, DOM IDs and everything where a unique ID is needed. final int _mapId; + final CameraPosition _initialCameraPosition; + final Set _markers; + final Set _polygons; + final Set _polylines; + final Set _circles; // The raw options passed by the user, before converting to gmaps. // Caching this allows us to re-create the map faithfully when needed. - Map _rawOptions = { - 'options': {}, - }; + Map _rawMapOptions = {}; // Creates the 'viewType' for the _widget String _getViewType(int mapId) => 'plugins.flutter.io/google_maps_$mapId'; // The Flutter widget that contains the rendered Map. - HtmlElementView _widget; - HtmlElement _div; + HtmlElementView? _widget; + late HtmlElement _div; /// The Flutter widget that will contain the rendered Map. Used for caching. - HtmlElementView get widget { + Widget? get widget { if (_widget == null && !_streamController.isClosed) { _widget = HtmlElementView( viewType: _getViewType(_mapId), @@ -38,14 +41,14 @@ class GoogleMapController { } // The currently-enabled traffic layer. - gmaps.TrafficLayer _trafficLayer; + gmaps.TrafficLayer? _trafficLayer; /// A getter for the current traffic layer. Only for tests. @visibleForTesting - gmaps.TrafficLayer get trafficLayer => _trafficLayer; + gmaps.TrafficLayer? get trafficLayer => _trafficLayer; // The underlying GMap instance. This is the interface with the JS SDK. - gmaps.GMap _googleMap; + gmaps.GMap? _googleMap; // The StreamController used by this controller and the geometry ones. final StreamController _streamController; @@ -54,10 +57,10 @@ class GoogleMapController { Stream get events => _streamController.stream; // Geometry controllers, for different features of the map. - CirclesController _circlesController; - PolygonsController _polygonsController; - PolylinesController _polylinesController; - MarkersController _markersController; + CirclesController? _circlesController; + PolygonsController? _polygonsController; + PolylinesController? _polylinesController; + MarkersController? _markersController; // Keeps track if _attachGeometryControllers has been called or not. bool _controllersBoundToMap = false; @@ -66,12 +69,25 @@ class GoogleMapController { /// Initializes the GMap, and the sub-controllers related to it. Wires events. GoogleMapController({ - @required int mapId, - @required StreamController streamController, - @required Map rawOptions, - }) : this._mapId = mapId, - this._streamController = streamController, - this._rawOptions = rawOptions { + 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); @@ -80,7 +96,10 @@ class GoogleMapController { // 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); + _div = DivElement() + ..id = _getViewType(mapId) + ..style.width = '100%' + ..style.height = '100%'; ui.platformViewRegistry.registerViewFactory( _getViewType(mapId), @@ -91,11 +110,11 @@ class GoogleMapController { /// Overrides certain properties to install mocks defined during testing. @visibleForTesting void debugSetOverrides({ - DebugCreateMapFunction createMap, - MarkersController markers, - CirclesController circles, - PolygonsController polygons, - PolylinesController polylines, + DebugCreateMapFunction? createMap, + MarkersController? markers, + CirclesController? circles, + PolygonsController? polygons, + PolylinesController? polylines, }) { _overrideCreateMap = createMap; _markersController = markers ?? _markersController; @@ -104,11 +123,11 @@ class GoogleMapController { _polylinesController = polylines ?? _polylinesController; } - DebugCreateMapFunction _overrideCreateMap; + DebugCreateMapFunction? _overrideCreateMap; gmaps.GMap _createMap(HtmlElement div, gmaps.MapOptions options) { if (_overrideCreateMap != null) { - return _overrideCreateMap(div, options); + return _overrideCreateMap!(div, options); } return gmaps.GMap(div, options); } @@ -121,36 +140,39 @@ 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(_rawOptions); + var options = _rawOptionsToGmapsOptions(_rawMapOptions); // Initial position can only to be set here! - options = _applyInitialPosition(_rawOptions, options); + options = _applyInitialPosition(_initialCameraPosition, options); // Create the map... - _googleMap = _createMap(_div, options); + final map = _createMap(_div, options); + _googleMap = map; - _attachMapEvents(_googleMap); - _attachGeometryControllers(_googleMap); + _attachMapEvents(map); + _attachGeometryControllers(map); _renderInitialGeometry( - markers: _rawOptionsToInitialMarkers(_rawOptions), - circles: _rawOptionsToInitialCircles(_rawOptions), - polygons: _rawOptionsToInitialPolygons(_rawOptions), - polylines: _rawOptionsToInitialPolylines(_rawOptions), + markers: _markers, + circles: _circles, + polygons: _polygons, + polylines: _polylines, ); - _setTrafficLayer(_googleMap, _isTrafficLayerEnabled(_rawOptions)); + _setTrafficLayer(map, _isTrafficLayerEnabled(_rawMapOptions)); } // Funnels map gmap events into the plugin's stream controller. void _attachMapEvents(gmaps.GMap map) { map.onClick.listen((event) { + assert(event.latLng != null); _streamController.add( - MapTapEvent(_mapId, _gmLatLngToLatLng(event.latLng)), + MapTapEvent(_mapId, _gmLatLngToLatLng(event.latLng!)), ); }); map.onRightclick.listen((event) { + assert(event.latLng != null); _streamController.add( - MapLongPressEvent(_mapId, _gmLatLngToLatLng(event.latLng)), + MapLongPressEvent(_mapId, _gmLatLngToLatLng(event.latLng!)), ); }); map.onBoundsChanged.listen((event) { @@ -172,54 +194,70 @@ class GoogleMapController { void _attachGeometryControllers(gmaps.GMap map) { // Now we can add the initial geometry. // And bind the (ready) map instance to the other geometry controllers. - _circlesController.bindToMap(_mapId, map); - _polygonsController.bindToMap(_mapId, map); - _polylinesController.bindToMap(_mapId, map); - _markersController.bindToMap(_mapId, map); + // + // These controllers are either created in the constructor of this class, or + // overriden (for testing) by the [debugSetOverrides] method. They can't be + // null. + assert(_circlesController != null, + 'Cannot attach a map to a null CirclesController instance.'); + assert(_polygonsController != null, + 'Cannot attach a map to a null PolygonsController instance.'); + assert(_polylinesController != null, + 'Cannot attach a map to a null PolylinesController instance.'); + assert(_markersController != null, + 'Cannot attach a map to a null MarkersController instance.'); + + _circlesController!.bindToMap(_mapId, map); + _polygonsController!.bindToMap(_mapId, map); + _polylinesController!.bindToMap(_mapId, map); + _markersController!.bindToMap(_mapId, map); + _controllersBoundToMap = true; } // Renders the initial sets of geometry. void _renderInitialGeometry({ - Set markers, - Set circles, - Set polygons, - Set polylines, + 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.'); - _markersController.addMarkers(markers); - _circlesController.addCircles(circles); - _polygonsController.addPolygons(polygons); - _polylinesController.addPolylines(polylines); + + // The above assert will only succeed if the controllers have been bound to a map + // in the [_attachGeometryControllers] method, which ensures that all these + // controllers below are *not* null. + + _markersController!.addMarkers(markers); + _circlesController!.addCircles(circles); + _polygonsController!.addPolygons(polygons); + _polylinesController!.addPolylines(polylines); } - // Merges new options coming from the plugin into the `key` entry of the _rawOptions map. + // Merges new options coming from the plugin into the _rawMapOptions map. // - // By default: `key` is 'options'. - // - // Returns the updated _rawOptions object. - Map _mergeRawOptions( - Map newOptions, { - String key = 'options', - }) { - _rawOptions[key] = { - ...(_rawOptions[key] ?? {}), + // Returns the updated _rawMapOptions object. + Map _mergeRawOptions(Map newOptions) { + _rawMapOptions = { + ..._rawMapOptions, ...newOptions, }; - return _rawOptions; + return _rawMapOptions; } /// Updates the map options from a `Map`. /// /// This method converts the map into the proper [gmaps.MapOptions] void updateRawOptions(Map optionsUpdate) { + assert(_googleMap != null, 'Cannot update options on a null map.'); + final newOptions = _mergeRawOptions(optionsUpdate); _setOptions(_rawOptionsToGmapsOptions(newOptions)); - _setTrafficLayer(_googleMap, _isTrafficLayerEnabled(newOptions)); + _setTrafficLayer(_googleMap!, _isTrafficLayerEnabled(newOptions)); } // Sets new [gmaps.MapOptions] on the wrapped map. @@ -230,11 +268,10 @@ class GoogleMapController { // Attaches/detaches a Traffic Layer on the passed `map` if `attach` is true/false. void _setTrafficLayer(gmaps.GMap map, bool attach) { if (attach && _trafficLayer == null) { - _trafficLayer = gmaps.TrafficLayer(); - _trafficLayer.set('map', map); + _trafficLayer = gmaps.TrafficLayer()..set('map', map); } if (!attach && _trafficLayer != null) { - _trafficLayer.set('map', null); + _trafficLayer!.set('map', null); _trafficLayer = null; } } @@ -244,36 +281,61 @@ class GoogleMapController { /// Returns the [LatLngBounds] of the current viewport. Future getVisibleRegion() async { - return _gmLatLngBoundsTolatLngBounds(await _googleMap.bounds); + assert(_googleMap != null, 'Cannot get the visible region of a null map.'); + + return _gmLatLngBoundsTolatLngBounds( + await _googleMap!.bounds ?? _nullGmapsLatLngBounds, + ); } /// Returns the [ScreenCoordinate] for a given viewport [LatLng]. Future getScreenCoordinate(LatLng latLng) async { + assert(_googleMap != null, + 'Cannot get the screen coordinates with a null map.'); + assert(_googleMap!.projection != null, + 'Cannot compute screen coordinate with a null map or projection.'); + final point = - _googleMap.projection.fromLatLngToPoint(_latLngToGmLatLng(latLng)); - return ScreenCoordinate(x: point.x, y: point.y); + _googleMap!.projection!.fromLatLngToPoint!(_latLngToGmLatLng(latLng))!; + + assert(point.x != null && point.y != null, + 'The x and y of a ScreenCoordinate cannot be null.'); + + return ScreenCoordinate(x: point.x!.toInt(), y: point.y!.toInt()); } /// Returns the [LatLng] for a `screenCoordinate` (in pixels) of the viewport. Future getLatLng(ScreenCoordinate screenCoordinate) async { - final latLng = _googleMap.projection.fromPointToLatLng( - gmaps.Point(screenCoordinate.x, screenCoordinate.y), - ); + assert(_googleMap != null, + 'Cannot get the lat, lng of a screen coordinate with a null map.'); + + final gmaps.LatLng latLng = + _pixelToLatLng(_googleMap!, screenCoordinate.x, screenCoordinate.y); return _gmLatLngToLatLng(latLng); } /// Applies a `cameraUpdate` to the current viewport. Future moveCamera(CameraUpdate cameraUpdate) async { - return _applyCameraUpdate(_googleMap, cameraUpdate); + assert(_googleMap != null, 'Cannot update the camera of a null map.'); + + return _applyCameraUpdate(_googleMap!, cameraUpdate); } /// Returns the zoom level of the current viewport. - Future getZoomLevel() async => _googleMap.zoom.toDouble(); + Future getZoomLevel() async { + assert(_googleMap != null, 'Cannot get zoom level of a null map.'); + assert(_googleMap!.zoom != null, + 'Zoom level should not be null. Is the map correctly initialized?'); + + return _googleMap!.zoom!.toDouble(); + } // Geometry manipulation /// Applies [CircleUpdates] to the currently managed circles. void updateCircles(CircleUpdates updates) { + assert( + _circlesController != null, 'Cannot update circles after dispose().'); _circlesController?.addCircles(updates.circlesToAdd); _circlesController?.changeCircles(updates.circlesToChange); _circlesController?.removeCircles(updates.circleIdsToRemove); @@ -281,6 +343,8 @@ class GoogleMapController { /// Applies [PolygonUpdates] to the currently managed polygons. void updatePolygons(PolygonUpdates updates) { + assert( + _polygonsController != null, 'Cannot update polygons after dispose().'); _polygonsController?.addPolygons(updates.polygonsToAdd); _polygonsController?.changePolygons(updates.polygonsToChange); _polygonsController?.removePolygons(updates.polygonIdsToRemove); @@ -288,6 +352,8 @@ class GoogleMapController { /// Applies [PolylineUpdates] to the currently managed lines. void updatePolylines(PolylineUpdates updates) { + assert(_polylinesController != null, + 'Cannot update polylines after dispose().'); _polylinesController?.addPolylines(updates.polylinesToAdd); _polylinesController?.changePolylines(updates.polylinesToChange); _polylinesController?.removePolylines(updates.polylineIdsToRemove); @@ -295,6 +361,8 @@ class GoogleMapController { /// Applies [MarkerUpdates] to the currently managed markers. void updateMarkers(MarkerUpdates updates) { + assert( + _markersController != null, 'Cannot update markers after dispose().'); _markersController?.addMarkers(updates.markersToAdd); _markersController?.changeMarkers(updates.markersToChange); _markersController?.removeMarkers(updates.markerIdsToRemove); @@ -302,22 +370,29 @@ class GoogleMapController { /// Shows the [InfoWindow] of the marker identified by its [MarkerId]. void showInfoWindow(MarkerId markerId) { + assert(_markersController != null, + 'Cannot show infowindow of marker [${markerId.value}] after dispose().'); _markersController?.showMarkerInfoWindow(markerId); } /// Hides the [InfoWindow] of the marker identified by its [MarkerId]. void hideInfoWindow(MarkerId markerId) { + assert(_markersController != null, + 'Cannot hide infowindow of marker [${markerId.value}] after dispose().'); _markersController?.hideMarkerInfoWindow(markerId); } /// Returns true if the [InfoWindow] of the marker identified by [MarkerId] is shown. bool isInfoWindowShown(MarkerId markerId) { - return _markersController?.isInfoWindowShown(markerId); + return _markersController?.isInfoWindowShown(markerId) ?? false; } // Cleanup /// Disposes of this controller and its resources. + /// + /// You won't be able to call many of the methods on this controller after + /// calling `dispose`! void dispose() { _widget = null; _googleMap = 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 cf549e8e375e..692917fef4da 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -45,7 +45,7 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { @override Future updateMapOptions( Map optionsUpdate, { - @required int mapId, + required int mapId, }) async { _map(mapId).updateRawOptions(optionsUpdate); } @@ -54,7 +54,7 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { @override Future updateMarkers( MarkerUpdates markerUpdates, { - @required int mapId, + required int mapId, }) async { _map(mapId).updateMarkers(markerUpdates); } @@ -63,7 +63,7 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { @override Future updatePolygons( PolygonUpdates polygonUpdates, { - @required int mapId, + required int mapId, }) async { _map(mapId).updatePolygons(polygonUpdates); } @@ -72,7 +72,7 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { @override Future updatePolylines( PolylineUpdates polylineUpdates, { - @required int mapId, + required int mapId, }) async { _map(mapId).updatePolylines(polylineUpdates); } @@ -81,16 +81,32 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { @override Future updateCircles( CircleUpdates circleUpdates, { - @required int mapId, + required int mapId, }) async { _map(mapId).updateCircles(circleUpdates); } + @override + Future updateTileOverlays({ + required Set newTileOverlays, + required int mapId, + }) async { + return; // Noop for now! + } + + @override + Future clearTileCache( + TileOverlayId tileOverlayId, { + required int mapId, + }) async { + return; // Noop for now! + } + /// Applies the given `cameraUpdate` to the current viewport (with animation). @override Future animateCamera( CameraUpdate cameraUpdate, { - @required int mapId, + required int mapId, }) async { return moveCamera(cameraUpdate, mapId: mapId); } @@ -99,7 +115,7 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { @override Future moveCamera( CameraUpdate cameraUpdate, { - @required int mapId, + required int mapId, }) async { return _map(mapId).moveCamera(cameraUpdate); } @@ -112,8 +128,8 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { /// pass full styles. @override Future setMapStyle( - String mapStyle, { - @required int mapId, + String? mapStyle, { + required int mapId, }) async { _map(mapId).updateRawOptions({ 'styles': _mapStyles(mapStyle), @@ -123,7 +139,7 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { /// Returns the bounds of the current viewport. @override Future getVisibleRegion({ - @required int mapId, + required int mapId, }) { return _map(mapId).getVisibleRegion(); } @@ -132,7 +148,7 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { @override Future getScreenCoordinate( LatLng latLng, { - @required int mapId, + required int mapId, }) { return _map(mapId).getScreenCoordinate(latLng); } @@ -141,7 +157,7 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { @override Future getLatLng( ScreenCoordinate screenCoordinate, { - @required int mapId, + required int mapId, }) { return _map(mapId).getLatLng(screenCoordinate); } @@ -154,7 +170,7 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { @override Future showMarkerInfoWindow( MarkerId markerId, { - @required int mapId, + required int mapId, }) async { _map(mapId).showInfoWindow(markerId); } @@ -167,7 +183,7 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { @override Future hideMarkerInfoWindow( MarkerId markerId, { - @required int mapId, + required int mapId, }) async { _map(mapId).hideInfoWindow(markerId); } @@ -180,7 +196,7 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { @override Future isMarkerInfoWindowShown( MarkerId markerId, { - @required int mapId, + required int mapId, }) async { return _map(mapId).isInfoWindowShown(markerId); } @@ -188,7 +204,7 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { /// Returns the zoom level of the `mapId`. @override Future getZoomLevel({ - @required int mapId, + required int mapId, }) { return _map(mapId).getZoomLevel(); } @@ -197,95 +213,107 @@ class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { // into the plugin @override - Stream onCameraMoveStarted({@required int mapId}) { + Stream onCameraMoveStarted({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onCameraMove({@required int mapId}) { + Stream onCameraMove({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onCameraIdle({@required int mapId}) { + Stream onCameraIdle({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onMarkerTap({@required int mapId}) { + Stream onMarkerTap({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onInfoWindowTap({@required int mapId}) { + Stream onInfoWindowTap({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onMarkerDragEnd({@required int mapId}) { + Stream onMarkerDragEnd({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onPolylineTap({@required int mapId}) { + Stream onPolylineTap({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onPolygonTap({@required int mapId}) { + Stream onPolygonTap({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onCircleTap({@required int mapId}) { + Stream onCircleTap({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onTap({@required int mapId}) { + Stream onTap({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onLongPress({@required int mapId}) { + Stream onLongPress({required int mapId}) { return _events(mapId).whereType(); } /// Disposes of the current map. It can't be used afterwards! @override - void dispose({@required int mapId}) { - _map(mapId)?.dispose(); + void dispose({required int mapId}) { + _map(mapId).dispose(); _mapById.remove(mapId); } @override Widget buildView( - Map creationParams, - Set> gestureRecognizers, - PlatformViewCreatedCallback onPlatformViewCreated) { - int mapId = creationParams.remove('_webOnlyMapCreationId'); - - assert(mapId != null, - 'buildView needs a `_webOnlyMapCreationId` in its creationParams to prevent widget reloads in web.'); - - // Bail fast if we've already rendered this mapId... - if (_mapById[mapId]?.widget != null) { - return _mapById[mapId].widget; + 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 {}, + }) { + // Bail fast if we've already rendered this map ID... + if (_mapById[creationId]?.widget != null) { + return _mapById[creationId].widget; } final StreamController controller = StreamController.broadcast(); final mapController = GoogleMapController( - mapId: mapId, + initialCameraPosition: initialCameraPosition, + mapId: creationId, streamController: controller, - rawOptions: creationParams, + markers: markers, + polygons: polygons, + polylines: polylines, + circles: circles, + mapOptions: mapOptions, ); - _mapById[mapId] = mapController; + _mapById[creationId] = mapController; + + onPlatformViewCreated.call(creationId); - onPlatformViewCreated.call(mapId); + assert(mapController.widget != null, + 'The widget of a GoogleMapController cannot be null before calling dispose on it.'); - return mapController.widget; + return mapController.widget!; } } 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 a067e352732f..5b0169b565e5 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -6,33 +6,35 @@ 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; + gmaps.Marker? _marker; final bool _consumeTapEvents; - final gmaps.InfoWindow _infoWindow; + 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, + required gmaps.Marker marker, + gmaps.InfoWindow? infoWindow, bool consumeTapEvents = false, - LatLngCallback onDragEnd, - ui.VoidCallback onTap, + LatLngCallback? onDragEnd, + ui.VoidCallback? onTap, }) : _marker = marker, _infoWindow = infoWindow, _consumeTapEvents = consumeTapEvents { if (onTap != null) { - _marker.onClick.listen((event) { + marker.onClick.listen((event) { onTap.call(); }); } if (onDragEnd != null) { - _marker.onDragend.listen((event) { - _marker.position = event.latLng; - onDragEnd.call(event.latLng); + marker.onDragend.listen((event) { + if (marker != null) { + marker.position = event.latLng; + } + onDragEnd.call(event.latLng ?? _nullGmapsLatLng); }); } } @@ -44,38 +46,54 @@ class MarkerController { bool get infoWindowShown => _infoWindowShown; /// Returns the [gmaps.Marker] associated to this controller. - gmaps.Marker get marker => _marker; + gmaps.Marker? get marker => _marker; + + /// Returns the [gmaps.InfoWindow] associated to the marker. + @visibleForTesting + gmaps.InfoWindow? get infoWindow => _infoWindow; /// Updates the options of the wrapped [gmaps.Marker] object. + /// + /// This cannot be called after [remove]. void update( gmaps.MarkerOptions options, { - String newInfoWindowContent, + HtmlElement? newInfoWindowContent, }) { - _marker.options = options; + assert(_marker != null, 'Cannot `update` Marker after calling `remove`.'); + _marker!.options = options; if (_infoWindow != null && newInfoWindowContent != null) { - _infoWindow.content = newInfoWindowContent; + _infoWindow!.content = newInfoWindowContent; } } /// Disposes of the currently wrapped [gmaps.Marker]. void remove() { - _marker.visible = false; - _marker.map = null; - _marker = null; + if (_marker != null) { + _infoWindowShown = false; + _marker!.visible = false; + _marker!.map = null; + _marker = null; + } } /// Hide the associated [gmaps.InfoWindow]. + /// + /// This cannot be called after [remove]. void hideInfoWindow() { + assert(_marker != null, 'Cannot `hideInfoWindow` on a `remove`d Marker.'); if (_infoWindow != null) { - _infoWindow.close(); + _infoWindow!.close(); _infoWindowShown = false; } } /// Show the associated [gmaps.InfoWindow]. + /// + /// This cannot be called after [remove]. void showInfoWindow() { + assert(_marker != null, 'Cannot `showInfoWindow` on a `remove`d Marker.'); if (_infoWindow != null) { - _infoWindow.open(_marker.map, _marker); + _infoWindow!.open(_marker!.map, _marker); _infoWindowShown = true; } } 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 ebb478d20b06..b650b9bcf1c8 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -14,7 +14,7 @@ class MarkersController extends GeometryController { /// Initialize the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. MarkersController({ - @required StreamController stream, + required StreamController stream, }) : _streamController = stream, _markerIdToController = Map(); @@ -26,7 +26,7 @@ class MarkersController extends GeometryController { /// /// Wraps each [Marker] into its corresponding [MarkerController]. void addMarkers(Set markersToAdd) { - markersToAdd?.forEach(_addMarker); + markersToAdd.forEach(_addMarker); } void _addMarker(Marker marker) { @@ -35,13 +35,18 @@ class MarkersController extends GeometryController { } final infoWindowOptions = _infoWindowOptionsFromMarker(marker); - gmaps.InfoWindow gmInfoWindow; + gmaps.InfoWindow? gmInfoWindow; if (infoWindowOptions != null) { - gmInfoWindow = gmaps.InfoWindow(infoWindowOptions) - ..addListener('click', () { + 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; + content.onClick.listen((_) { _onInfoWindowTap(marker.markerId); }); + } } final currentMarker = _markerIdToController[marker.markerId]?.marker; @@ -66,11 +71,11 @@ class MarkersController extends GeometryController { /// Updates a set of [Marker] objects with new options. void changeMarkers(Set markersToChange) { - markersToChange?.forEach(_changeMarker); + markersToChange.forEach(_changeMarker); } void _changeMarker(Marker marker) { - MarkerController markerController = _markerIdToController[marker?.markerId]; + MarkerController? markerController = _markerIdToController[marker.markerId]; if (markerController != null) { final markerOptions = _markerOptionsFromMarker( marker, @@ -79,18 +84,18 @@ class MarkersController extends GeometryController { final infoWindow = _infoWindowOptionsFromMarker(marker); markerController.update( markerOptions, - newInfoWindowContent: infoWindow?.content, + newInfoWindowContent: infoWindow?.content as HtmlElement?, ); } } /// Removes a set of [MarkerId]s from the cache. void removeMarkers(Set markerIdsToRemove) { - markerIdsToRemove?.forEach(_removeMarker); + markerIdsToRemove.forEach(_removeMarker); } void _removeMarker(MarkerId markerId) { - final MarkerController markerController = _markerIdToController[markerId]; + final MarkerController? markerController = _markerIdToController[markerId]; markerController?.remove(); _markerIdToController.remove(markerId); } @@ -101,7 +106,8 @@ class MarkersController extends GeometryController { /// /// See also [hideMarkerInfoWindow] and [isInfoWindowShown]. void showMarkerInfoWindow(MarkerId markerId) { - MarkerController markerController = _markerIdToController[markerId]; + _hideAllMarkerInfoWindow(); + MarkerController? markerController = _markerIdToController[markerId]; markerController?.showInfoWindow(); } @@ -109,7 +115,7 @@ class MarkersController extends GeometryController { /// /// See also [showMarkerInfoWindow] and [isInfoWindowShown]. void hideMarkerInfoWindow(MarkerId markerId) { - MarkerController markerController = _markerIdToController[markerId]; + MarkerController? markerController = _markerIdToController[markerId]; markerController?.hideInfoWindow(); } @@ -117,7 +123,7 @@ class MarkersController extends GeometryController { /// /// See also [showMarkerInfoWindow] and [hideMarkerInfoWindow]. bool isInfoWindowShown(MarkerId markerId) { - MarkerController markerController = _markerIdToController[markerId]; + MarkerController? markerController = _markerIdToController[markerId]; return markerController?.infoWindowShown ?? false; } @@ -141,4 +147,11 @@ class MarkersController extends GeometryController { markerId, )); } + + void _hideAllMarkerInfoWindow() { + _markerIdToController.values + .where((controller) => + controller == null ? false : controller.infoWindowShown) + .forEach((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 f4c692d2ee83..9921d2ff3876 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -6,15 +6,15 @@ part of google_maps_flutter_web; /// The `PolygonController` class wraps a [gmaps.Polygon] and its `onTap` behavior. class PolygonController { - gmaps.Polygon _polygon; + gmaps.Polygon? _polygon; final bool _consumeTapEvents; /// Creates a `PolygonController` that wraps a [gmaps.Polygon] object and its `onTap` behavior. PolygonController({ - @required gmaps.Polygon polygon, + required gmaps.Polygon polygon, bool consumeTapEvents = false, - ui.VoidCallback onTap, + ui.VoidCallback? onTap, }) : _polygon = polygon, _consumeTapEvents = consumeTapEvents { if (onTap != null) { @@ -26,20 +26,25 @@ class PolygonController { /// Returns the wrapped [gmaps.Polygon]. Only used for testing. @visibleForTesting - gmaps.Polygon get polygon => _polygon; + gmaps.Polygon? get polygon => _polygon; /// Returns `true` if this Controller will use its own `onTap` handler to consume events. bool get consumeTapEvents => _consumeTapEvents; /// Updates the options of the wrapped [gmaps.Polygon] object. + /// + /// This cannot be called after [remove]. void update(gmaps.PolygonOptions options) { - _polygon.options = options; + assert(_polygon != null, 'Cannot `update` Polygon after calling `remove`.'); + _polygon!.options = options; } /// Disposes of the currently wrapped [gmaps.Polygon]. void remove() { - _polygon.visible = false; - _polygon.map = null; - _polygon = null; + if (_polygon != null) { + _polygon!.visible = false; + _polygon!.map = null; + _polygon = null; + } } } 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 5c078ea0aa7a..8a9643156351 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -14,7 +14,7 @@ class PolygonsController extends GeometryController { /// Initializes the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. PolygonsController({ - @required StreamController stream, + required StreamController stream, }) : _streamController = stream, _polygonIdToController = Map(); @@ -60,15 +60,15 @@ class PolygonsController extends GeometryController { } void _changePolygon(Polygon polygon) { - PolygonController polygonController = - _polygonIdToController[polygon?.polygonId]; + 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 = + polygonIdsToRemove.forEach((polygonId) { + final PolygonController? polygonController = _polygonIdToController[polygonId]; polygonController?.remove(); _polygonIdToController.remove(polygonId); 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 f8ff2c62191d..eb4b6d88b503 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -6,15 +6,15 @@ part of google_maps_flutter_web; /// The `PolygonController` class wraps a [gmaps.Polyline] and its `onTap` behavior. class PolylineController { - gmaps.Polyline _polyline; + gmaps.Polyline? _polyline; final bool _consumeTapEvents; /// Creates a `PolylineController` that wraps a [gmaps.Polyline] object and its `onTap` behavior. PolylineController({ - @required gmaps.Polyline polyline, + required gmaps.Polyline polyline, bool consumeTapEvents = false, - ui.VoidCallback onTap, + ui.VoidCallback? onTap, }) : _polyline = polyline, _consumeTapEvents = consumeTapEvents { if (onTap != null) { @@ -26,20 +26,26 @@ class PolylineController { /// Returns the wrapped [gmaps.Polyline]. Only used for testing. @visibleForTesting - gmaps.Polyline get line => _polyline; + gmaps.Polyline? get line => _polyline; /// Returns `true` if this Controller will use its own `onTap` handler to consume events. bool get consumeTapEvents => _consumeTapEvents; /// Updates the options of the wrapped [gmaps.Polyline] object. + /// + /// This cannot be called after [remove]. void update(gmaps.PolylineOptions options) { - _polyline.options = options; + assert( + _polyline != null, 'Cannot `update` Polyline after calling `remove`.'); + _polyline!.options = options; } /// Disposes of the currently wrapped [gmaps.Polyline]. void remove() { - _polyline.visible = false; - _polyline.map = null; - _polyline = null; + if (_polyline != null) { + _polyline!.visible = false; + _polyline!.map = null; + _polyline = null; + } } } 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 f24ca4b1bb42..695b29554c04 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -14,7 +14,7 @@ class PolylinesController extends GeometryController { /// Initializes the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. PolylinesController({ - @required StreamController stream, + required StreamController stream, }) : _streamController = stream, _polylineIdToController = Map(); @@ -26,7 +26,7 @@ class PolylinesController extends GeometryController { /// /// Wraps each line into its corresponding [PolylineController]. void addPolylines(Set polylinesToAdd) { - polylinesToAdd?.forEach((polyline) { + polylinesToAdd.forEach((polyline) { _addPolyline(polyline); }); } @@ -50,22 +50,22 @@ class PolylinesController extends GeometryController { /// Updates a set of [Polyline] objects with new options. void changePolylines(Set polylinesToChange) { - polylinesToChange?.forEach((polylineToChange) { + polylinesToChange.forEach((polylineToChange) { _changePolyline(polylineToChange); }); } void _changePolyline(Polyline polyline) { - PolylineController polylineController = - _polylineIdToController[polyline?.polylineId]; + PolylineController? polylineController = + _polylineIdToController[polyline.polylineId]; polylineController ?.update(_polylineOptionsFromPolyline(googleMap, polyline)); } /// Removes a set of [PolylineId]s from the cache. void removePolylines(Set polylineIdsToRemove) { - polylineIdsToRemove?.forEach((polylineId) { - final PolylineController polylineController = + polylineIdsToRemove.forEach((polylineId) { + final PolylineController? polylineController = _polylineIdToController[polylineId]; polylineController?.remove(); _polylineIdToController.remove(polylineId); 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 new file mode 100644 index 000000000000..5eacec5fe867 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/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(flutter/flutter#55000) Remove this file once web-only dart:ui APIs +// 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 new file mode 100644 index 000000000000..f2862af8b704 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/shims/dart_ui_fake.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 '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. + +/// Shim for web_ui engine.PlatformViewRegistry +/// https://github.com/flutter/engine/blob/master/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) {} +} + +/// Shim for web_ui engine.AssetManager. +/// https://github.com/flutter/engine/blob/master/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) {} +} + +/// Signature of callbacks that have no arguments and return no data. +typedef VoidCallback = void Function(); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/shims/dart_ui_real.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/shims/dart_ui_real.dart new file mode 100644 index 000000000000..276b768c76c5 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/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/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 039cc473db5e..ff980eb4c34b 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -17,10 +17,10 @@ typedef LatLngCallback = void Function(gmaps.LatLng latLng); /// instance and our internal `mapId` value. abstract class GeometryController { /// The GMap instance that this controller operates on. - gmaps.GMap googleMap; + late gmaps.GMap googleMap; /// The map ID for events. - int mapId; + late int mapId; /// Binds a `mapId` and the [gmaps.GMap] instance to this controller. void bindToMap(int mapId, gmaps.GMap googleMap) { 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 5a879c2b39b0..c69b8e55fa1c 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -1,7 +1,12 @@ name: google_maps_flutter_web description: Web platform implementation of google_maps_flutter -homepage: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter -version: 0.1.0+2 +repository: https://github.com/flutter/plugins/tree/master/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.0+2 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: @@ -15,21 +20,13 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - meta: ^1.1.7 - google_maps_flutter_platform_interface: ^1.0.4 - google_maps: ^3.0.0 - stream_transform: ^1.2.0 - sanitize_html: ^1.3.0 + google_maps_flutter_platform_interface: ^2.0.1 + google_maps: ^5.1.0 + meta: ^1.3.0 + sanitize_html: ^2.0.0 + stream_transform: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - url_launcher: ^5.2.5 - pedantic: ^1.8.0 - mockito: ^4.1.1 - integration_test: - path: ../../integration_test - -environment: - sdk: ">=2.3.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + pedantic: ^1.10.0 diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/README.md b/packages/google_maps_flutter/google_maps_flutter_web/test/README.md index 7c48d024ba57..7c5b4ad682ba 100644 --- a/packages/google_maps_flutter/google_maps_flutter_web/test/README.md +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/README.md @@ -1,17 +1,5 @@ -# Running browser_tests +## test -Make sure you have updated to the latest Flutter master. +This package uses integration tests for testing. -1. Check what version of Chrome is running on the machine you're running tests on. - -2. Download and install driver for that version from here: - * - -3. Start the driver using `chromedriver --port=4444` - -4. Change into the `test` directory of your clone. - -5. Run tests: `flutter drive -d web-server --browser-name=chrome --target=test_driver/TEST_NAME_integration.dart`, or (in Linux): - - * Single: `./run_test test_driver/TEST_NAME_integration.dart` - * All: `./run_test` +See `example/README.md` for more info. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/pubspec.yaml b/packages/google_maps_flutter/google_maps_flutter_web/test/pubspec.yaml deleted file mode 100644 index 008cc0350430..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter_web/test/pubspec.yaml +++ /dev/null @@ -1,23 +0,0 @@ -name: regular_integration_tests -publish_to: none - -environment: - sdk: ">=2.2.2 <3.0.0" - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - google_maps: ^3.4.4 - flutter_driver: - sdk: flutter - flutter_test: - sdk: flutter - http: ^0.12.2 - mockito: ^4.1.1 - google_maps_flutter_web: - path: ../ - integration_test: - path: ../../../integration_test - diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/run_test b/packages/google_maps_flutter/google_maps_flutter_web/test/run_test deleted file mode 100755 index 74a8526a0fa3..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter_web/test/run_test +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/bash -if pgrep -lf chromedriver > /dev/null; then - echo "chromedriver is running." - - if [ $# -eq 0 ]; then - echo "No target specified, running all tests..." - find test_driver/ -iname *_integration.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --target='{}' - else - echo "Running test target: $1..." - set -x - flutter drive -d web-server --web-port=7357 --browser-name=chrome --target=$1 - fi - - else - echo "chromedriver is not running." -fi - diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_controller_integration.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_controller_integration.dart deleted file mode 100644 index fc21476e9703..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_controller_integration.dart +++ /dev/null @@ -1,513 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:integration_test/integration_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:mockito/mockito.dart'; - -import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; - -class _MockCirclesController extends Mock implements CirclesController {} - -class _MockPolygonsController extends Mock implements PolygonsController {} - -class _MockPolylinesController extends Mock implements PolylinesController {} - -class _MockMarkersController extends Mock implements MarkersController {} - -class _MockGMap extends Mock implements gmaps.GMap { - final onClickController = StreamController.broadcast(); - @override - Stream get onClick => onClickController.stream; - - final onRightclickController = StreamController.broadcast(); - @override - Stream get onRightclick => onRightclickController.stream; - - final onBoundsChangedController = StreamController.broadcast(); - @override - Stream get onBoundsChanged => onBoundsChangedController.stream; - - final onIdleController = StreamController.broadcast(); - @override - Stream get onIdle => onIdleController.stream; -} - -/// Test Google Map Controller -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('GoogleMapController', () { - final int mapId = 33930; - GoogleMapController controller; - StreamController stream; - - // Creates a controller with the default mapId and stream controller, and any `options` needed. - GoogleMapController _createController({Map options}) { - return GoogleMapController( - mapId: mapId, - streamController: stream, - rawOptions: options ?? {}); - } - - setUp(() { - stream = StreamController.broadcast(); - }); - - group('construct/dispose', () { - setUp(() { - controller = _createController(); - }); - - testWidgets('constructor creates widget', (WidgetTester tester) async { - expect(controller.widget, isNotNull); - expect(controller.widget.viewType, endsWith('$mapId')); - }); - - testWidgets('widget is cached when reused', (WidgetTester tester) async { - final first = controller.widget; - final again = controller.widget; - expect(identical(first, again), isTrue); - }); - - testWidgets('dispose closes the stream and removes the widget', - (WidgetTester tester) async { - controller.dispose(); - expect(stream.isClosed, isTrue); - expect(controller.widget, isNull); - }); - }); - - group('init', () { - _MockCirclesController circles; - _MockMarkersController markers; - _MockPolygonsController polygons; - _MockPolylinesController polylines; - _MockGMap map; - - setUp(() { - circles = _MockCirclesController(); - markers = _MockMarkersController(); - polygons = _MockPolygonsController(); - polylines = _MockPolylinesController(); - map = _MockGMap(); - }); - - testWidgets('listens to map events', (WidgetTester tester) async { - controller = _createController(); - controller.debugSetOverrides( - createMap: (_, __) => map, - circles: circles, - markers: markers, - polygons: polygons, - polylines: polylines, - ); - - expect(map.onClickController.hasListener, isFalse); - expect(map.onRightclickController.hasListener, isFalse); - expect(map.onBoundsChangedController.hasListener, isFalse); - expect(map.onIdleController.hasListener, isFalse); - - controller.init(); - - expect(map.onClickController.hasListener, isTrue); - expect(map.onRightclickController.hasListener, isTrue); - expect(map.onBoundsChangedController.hasListener, isTrue); - expect(map.onIdleController.hasListener, isTrue); - }); - - testWidgets('binds geometry controllers to map\'s', - (WidgetTester tester) async { - controller = _createController(); - controller.debugSetOverrides( - createMap: (_, __) => map, - circles: circles, - markers: markers, - polygons: polygons, - polylines: polylines, - ); - - controller.init(); - - verify(circles.bindToMap(mapId, map)); - verify(markers.bindToMap(mapId, map)); - verify(polygons.bindToMap(mapId, map)); - verify(polylines.bindToMap(mapId, map)); - }); - - testWidgets('renders initial geometry', (WidgetTester tester) async { - controller = _createController(options: { - 'circlesToAdd': [ - {'circleId': 'circle-1'} - ], - 'markersToAdd': [ - {'markerId': 'marker-1'} - ], - 'polygonsToAdd': [ - {'polygonId': 'polygon-1'} - ], - 'polylinesToAdd': [ - {'polylineId': 'polyline-1'} - ], - }); - controller.debugSetOverrides( - circles: circles, - markers: markers, - polygons: polygons, - polylines: polylines, - ); - - controller.init(); - - final capturedCircles = - verify(circles.addCircles(captureAny)).captured[0] as Set; - final 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; - - expect(capturedCircles.first.circleId.value, 'circle-1'); - expect(capturedMarkers.first.markerId.value, 'marker-1'); - expect(capturedPolygons.first.polygonId.value, 'polygon-1'); - expect(capturedPolylines.first.polylineId.value, 'polyline-1'); - }); - - group('Initialization options', () { - gmaps.MapOptions capturedOptions; - setUp(() { - capturedOptions = null; - }); - testWidgets('translates initial options', (WidgetTester tester) async { - controller = _createController(options: { - 'options': { - 'mapType': 2, - 'zoomControlsEnabled': true, - } - }); - controller.debugSetOverrides(createMap: (_, options) { - capturedOptions = options; - return map; - }); - - controller.init(); - - expect(capturedOptions, isNotNull); - expect(capturedOptions.mapTypeId, gmaps.MapTypeId.SATELLITE); - expect(capturedOptions.zoomControl, true); - expect(capturedOptions.gestureHandling, 'auto', - reason: - 'by default the map handles zoom/pan gestures internally'); - }); - - testWidgets('disables gestureHandling with scrollGesturesEnabled false', - (WidgetTester tester) async { - controller = _createController(options: { - 'options': { - 'scrollGesturesEnabled': false, - } - }); - controller.debugSetOverrides(createMap: (_, options) { - capturedOptions = options; - return map; - }); - - controller.init(); - - expect(capturedOptions, isNotNull); - expect(capturedOptions.gestureHandling, 'none', - reason: - 'disabling scroll gestures disables all gesture handling'); - }); - - testWidgets('disables gestureHandling with zoomGesturesEnabled false', - (WidgetTester tester) async { - controller = _createController(options: { - 'options': { - 'zoomGesturesEnabled': false, - } - }); - controller.debugSetOverrides(createMap: (_, options) { - capturedOptions = options; - return map; - }); - - controller.init(); - - expect(capturedOptions, isNotNull); - expect(capturedOptions.gestureHandling, 'none', - reason: - 'disabling scroll gestures disables all gesture handling'); - }); - - testWidgets('does not set initial position if absent', - (WidgetTester tester) async { - controller = _createController(); - controller.debugSetOverrides(createMap: (_, options) { - capturedOptions = options; - return map; - }); - - controller.init(); - - expect(capturedOptions, isNotNull); - expect(capturedOptions.zoom, isNull); - expect(capturedOptions.center, isNull); - }); - - testWidgets('sets initial position when passed', - (WidgetTester tester) async { - controller = _createController(options: { - 'initialCameraPosition': { - 'target': [43.308, -5.6910], - 'zoom': 12, - 'bearing': 0, - 'tilt': 0, - } - }); - controller.debugSetOverrides(createMap: (_, options) { - capturedOptions = options; - return map; - }); - - controller.init(); - - expect(capturedOptions, isNotNull); - expect(capturedOptions.zoom, 12); - expect(capturedOptions.center, isNotNull); - }); - }); - - group('Traffic Layer', () { - testWidgets('by default is disabled', (WidgetTester tester) async { - controller = _createController(); - controller.init(); - expect(controller.trafficLayer, isNull); - }); - - testWidgets('initializes with traffic layer', - (WidgetTester tester) async { - controller = _createController(options: { - 'options': { - 'trafficEnabled': true, - } - }); - controller.debugSetOverrides(createMap: (_, __) => map); - controller.init(); - expect(controller.trafficLayer, isNotNull); - }); - }); - }); - - // These are the methods that are delegated to the gmaps.GMap object, that we can mock... - group('Map control methods', () { - _MockGMap map; - - setUp(() { - map = _MockGMap(); - controller = _createController(); - controller.debugSetOverrides(createMap: (_, __) => map); - controller.init(); - }); - - group('updateRawOptions', () { - testWidgets('can update `options`', (WidgetTester tester) async { - controller.updateRawOptions({ - 'mapType': 2, - }); - final options = verify(map.options = captureAny).captured[0]; - - expect(options.mapTypeId, gmaps.MapTypeId.SATELLITE); - }); - - testWidgets('can turn on/off traffic', (WidgetTester tester) async { - expect(controller.trafficLayer, isNull); - - controller.updateRawOptions({ - 'trafficEnabled': true, - }); - - expect(controller.trafficLayer, isNotNull); - - controller.updateRawOptions({ - 'trafficEnabled': false, - }); - - expect(controller.trafficLayer, isNull); - }); - }); - - group('viewport getters', () { - testWidgets('getVisibleRegion', (WidgetTester tester) async { - await controller.getVisibleRegion(); - - verify(map.bounds); - }); - - testWidgets('getZoomLevel', (WidgetTester tester) async { - when(map.zoom).thenReturn(10); - - await controller.getZoomLevel(); - - verify(map.zoom); - }); - }); - - group('moveCamera', () { - testWidgets('newLatLngZoom', (WidgetTester tester) async { - await (controller - .moveCamera(CameraUpdate.newLatLngZoom(LatLng(19, 26), 12))); - - verify(map.zoom = 12); - final captured = verify(map.panTo(captureAny)).captured[0]; - expect(captured.lat, 19); - expect(captured.lng, 26); - }); - }); - - group('map.projection methods', () { - // These are too much for dart mockito, can't mock: - // map.projection.method() (in Javascript ;) ) - }); - }); - - // These are the methods that get forwarded to other controllers, so we just verify calls. - group('Pass-through methods', () { - setUp(() { - controller = _createController(); - }); - - testWidgets('updateCircles', (WidgetTester tester) async { - final mock = _MockCirclesController(); - controller.debugSetOverrides(circles: mock); - - final previous = { - Circle(circleId: CircleId('to-be-updated')), - Circle(circleId: CircleId('to-be-removed')), - }; - - final current = { - Circle(circleId: CircleId('to-be-updated'), visible: false), - Circle(circleId: CircleId('to-be-added')), - }; - - controller.updateCircles(CircleUpdates.from(previous, current)); - - verify(mock.removeCircles({ - CircleId('to-be-removed'), - })); - verify(mock.addCircles({ - Circle(circleId: CircleId('to-be-added')), - })); - verify(mock.changeCircles({ - Circle(circleId: CircleId('to-be-updated'), visible: false), - })); - }); - - testWidgets('updateMarkers', (WidgetTester tester) async { - final mock = _MockMarkersController(); - controller.debugSetOverrides(markers: mock); - - final previous = { - Marker(markerId: MarkerId('to-be-updated')), - Marker(markerId: MarkerId('to-be-removed')), - }; - - final current = { - Marker(markerId: MarkerId('to-be-updated'), visible: false), - Marker(markerId: MarkerId('to-be-added')), - }; - - controller.updateMarkers(MarkerUpdates.from(previous, current)); - - verify(mock.removeMarkers({ - MarkerId('to-be-removed'), - })); - verify(mock.addMarkers({ - Marker(markerId: MarkerId('to-be-added')), - })); - verify(mock.changeMarkers({ - Marker(markerId: MarkerId('to-be-updated'), visible: false), - })); - }); - - testWidgets('updatePolygons', (WidgetTester tester) async { - final mock = _MockPolygonsController(); - controller.debugSetOverrides(polygons: mock); - - final previous = { - Polygon(polygonId: PolygonId('to-be-updated')), - Polygon(polygonId: PolygonId('to-be-removed')), - }; - - final current = { - Polygon(polygonId: PolygonId('to-be-updated'), visible: false), - Polygon(polygonId: PolygonId('to-be-added')), - }; - - controller.updatePolygons(PolygonUpdates.from(previous, current)); - - verify(mock.removePolygons({ - PolygonId('to-be-removed'), - })); - verify(mock.addPolygons({ - Polygon(polygonId: PolygonId('to-be-added')), - })); - verify(mock.changePolygons({ - Polygon(polygonId: PolygonId('to-be-updated'), visible: false), - })); - }); - - testWidgets('updatePolylines', (WidgetTester tester) async { - final mock = _MockPolylinesController(); - controller.debugSetOverrides(polylines: mock); - - final previous = { - Polyline(polylineId: PolylineId('to-be-updated')), - Polyline(polylineId: PolylineId('to-be-removed')), - }; - - final current = { - Polyline(polylineId: PolylineId('to-be-updated'), visible: false), - Polyline(polylineId: PolylineId('to-be-added')), - }; - - controller.updatePolylines(PolylineUpdates.from(previous, current)); - - verify(mock.removePolylines({ - PolylineId('to-be-removed'), - })); - verify(mock.addPolylines({ - Polyline(polylineId: PolylineId('to-be-added')), - })); - verify(mock.changePolylines({ - Polyline(polylineId: PolylineId('to-be-updated'), visible: false), - })); - }); - - testWidgets('infoWindow visibility', (WidgetTester tester) async { - final mock = _MockMarkersController(); - controller.debugSetOverrides(markers: mock); - final markerId = MarkerId('marker-with-infowindow'); - - controller.showInfoWindow(markerId); - - verify(mock.showMarkerInfoWindow(markerId)); - - controller.hideInfoWindow(markerId); - - verify(mock.hideMarkerInfoWindow(markerId)); - - controller.isInfoWindowShown(markerId); - - verify(mock.isInfoWindowShown(markerId)); - }); - }); - }); -} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_controller_integration_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_controller_integration_test.dart deleted file mode 100644 index 39444c0daa24..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_controller_integration_test.dart +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:integration_test/integration_test_driver.dart'; - -Future main() async => integrationDriver(); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_plugin_integration.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_plugin_integration.dart deleted file mode 100644 index 59b5de42400f..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_plugin_integration.dart +++ /dev/null @@ -1,392 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:integration_test/integration_test.dart'; -import 'package:flutter/widgets.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:mockito/mockito.dart'; - -import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; - -class _MockGoogleMapController extends Mock implements GoogleMapController {} - -/// Test GoogleMapsPlugin -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('GoogleMapsPlugin', () { - _MockGoogleMapController controller; - GoogleMapsPlugin plugin; - int reportedMapId; - - void onPlatformViewCreated(int id) { - reportedMapId = id; - } - - setUp(() { - controller = _MockGoogleMapController(); - plugin = GoogleMapsPlugin(); - reportedMapId = null; - }); - - group('init/dispose', () { - group('before buildWidget', () { - testWidgets('init throws assertion', (WidgetTester tester) async { - expect(() => plugin.init(0), throwsAssertionError); - }); - }); - - group('after buildWidget', () { - setUp(() { - plugin.debugSetMapById({0: controller}); - }); - - testWidgets('init initializes controller', (WidgetTester tester) async { - await plugin.init(0); - - verify(controller.init()); - }); - - testWidgets('cannot call methods after dispose', - (WidgetTester tester) async { - plugin.dispose(mapId: 0); - - verify(controller.dispose()); - expect( - () => plugin.init(0), - throwsAssertionError, - reason: 'Method calls should fail after dispose.', - ); - }); - }); - }); - - group('buildView', () { - final testMapId = 33930; - - testWidgets('throws without _webOnlyMapCreationId', - (WidgetTester tester) async { - expect( - () => plugin.buildView({}, null, onPlatformViewCreated), - throwsAssertionError, - reason: - '_webOnlyMapCreationId is mandatory to prevent unnecessary reloads in web.', - ); - }); - - testWidgets( - 'returns an HtmlElementView and caches the controller for later', - (WidgetTester tester) async { - final Map cache = {}; - plugin.debugSetMapById(cache); - - final HtmlElementView widget = plugin.buildView({ - '_webOnlyMapCreationId': testMapId, - }, null, onPlatformViewCreated); - - expect( - widget.viewType, - contains('$testMapId'), - reason: - 'view type should contain the mapId passed when creating the map.', - ); - expect( - reportedMapId, - testMapId, - reason: 'Should call onPlatformViewCreated with the mapId', - ); - expect(cache, contains(testMapId)); - expect( - cache[testMapId], - isNotNull, - reason: 'cached controller cannot be null.', - ); - }); - - testWidgets('returns cached instance if it already exists', - (WidgetTester tester) async { - final expected = HtmlElementView(viewType: 'only-for-testing'); - when(controller.widget).thenReturn(expected); - plugin.debugSetMapById({testMapId: controller}); - - final widget = plugin.buildView({ - '_webOnlyMapCreationId': testMapId, - }, null, onPlatformViewCreated); - - expect(widget, equals(expected)); - expect( - reportedMapId, - isNull, - reason: - 'onPlatformViewCreated should not be called when returning a cached controller', - ); - }); - }); - - group('setMapStyles', () { - String mapStyle = '''[{ - "featureType": "poi.park", - "elementType": "labels.text.fill", - "stylers": [{"color": "#6b9a76"}] - }]'''; - - testWidgets('translates styles for controller', - (WidgetTester tester) async { - plugin.debugSetMapById({0: controller}); - - await plugin.setMapStyle(mapStyle, mapId: 0); - - var captured = - verify(controller.updateRawOptions(captureThat(isMap))).captured[0]; - - expect(captured, contains('styles')); - var styles = captured['styles']; - expect(styles.length, 1); - // Let's peek inside the styles... - var style = styles[0] as gmaps.MapTypeStyle; - expect(style.featureType, gmaps.MapTypeStyleFeatureType.POI_PARK); - expect( - style.elementType, gmaps.MapTypeStyleElementType.LABELS_TEXT_FILL); - expect(style.stylers.length, 1); - expect(style.stylers[0].color, '#6b9a76'); - }); - }); - - // 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; - setUp(() { - plugin.debugSetMapById({mapId: controller}); - }); - // Options - testWidgets('updateMapOptions', (WidgetTester tester) async { - final expectedMapOptions = {'someOption': 12345}; - - await plugin.updateMapOptions(expectedMapOptions, mapId: mapId); - - verify(controller.updateRawOptions(expectedMapOptions)); - }); - // Geometry - testWidgets('updateMarkers', (WidgetTester tester) async { - final expectedUpdates = MarkerUpdates.from(null, null); - - await plugin.updateMarkers(expectedUpdates, mapId: mapId); - - verify(controller.updateMarkers(expectedUpdates)); - }); - testWidgets('updatePolygons', (WidgetTester tester) async { - final expectedUpdates = PolygonUpdates.from(null, null); - - await plugin.updatePolygons(expectedUpdates, mapId: mapId); - - verify(controller.updatePolygons(expectedUpdates)); - }); - testWidgets('updatePolylines', (WidgetTester tester) async { - final expectedUpdates = PolylineUpdates.from(null, null); - - await plugin.updatePolylines(expectedUpdates, mapId: mapId); - - verify(controller.updatePolylines(expectedUpdates)); - }); - testWidgets('updateCircles', (WidgetTester tester) async { - final expectedUpdates = CircleUpdates.from(null, null); - - await plugin.updateCircles(expectedUpdates, mapId: mapId); - - verify(controller.updateCircles(expectedUpdates)); - }); - // Camera - testWidgets('animateCamera', (WidgetTester tester) async { - final expectedUpdates = - CameraUpdate.newLatLng(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)); - - await plugin.moveCamera(expectedUpdates, mapId: mapId); - - verify(controller.moveCamera(expectedUpdates)); - }); - // Viewport - testWidgets('getVisibleRegion', (WidgetTester tester) async { - await plugin.getVisibleRegion(mapId: mapId); - - verify(controller.getVisibleRegion()); - }); - testWidgets('getZoomLevel', (WidgetTester tester) async { - await plugin.getZoomLevel(mapId: mapId); - - verify(controller.getZoomLevel()); - }); - testWidgets('getScreenCoordinate', (WidgetTester tester) async { - final latLng = LatLng(43.3613, -5.8499); - - await plugin.getScreenCoordinate(latLng, mapId: mapId); - - verify(controller.getScreenCoordinate(latLng)); - }); - testWidgets('getLatLng', (WidgetTester tester) async { - final coordinates = ScreenCoordinate(x: 19, y: 26); - - await plugin.getLatLng(coordinates, mapId: mapId); - - verify(controller.getLatLng(coordinates)); - }); - // InfoWindows - testWidgets('showMarkerInfoWindow', (WidgetTester tester) async { - final markerId = MarkerId('testing-123'); - - await plugin.showMarkerInfoWindow(markerId, mapId: mapId); - - verify(controller.showInfoWindow(markerId)); - }); - testWidgets('hideMarkerInfoWindow', (WidgetTester tester) async { - final markerId = MarkerId('testing-123'); - - await plugin.hideMarkerInfoWindow(markerId, mapId: mapId); - - verify(controller.hideInfoWindow(markerId)); - }); - testWidgets('isMarkerInfoWindowShown', (WidgetTester tester) async { - final markerId = MarkerId('testing-123'); - - await plugin.isMarkerInfoWindowShown(markerId, mapId: mapId); - - verify(controller.isInfoWindowShown(markerId)); - }); - }); - - // Verify all event streams are filtered correctly from the main one... - group('Event Streams', () { - int mapId = 0; - StreamController streamController; - setUp(() { - streamController = StreamController.broadcast(); - when(controller.events) - .thenAnswer((realInvocation) => streamController.stream); - plugin.debugSetMapById({mapId: controller}); - }); - - // Dispatches a few events in the global streamController, and expects *only* the passed event to be there. - void _testStreamFiltering(Stream stream, MapEvent event) async { - Timer.run(() { - streamController.add(_OtherMapEvent(mapId)); - streamController.add(event); - streamController.add(_OtherMapEvent(mapId)); - streamController.close(); - }); - - final events = await stream.toList(); - - expect(events.length, 1); - expect(events[0], event); - } - - // Camera events - testWidgets('onCameraMoveStarted', (WidgetTester tester) async { - final event = CameraMoveStartedEvent(mapId); - - final stream = plugin.onCameraMoveStarted(mapId: mapId); - - await _testStreamFiltering(stream, event); - }); - testWidgets('onCameraMoveStarted', (WidgetTester tester) async { - final event = CameraMoveEvent( - mapId, - CameraPosition( - target: LatLng(43.3790, -5.8660), - ), - ); - - final stream = plugin.onCameraMove(mapId: mapId); - - await _testStreamFiltering(stream, event); - }); - testWidgets('onCameraIdle', (WidgetTester tester) async { - final event = CameraIdleEvent(mapId); - - final stream = plugin.onCameraIdle(mapId: mapId); - - await _testStreamFiltering(stream, event); - }); - // Marker events - testWidgets('onMarkerTap', (WidgetTester tester) async { - final event = MarkerTapEvent(mapId, MarkerId('test-123')); - - final stream = plugin.onMarkerTap(mapId: mapId); - - await _testStreamFiltering(stream, event); - }); - testWidgets('onInfoWindowTap', (WidgetTester tester) async { - final event = InfoWindowTapEvent(mapId, MarkerId('test-123')); - - final stream = plugin.onInfoWindowTap(mapId: mapId); - - await _testStreamFiltering(stream, event); - }); - testWidgets('onMarkerDragEnd', (WidgetTester tester) async { - final event = MarkerDragEndEvent( - mapId, - LatLng(43.3677, -5.8372), - MarkerId('test-123'), - ); - - final stream = plugin.onMarkerDragEnd(mapId: mapId); - - await _testStreamFiltering(stream, event); - }); - // Geometry - testWidgets('onPolygonTap', (WidgetTester tester) async { - final event = PolygonTapEvent(mapId, PolygonId('test-123')); - - final stream = plugin.onPolygonTap(mapId: mapId); - - await _testStreamFiltering(stream, event); - }); - testWidgets('onPolylineTap', (WidgetTester tester) async { - final event = PolylineTapEvent(mapId, PolylineId('test-123')); - - final stream = plugin.onPolylineTap(mapId: mapId); - - await _testStreamFiltering(stream, event); - }); - testWidgets('onCircleTap', (WidgetTester tester) async { - final event = CircleTapEvent(mapId, CircleId('test-123')); - - final 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 stream = plugin.onTap(mapId: mapId); - - await _testStreamFiltering(stream, event); - }); - testWidgets('onLongPress', (WidgetTester tester) async { - final event = MapLongPressEvent(mapId, LatLng(43.3608, -5.8425)); - - final stream = plugin.onLongPress(mapId: mapId); - - await _testStreamFiltering(stream, event); - }); - }); - }); -} - -class _OtherMapEvent extends MapEvent { - _OtherMapEvent(int mapId) : super(mapId, null); -} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_plugin_integration_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_plugin_integration_test.dart deleted file mode 100644 index 39444c0daa24..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/google_maps_plugin_integration_test.dart +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:integration_test/integration_test_driver.dart'; - -Future main() async => integrationDriver(); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/marker_integration.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/marker_integration.dart deleted file mode 100644 index 1cada32104af..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/marker_integration.dart +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:integration_test/integration_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:mockito/mockito.dart'; - -class _MockMarker extends Mock implements gmaps.Marker { - final onClickController = StreamController(); - final onDragEndController = StreamController(); - - @override - Stream get onClick => onClickController.stream; - - @override - Stream get onDragend => onDragEndController.stream; -} - -class _MockMouseEvent extends Mock implements gmaps.MouseEvent {} - -class _MockInfoWindow extends Mock implements gmaps.InfoWindow {} - -/// Test Markers -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - bool called = false; - void onTap() { - called = true; - } - - void onDragEnd(gmaps.LatLng _) { - called = true; - } - - setUp(() { - called = false; - }); - - group('MarkerController', () { - _MockMarker marker; - - setUp(() { - marker = _MockMarker(); - }); - - testWidgets('onTap gets called', (WidgetTester tester) async { - MarkerController(marker: marker, onTap: onTap); - // Simulate a click - await marker.onClickController.add(null); - expect(called, isTrue); - }); - - testWidgets('onDragEnd gets called', (WidgetTester tester) async { - when(marker.draggable).thenReturn(true); - MarkerController(marker: marker, onDragEnd: onDragEnd); - // Simulate a drag end - await marker.onDragEndController.add(_MockMouseEvent()); - expect(called, isTrue); - }); - - testWidgets('update', (WidgetTester tester) async { - final controller = MarkerController(marker: marker); - final options = gmaps.MarkerOptions()..draggable = false; - controller.update(options); - verify(marker.options = options); - }); - - testWidgets('infoWindow null, showInfoWindow.', - (WidgetTester tester) async { - final controller = MarkerController(marker: marker); - controller.showInfoWindow(); - expect(controller.infoWindowShown, isFalse); - }); - - testWidgets('showInfoWindow', (WidgetTester tester) async { - final infoWindow = _MockInfoWindow(); - final controller = - MarkerController(marker: marker, infoWindow: infoWindow); - controller.showInfoWindow(); - verify(infoWindow.open(any, any)).called(1); - expect(controller.infoWindowShown, isTrue); - }); - - testWidgets('hideInfoWindow', (WidgetTester tester) async { - final infoWindow = _MockInfoWindow(); - final controller = - MarkerController(marker: marker, infoWindow: infoWindow); - controller.hideInfoWindow(); - verify(infoWindow.close()).called(1); - expect(controller.infoWindowShown, isFalse); - }); - }); -} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/marker_integration_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/marker_integration_test.dart deleted file mode 100644 index 39444c0daa24..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/marker_integration_test.dart +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:integration_test/integration_test_driver.dart'; - -Future main() async => integrationDriver(); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/markers_integration.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/markers_integration.dart deleted file mode 100644 index 76ddf017985c..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/markers_integration.dart +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -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'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('MarkersController', () { - StreamController stream; - MarkersController controller; - - setUp(() { - stream = StreamController(); - controller = MarkersController(stream: stream); - }); - - testWidgets('addMarkers', (WidgetTester tester) async { - final markers = { - Marker(markerId: MarkerId('1')), - 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')))); - }); - - testWidgets('changeMarkers', (WidgetTester tester) async { - final markers = { - Marker(markerId: MarkerId('1')), - }; - controller.addMarkers(markers); - - expect(controller.markers[MarkerId('1')].marker.draggable, isFalse); - - // Update the marker with radius 10 - final updatedMarkers = { - Marker(markerId: MarkerId('1'), draggable: true), - }; - controller.changeMarkers(updatedMarkers); - - expect(controller.markers.length, 1); - expect(controller.markers[MarkerId('1')].marker.draggable, isTrue); - }); - - testWidgets('removeMarkers', (WidgetTester tester) async { - final markers = { - Marker(markerId: MarkerId('1')), - Marker(markerId: MarkerId('2')), - Marker(markerId: MarkerId('3')), - }; - - controller.addMarkers(markers); - - expect(controller.markers.length, 3); - - // Remove some markers... - final markerIdsToRemove = { - MarkerId('1'), - 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')))); - }); - - testWidgets('InfoWindow show/hide', (WidgetTester tester) async { - final markers = { - Marker( - markerId: MarkerId('1'), - infoWindow: InfoWindow(title: "Title", snippet: "Snippet"), - ), - }; - - controller.addMarkers(markers); - - expect(controller.markers[MarkerId('1')].infoWindowShown, isFalse); - - controller.showMarkerInfoWindow(MarkerId('1')); - - expect(controller.markers[MarkerId('1')].infoWindowShown, isTrue); - - controller.hideMarkerInfoWindow(MarkerId('1')); - - expect(controller.markers[MarkerId('1')].infoWindowShown, isFalse); - }); - - // https://github.com/flutter/flutter/issues/64938 - testWidgets('markers with icon:null work', (WidgetTester tester) async { - final markers = { - Marker(markerId: MarkerId('1'), icon: null), - }; - - controller.addMarkers(markers); - - expect(controller.markers.length, 1); - expect(controller.markers[MarkerId('1')].marker.icon, isNull); - }); - }); -} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/markers_integration_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/markers_integration_test.dart deleted file mode 100644 index 39444c0daa24..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/markers_integration_test.dart +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:integration_test/integration_test_driver.dart'; - -Future main() async => integrationDriver(); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shape_integration.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shape_integration.dart deleted file mode 100644 index a05d704850e6..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shape_integration.dart +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:integration_test/integration_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:mockito/mockito.dart'; - -class _MockCircle extends Mock implements gmaps.Circle { - final onClickController = StreamController(); - @override - Stream get onClick => onClickController.stream; -} - -class _MockPolygon extends Mock implements gmaps.Polygon { - final onClickController = StreamController(); - @override - Stream get onClick => onClickController.stream; -} - -class _MockPolyline extends Mock implements gmaps.Polyline { - final onClickController = StreamController(); - @override - Stream get onClick => onClickController.stream; -} - -/// Test Shapes (Circle, Polygon, Polyline) -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - bool called = false; - void onTap() { - called = true; - } - - setUp(() { - called = false; - }); - - group('CircleController', () { - _MockCircle circle; - - setUp(() { - circle = _MockCircle(); - }); - - testWidgets('onTap gets called', (WidgetTester tester) async { - CircleController(circle: circle, consumeTapEvents: true, onTap: onTap); - expect(circle.onClickController.hasListener, isTrue); - // Simulate a click - await circle.onClickController.add(null); - expect(called, isTrue); - }); - - testWidgets('update', (WidgetTester tester) async { - final controller = CircleController(circle: circle); - final options = gmaps.CircleOptions()..draggable = false; - controller.update(options); - verify(circle.options = options); - }); - }); - - group('PolygonController', () { - _MockPolygon polygon; - - setUp(() { - polygon = _MockPolygon(); - }); - - testWidgets('onTap gets called', (WidgetTester tester) async { - PolygonController(polygon: polygon, consumeTapEvents: true, onTap: onTap); - expect(polygon.onClickController.hasListener, isTrue); - // Simulate a click - await polygon.onClickController.add(null); - expect(called, isTrue); - }); - - testWidgets('update', (WidgetTester tester) async { - final controller = PolygonController(polygon: polygon); - final options = gmaps.PolygonOptions()..draggable = false; - controller.update(options); - verify(polygon.options = options); - }); - }); - - group('PolylineController', () { - _MockPolyline polyline; - - setUp(() { - polyline = _MockPolyline(); - }); - - testWidgets('onTap gets called', (WidgetTester tester) async { - PolylineController( - polyline: polyline, consumeTapEvents: true, onTap: onTap); - expect(polyline.onClickController.hasListener, isTrue); - // Simulate a click - await polyline.onClickController.add(null); - expect(called, isTrue); - }); - - testWidgets('update', (WidgetTester tester) async { - final controller = PolylineController(polyline: polyline); - final options = gmaps.PolylineOptions()..draggable = false; - controller.update(options); - verify(polyline.options = options); - }); - }); -} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shape_integration_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shape_integration_test.dart deleted file mode 100644 index 39444c0daa24..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shape_integration_test.dart +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:integration_test/integration_test_driver.dart'; - -Future main() async => integrationDriver(); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shapes_integration.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shapes_integration.dart deleted file mode 100644 index b1bff7edd429..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shapes_integration.dart +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -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'; - -/// Test Shapes (Circle, Polygon, Polyline) -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('CirclesController', () { - StreamController stream; - CirclesController controller; - - setUp(() { - stream = StreamController(); - controller = CirclesController(stream: stream); - }); - - testWidgets('addCircles', (WidgetTester tester) async { - final circles = { - Circle(circleId: CircleId('1')), - 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')))); - }); - - testWidgets('changeCircles', (WidgetTester tester) async { - final circles = { - Circle(circleId: CircleId('1')), - }; - controller.addCircles(circles); - - expect(controller.circles[CircleId('1')].circle.visible, isTrue); - - final updatedCircles = { - Circle(circleId: CircleId('1'), visible: false), - }; - controller.changeCircles(updatedCircles); - - expect(controller.circles.length, 1); - expect(controller.circles[CircleId('1')].circle.visible, isFalse); - }); - - testWidgets('removeCircles', (WidgetTester tester) async { - final circles = { - Circle(circleId: CircleId('1')), - Circle(circleId: CircleId('2')), - Circle(circleId: CircleId('3')), - }; - - controller.addCircles(circles); - - expect(controller.circles.length, 3); - - // Remove some circles... - final circleIdsToRemove = { - CircleId('1'), - 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')))); - }); - }); - - group('PolygonsController', () { - StreamController stream; - PolygonsController controller; - - setUp(() { - stream = StreamController(); - controller = PolygonsController(stream: stream); - }); - - testWidgets('addPolygons', (WidgetTester tester) async { - final polygons = { - Polygon(polygonId: PolygonId('1')), - 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')))); - }); - - testWidgets('changePolygons', (WidgetTester tester) async { - final polygons = { - Polygon(polygonId: PolygonId('1')), - }; - controller.addPolygons(polygons); - - expect(controller.polygons[PolygonId('1')].polygon.visible, isTrue); - - // Update the polygon - final updatedPolygons = { - Polygon(polygonId: PolygonId('1'), visible: false), - }; - controller.changePolygons(updatedPolygons); - - expect(controller.polygons.length, 1); - expect(controller.polygons[PolygonId('1')].polygon.visible, isFalse); - }); - - testWidgets('removePolygons', (WidgetTester tester) async { - final polygons = { - Polygon(polygonId: PolygonId('1')), - Polygon(polygonId: PolygonId('2')), - Polygon(polygonId: PolygonId('3')), - }; - - controller.addPolygons(polygons); - - expect(controller.polygons.length, 3); - - // Remove some polygons... - final polygonIdsToRemove = { - PolygonId('1'), - 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')))); - }); - }); - - group('PolylinesController', () { - StreamController stream; - PolylinesController controller; - - setUp(() { - stream = StreamController(); - controller = PolylinesController(stream: stream); - }); - - testWidgets('addPolylines', (WidgetTester tester) async { - final polylines = { - Polyline(polylineId: PolylineId('1')), - 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')))); - }); - - testWidgets('changePolylines', (WidgetTester tester) async { - final polylines = { - Polyline(polylineId: PolylineId('1')), - }; - controller.addPolylines(polylines); - - expect(controller.lines[PolylineId('1')].line.visible, isTrue); - - final updatedPolylines = { - Polyline(polylineId: PolylineId('1'), visible: false), - }; - controller.changePolylines(updatedPolylines); - - expect(controller.lines.length, 1); - expect(controller.lines[PolylineId('1')].line.visible, isFalse); - }); - - testWidgets('removePolylines', (WidgetTester tester) async { - final polylines = { - Polyline(polylineId: PolylineId('1')), - Polyline(polylineId: PolylineId('2')), - Polyline(polylineId: PolylineId('3')), - }; - - controller.addPolylines(polylines); - - expect(controller.lines.length, 3); - - // Remove some polylines... - final polylineIdsToRemove = { - PolylineId('1'), - 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')))); - }); - }); -} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shapes_integration_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shapes_integration_test.dart deleted file mode 100644 index 39444c0daa24..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter_web/test/test_driver/shapes_integration_test.dart +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:integration_test/integration_test_driver.dart'; - -Future main() async => integrationDriver(); diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/tests_exist_elsewhere_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/test/tests_exist_elsewhere_test.dart new file mode 100644 index 000000000000..442c50144727 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/test/tests_exist_elsewhere_test.dart @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '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/google_maps_flutter/google_maps_flutter_web/test/web/index.html b/packages/google_maps_flutter/google_maps_flutter_web/test/web/index.html deleted file mode 100644 index 3b7e4edc3df1..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter_web/test/web/index.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - Browser Tests - - - - - - - diff --git a/packages/google_sign_in/analysis_options.yaml b/packages/google_sign_in/analysis_options.yaml new file mode 100644 index 000000000000..cda4f6e153e6 --- /dev/null +++ b/packages/google_sign_in/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options_legacy.yaml diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/.gitignore b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/.gitignore deleted file mode 100644 index bb431f0d5b47..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/.gitignore +++ /dev/null @@ -1,75 +0,0 @@ -# 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/ -.dart_tool/ -.flutter-plugins -.flutter-plugins-dependencies -.packages -.pub-cache/ -.pub/ -build/ - -# Android related -**/android/**/gradle-wrapper.jar -**/android/.gradle -**/android/captures/ -**/android/gradlew -**/android/gradlew.bat -**/android/local.properties -**/android/**/GeneratedPluginRegistrant.java - -# iOS/XCode related -**/ios/**/*.mode1v3 -**/ios/**/*.mode2v3 -**/ios/**/*.moved-aside -**/ios/**/*.pbxuser -**/ios/**/*.perspectivev3 -**/ios/**/*sync/ -**/ios/**/.sconsign.dblite -**/ios/**/.tags* -**/ios/**/.vagrant/ -**/ios/**/DerivedData/ -**/ios/**/Icon? -**/ios/**/Pods/ -**/ios/**/.symlinks/ -**/ios/**/profile -**/ios/**/xcuserdata -**/ios/.generated/ -**/ios/Flutter/App.framework -**/ios/Flutter/Flutter.framework -**/ios/Flutter/Flutter.podspec -**/ios/Flutter/Generated.xcconfig -**/ios/Flutter/app.flx -**/ios/Flutter/app.zip -**/ios/Flutter/flutter_assets/ -**/ios/Flutter/flutter_export_environment.sh -**/ios/ServiceDefinitions.json -**/ios/Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!**/ios/**/default.mode1v3 -!**/ios/**/default.mode2v3 -!**/ios/**/default.pbxuser -!**/ios/**/default.perspectivev3 -!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/.metadata b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/.metadata deleted file mode 100644 index 2c91cc0fc35a..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/.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: 5e3e5a2a1a977c34b22f3709109fd237b5cab9c6 - channel: master - -project_type: package diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/CHANGELOG.md b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/CHANGELOG.md deleted file mode 100644 index 7f35c8490e1f..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -## 1.0.0 - -* First published version. diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/LICENSE b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/LICENSE deleted file mode 100644 index b707cc8221fb..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright 2020 The Flutter Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google LLC nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/README.md b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/README.md deleted file mode 100644 index 78ddb4bdc91b..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# extension_google_sign_in_as_googleapis_auth - -A bridge package between Flutter's [`google_sign_in` plugin](https://pub.dev/packages/google_sign_in) and Dart's [`googleapis` package](https://pub.dev/packages/googleapis), that is able to create [`googleapis_auth`-like `AuthClient` instances](https://pub.dev/documentation/googleapis_auth/latest/googleapis_auth.auth/AuthClient-class.html) directly from the `GoogleSignIn` plugin. - -## Usage - -This package is implemented as an [extension method](https://dart.dev/guides/language/extension-methods) on top of the `GoogleSignIn` plugin. - -In order to use it, you need to add a `dependency` to your `pubspec.yaml`. Then, wherever you're importing `package:google_sign_in/google_sign_in.dart`, add the following: - -```dart -... -import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart'; -... -``` - -From that moment on, your `GoogleSignIn` instance will have an additional `Future authenticatedClient()` method that you can call once your sign in is successful to retrieve an `AuthClient`. - -That object can then be used to create instances of `googleapis` API clients: - -```dart -... -final peopleApi = PeopleApi(await _googleSignIn.authenticatedClient()); -final response = await peopleApi.people.connections.list( - 'people/me', - personFields: 'names', -); -... -``` - -## Example - -This package contains a modified version of Flutter's Google Sign In example app that uses `package:googleapis`' API clients, instead of raw http requests. - -See it [here](https://github.com/flutter/plugins/blob/master/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/lib/main.dart). - -The original code (and its license) can be seen [here](https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in/example/lib/main.dart). - -## Testing - -Run tests with `flutter test`. - -## 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/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/README.md b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/README.md deleted file mode 100755 index 689071dcfe8d..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# extension_google_sign_in_example - -Demonstrates how to use the google_sign_in plugin with the `googleapis` package. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](http://flutter.io/). diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/build.gradle b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/build.gradle deleted file mode 100755 index e6da1a0aebf5..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/build.gradle +++ /dev/null @@ -1,64 +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 28 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - applicationId "io.flutter.plugins.googlesigninexample" - 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 - } - } - - testOptions { - unitTests.returnDefaultValues = true - } -} - -flutter { - source '../..' -} - -dependencies { - implementation 'com.google.android.gms:play-services-auth:16.0.1' - testImplementation'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:2.17.0' -} diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/google-services.json b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/google-services.json deleted file mode 100644 index efa524535553..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/google-services.json +++ /dev/null @@ -1,246 +0,0 @@ -{ - "project_info": { - "project_number": "479882132969", - "firebase_url": "https://my-flutter-proj.firebaseio.com", - "project_id": "my-flutter-proj", - "storage_bucket": "my-flutter-proj.appspot.com" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:479882132969:android:c73fd19ff7e2c0be", - "android_client_info": { - "package_name": "io.flutter.plugins.cameraexample" - } - }, - "oauth_client": [ - { - "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs" - } - ], - "services": { - "analytics_service": { - "status": 1 - }, - "appinvite_service": { - "status": 1, - "other_platform_oauth_client": [] - }, - "ads_service": { - "status": 2 - } - } - }, - { - "client_info": { - "mobilesdk_app_id": "1:479882132969:android:632cdf3fc0a17139", - "android_client_info": { - "package_name": "io.flutter.plugins.firebasedynamiclinksexample" - } - }, - "oauth_client": [ - { - "client_id": "479882132969-32qusitiag53931ck80h121ajhlc5a7e.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "io.flutter.plugins.firebasedynamiclinksexample", - "certificate_hash": "e733b7a303250b63e06de6f7c9767c517d69cfa0" - } - }, - { - "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs" - } - ], - "services": { - "analytics_service": { - "status": 1 - }, - "appinvite_service": { - "status": 2, - "other_platform_oauth_client": [ - { - "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", - "client_type": 3 - }, - { - "client_id": "479882132969-gjp4e63ogu2h6guttj2ie6t3f10ic7i8.apps.googleusercontent.com", - "client_type": 2, - "ios_info": { - "bundle_id": "io.flutter.plugins.firebaseMlVisionExample" - } - } - ] - }, - "ads_service": { - "status": 2 - } - } - }, - { - "client_info": { - "mobilesdk_app_id": "1:479882132969:android:ae50362b4bc06086", - "android_client_info": { - "package_name": "io.flutter.plugins.firebasemlvisionexample" - } - }, - "oauth_client": [ - { - "client_id": "479882132969-9pp74fkgmtvt47t9rikc1p861v7n85tn.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "io.flutter.plugins.firebasemlvisionexample", - "certificate_hash": "e733b7a303250b63e06de6f7c9767c517d69cfa0" - } - }, - { - "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs" - } - ], - "services": { - "analytics_service": { - "status": 1 - }, - "appinvite_service": { - "status": 2, - "other_platform_oauth_client": [ - { - "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", - "client_type": 3 - }, - { - "client_id": "479882132969-gjp4e63ogu2h6guttj2ie6t3f10ic7i8.apps.googleusercontent.com", - "client_type": 2, - "ios_info": { - "bundle_id": "io.flutter.plugins.firebaseMlVisionExample" - } - } - ] - }, - "ads_service": { - "status": 2 - } - } - }, - { - "client_info": { - "mobilesdk_app_id": "1:479882132969:android:215a22700e1b466b", - "android_client_info": { - "package_name": "io.flutter.plugins.firebaseperformanceexample" - } - }, - "oauth_client": [ - { - "client_id": "479882132969-8h4kiv8m7ho4tvn6uuujsfcrf69unuf7.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "io.flutter.plugins.firebaseperformanceexample", - "certificate_hash": "e733b7a303250b63e06de6f7c9767c517d69cfa0" - } - }, - { - "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs" - } - ], - "services": { - "analytics_service": { - "status": 1 - }, - "appinvite_service": { - "status": 2, - "other_platform_oauth_client": [ - { - "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", - "client_type": 3 - }, - { - "client_id": "479882132969-gjp4e63ogu2h6guttj2ie6t3f10ic7i8.apps.googleusercontent.com", - "client_type": 2, - "ios_info": { - "bundle_id": "io.flutter.plugins.firebaseMlVisionExample" - } - } - ] - }, - "ads_service": { - "status": 2 - } - } - }, - { - "client_info": { - "mobilesdk_app_id": "1:479882132969:android:5e9f1f89e134dc86", - "android_client_info": { - "package_name": "io.flutter.plugins.googlesigninexample" - } - }, - "oauth_client": [ - { - "client_id": "479882132969-90ml692hkonp587sl0v0rurmnvkekgrg.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "io.flutter.plugins.googlesigninexample", - "certificate_hash": "e733b7a303250b63e06de6f7c9767c517d69cfa0" - } - }, - { - "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyCrZz9T0Pg0rDnpxfNuPBrOxGhXskfebXs" - } - ], - "services": { - "analytics_service": { - "status": 1 - }, - "appinvite_service": { - "status": 2, - "other_platform_oauth_client": [ - { - "client_id": "479882132969-0d20fkjtr1p8evfomfkf3vmi50uajml2.apps.googleusercontent.com", - "client_type": 3 - }, - { - "client_id": "479882132969-gjp4e63ogu2h6guttj2ie6t3f10ic7i8.apps.googleusercontent.com", - "client_type": 2, - "ios_info": { - "bundle_id": "io.flutter.plugins.firebaseMlVisionExample" - } - } - ] - }, - "ads_service": { - "status": 2 - } - } - } - ], - "configuration_version": "1" -} \ No newline at end of file diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/AndroidManifest.xml b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/AndroidManifest.xml deleted file mode 100755 index df80f829c1e7..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/.gitignore b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/.gitignore deleted file mode 100755 index 9eb4563d2ae1..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/.gitignore +++ /dev/null @@ -1 +0,0 @@ -GeneratedPluginRegistrant.java diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1Activity.java b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1Activity.java deleted file mode 100644 index 5ec19822734c..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1Activity.java +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.googlesigninexample; - -import android.os.Bundle; -import io.flutter.plugins.googlesignin.GoogleSignInPlugin; -import io.flutter.view.FlutterMain; - -@SuppressWarnings("deprecation") -public class EmbeddingV1Activity extends io.flutter.app.FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - FlutterMain.startInitialization(this); - super.onCreate(savedInstanceState); - GoogleSignInPlugin.registerWith(registrarFor("io.flutter.plugins.googlesignin")); - } -} diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1ActivityTest.java b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1ActivityTest.java deleted file mode 100644 index 3e7250cea0ee..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.googlesigninexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -@SuppressWarnings("deprecation") -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java deleted file mode 100644 index f9aa77b30e5d..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.googlesigninexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import io.flutter.embedding.android.FlutterActivity; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -public class FlutterActivityTest { - @Rule - public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); -} diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/values/strings.xml b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/values/strings.xml deleted file mode 100644 index c7e28ffcedd1..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - YOUR_WEB_CLIENT_ID - diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/build.gradle b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/build.gradle deleted file mode 100755 index 541636cc492a..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' - } -} - -allprojects { - repositories { - google() - jcenter() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { - project.evaluationDependsOn(':app') -} - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 019065d1d650..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/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/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/AppFrameworkInfo.plist b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100755 index 6c2de8086bcd..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/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/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/Debug.xcconfig b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/Debug.xcconfig deleted file mode 100755 index 9803018ca79d..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Generated.xcconfig" -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/Release.xcconfig b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/Release.xcconfig deleted file mode 100755 index a4a8c604e13d..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Generated.xcconfig" -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/project.pbxproj b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index faaaa58070bd..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,502 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - 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 */; }; -/* 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 */ - 0263E28FA425D1CE928BDE15 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 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 = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - C2FB9CBA01DB0A2DE5F31E12 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 840012C8B5EDBCF56B0E4AC1 /* Pods */ = { - isa = PBXGroup; - children = ( - 5A76713E622F06379AEDEBFA /* Pods-Runner.debug.xcconfig */, - F582639B44581540871D9BB0 /* Pods-Runner.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 */, - 97C146EF1CF9000F007C117D /* Products */, - 840012C8B5EDBCF56B0E4AC1 /* Pods */, - CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - 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 */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */, - 532EA9D341340B1DCD08293D /* [CP] Copy Pods Resources */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 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; - }; -/* 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\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_ROOT}/GoogleSignIn/Resources/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; - }; - 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${PODS_ROOT}/../Flutter/Flutter.framework", - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 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; - }; -/* 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 = io.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 = io.flutter.plugins.googleSignInExample; - 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/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100755 index 3bb3697ef41c..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/AppDelegate.h b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/AppDelegate.h deleted file mode 100644 index d9e18e990f2e..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/AppDelegate.m b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/AppDelegate.m deleted file mode 100644 index f08675707182..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100755 index d22f10b2ab63..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100755 index 28c6bf03016f..000000000000 Binary files a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100755 index 2ccbfd967d96..000000000000 Binary files a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100755 index f091b6b0bca8..000000000000 Binary files a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100755 index 4cde12118dda..000000000000 Binary files a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100755 index d0ef06e7edb8..000000000000 Binary files a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100755 index dcdc2306c285..000000000000 Binary files a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100755 index 2ccbfd967d96..000000000000 Binary files a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100755 index c8f9ed8f5cee..000000000000 Binary files a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100755 index a6d6b8609df0..000000000000 Binary files a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100755 index a6d6b8609df0..000000000000 Binary files a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100755 index 75b2d164a5a9..000000000000 Binary files a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100755 index c4df70d39da7..000000000000 Binary files a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100755 index 6a84f41e14e2..000000000000 Binary files a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100755 index d0e1f5853602..000000000000 Binary files a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100755 index ebf48f603974..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Base.lproj/Main.storyboard b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100755 index f3c28516fb38..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/GoogleService-Info.plist b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/GoogleService-Info.plist deleted file mode 100644 index 6042aab908af..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/GoogleService-Info.plist +++ /dev/null @@ -1,44 +0,0 @@ - - - - - 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/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Info.plist b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Info.plist deleted file mode 100755 index e03ccfe55e37..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/Info.plist +++ /dev/null @@ -1,62 +0,0 @@ - - - - - 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 - - - diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/main.m b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/main.m deleted file mode 100644 index bec320c0bee0..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/lib/main.dart b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/lib/main.dart deleted file mode 100755 index a238ca3bf8b5..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/lib/main.dart +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// ignore_for_file: public_member_api_docs - -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:google_sign_in/google_sign_in.dart'; - -import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart'; -import 'package:googleapis/people/v1.dart'; - -GoogleSignIn _googleSignIn = GoogleSignIn( - scopes: [ - 'email', - 'https://www.googleapis.com/auth/contacts.readonly', - ], -); - -void main() { - runApp( - MaterialApp( - title: 'Google Sign In', - home: SignInDemo(), - ), - ); -} - -class SignInDemo extends StatefulWidget { - @override - State createState() => SignInDemoState(); -} - -class SignInDemoState extends State { - GoogleSignInAccount _currentUser; - String _contactText; - - @override - void initState() { - super.initState(); - _googleSignIn.onCurrentUserChanged.listen((GoogleSignInAccount account) { - setState(() { - _currentUser = account; - }); - if (_currentUser != null) { - _handleGetContact(); - } - }); - _googleSignIn.signInSilently(); - } - - Future _handleGetContact() async { - setState(() { - _contactText = 'Loading contact info...'; - }); - - final peopleApi = PeopleApi(await _googleSignIn.authenticatedClient()); - final response = await peopleApi.people.connections.list( - 'people/me', - personFields: 'names', - ); - - final firstNamedContactName = _pickFirstNamedContact(response.connections); - - setState(() { - if (firstNamedContactName != null) { - _contactText = 'I see you know $firstNamedContactName!'; - } else { - _contactText = 'No contacts to display.'; - } - }); - } - - String _pickFirstNamedContact(List connections) { - return connections - ?.firstWhere( - (person) => person.names != null, - orElse: () => null, - ) - ?.names - ?.firstWhere( - (name) => name.displayName != null, - orElse: () => null, - ) - ?.displayName; - } - - Future _handleSignIn() async { - try { - await _googleSignIn.signIn(); - } catch (error) { - print(error); - } - } - - Future _handleSignOut() => _googleSignIn.disconnect(); - - Widget _buildBody() { - if (_currentUser != null) { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - ListTile( - leading: GoogleUserCircleAvatar( - identity: _currentUser, - ), - title: Text(_currentUser.displayName ?? ''), - subtitle: Text(_currentUser.email ?? ''), - ), - const Text('Signed in successfully.'), - Text(_contactText ?? ''), - RaisedButton( - child: const Text('SIGN OUT'), - onPressed: _handleSignOut, - ), - RaisedButton( - child: const Text('REFRESH'), - onPressed: _handleGetContact, - ), - ], - ); - } else { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - const Text('You are not currently signed in.'), - RaisedButton( - child: const Text('SIGN IN'), - onPressed: _handleSignIn, - ), - ], - ); - } - } - - @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/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml deleted file mode 100755 index 3e64fa2da3f2..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/pubspec.yaml +++ /dev/null @@ -1,24 +0,0 @@ -name: extension_google_sign_in_example -description: Example of Google Sign-In plugin and googleapis. - -dependencies: - flutter: - sdk: flutter - google_sign_in: ^4.4.1 - extension_google_sign_in_as_googleapis_auth: - path: ../ - googleapis: ^0.55.0 - -dev_dependencies: - pedantic: ^1.8.0 - integration_test: - path: ../../../integration_test - flutter_driver: - sdk: flutter - -flutter: - uses-material-design: true - -environment: - sdk: ">=2.0.0-dev.28.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4 <2.0.0" diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/web/index.html b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/web/index.html deleted file mode 100644 index 42a7d93582ba..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/web/index.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - Google Sign-in Example - - - - - diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/lib/extension_google_sign_in_as_googleapis_auth.dart b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/lib/extension_google_sign_in_as_googleapis_auth.dart deleted file mode 100644 index eec45cc0e89a..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/lib/extension_google_sign_in_as_googleapis_auth.dart +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2020 The Flutter Authors -// -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file or at -// https://developers.google.com/open-source/licenses/bsd - -import 'package:meta/meta.dart'; -import 'package:google_sign_in/google_sign_in.dart'; -import 'package:googleapis_auth/auth.dart' as googleapis_auth; -import 'package:http/http.dart' as http; - -/// Extension on [GoogleSignIn] that adds an `authenticatedClient` method. -/// -/// This method can be used to retrieve an authenticated [googleapis_auth.AuthClient] -/// client that can be used with the rest of the `googleapis` libraries. -extension GoogleApisGoogleSignInAuth on GoogleSignIn { - /// Retrieve a `googleapis` authenticated client. - Future authenticatedClient({ - @visibleForTesting GoogleSignInAuthentication debugAuthentication, - @visibleForTesting List debugScopes = const [], - }) async { - final auth = debugAuthentication ?? await currentUser.authentication; - final credentials = googleapis_auth.AccessCredentials( - googleapis_auth.AccessToken( - 'Bearer', - auth.accessToken, - // We don't know when the token expires, so we assume "never" - DateTime.now().toUtc().add(Duration(days: 365)), - ), - null, // We don't have a refreshToken - debugScopes ?? this.scopes, - ); - - return googleapis_auth.authenticatedClient(http.Client(), credentials); - } -} diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/pubspec.yaml b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/pubspec.yaml deleted file mode 100644 index f94b5e505946..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/pubspec.yaml +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright 2020 The Flutter Authors -# -# Use of this source code is governed by a BSD-style -# license that can be found in the LICENSE file or at -# https://developers.google.com/open-source/licenses/bsd - -name: extension_google_sign_in_as_googleapis_auth -description: A bridge package between google_sign_in and googleapis_auth, to create Authenticated Clients from google_sign_in user credentials. -version: 1.0.0 -homepage: https://github.com/flutter/plugins/google_sign_in/extension_google_sign_in_as_googleapis_auth - -dependencies: - flutter: - sdk: flutter - google_sign_in: ^4.4.1 - googleapis_auth: ^0.2.11+1 - meta: ^1.1.8 - http: ^0.12.1 - -dev_dependencies: - mockito: ^4.1.1 - pedantic: ^1.9.0 - flutter_test: - sdk: flutter - -environment: - sdk: ">=2.7.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4 <2.0.0" diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/test/extension_google_sign_in_as_googleapis_auth_test.dart b/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/test/extension_google_sign_in_as_googleapis_auth_test.dart deleted file mode 100644 index 9f86703d4bb8..000000000000 --- a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/test/extension_google_sign_in_as_googleapis_auth_test.dart +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2020 The Flutter Authors -// -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file or at -// https://developers.google.com/open-source/licenses/bsd - -import 'package:google_sign_in/google_sign_in.dart'; -import 'package:googleapis_auth/auth.dart' as auth; -import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart'; -import 'package:mockito/mockito.dart'; -import 'package:flutter_test/flutter_test.dart'; - -// Mocks so I don't have to prepare all the GoogleSignIn environment. -class MockGoogleSignIn extends Mock implements GoogleSignIn {} - -class MockGoogleSignInAuthentication extends Mock - implements GoogleSignInAuthentication {} - -const SOME_FAKE_ACCESS_TOKEN = 'this-is-something-not-null'; -const SOME_FAKE_SCOPES = ['some-scope', 'another-scope']; - -void main() { - GoogleSignIn signIn = MockGoogleSignIn(); - final authMock = MockGoogleSignInAuthentication(); - - setUp(() { - when(authMock.accessToken).thenReturn(SOME_FAKE_ACCESS_TOKEN); - }); - - test('authenticatedClient returns an authenticated client', () async { - final client = await signIn.authenticatedClient( - debugAuthentication: authMock, - ); - expect(client, isA()); - }); - - test('authenticatedClient returned client contains the passed-in credentials', - () async { - final client = await signIn.authenticatedClient( - debugAuthentication: authMock, - debugScopes: SOME_FAKE_SCOPES, - ); - expect(client.credentials.accessToken.data, equals(SOME_FAKE_ACCESS_TOKEN)); - expect(client.credentials.scopes, equals(SOME_FAKE_SCOPES)); - }); -} diff --git a/packages/google_sign_in/google_sign_in/AUTHORS b/packages/google_sign_in/google_sign_in/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/google_sign_in/google_sign_in/CHANGELOG.md b/packages/google_sign_in/google_sign_in/CHANGELOG.md index 8de8a3badc1b..ce0849845e40 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,3 +1,49 @@ +## NEXT + +* Add iOS unit and UI integration test targets. + +## 5.0.4 + +* Migrate maven repo from jcenter to mavenCentral. + +## 5.0.3 + +* Fixed links in `README.md`. +* Added documentation for usage on the web. + +## 5.0.2 + +* Fix flutter/flutter#48602 iOS flow shows account selection, if user is signed in to Google on the device. + +## 5.0.1 + +* Update platforms `init` function to prioritize `clientId` property when available; +* Updates `google_sign_in_platform_interface` version. + +## 5.0.0 + +* Migrate to null safety. + +## 4.5.9 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 4.5.8 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 4.5.7 + +* Update Flutter SDK constraint. + +## 4.5.6 + +* Fix deprecated member warning in tests. + +## 4.5.5 + +* Update android compileSdkVersion to 29. + ## 4.5.4 * Keep handling deprecated Android v1 classes for backward compatibility. diff --git a/packages/google_sign_in/google_sign_in/LICENSE b/packages/google_sign_in/google_sign_in/LICENSE index 26351460d9de..c6823b81eb84 100644 --- a/packages/google_sign_in/google_sign_in/LICENSE +++ b/packages/google_sign_in/google_sign_in/LICENSE @@ -1,4 +1,4 @@ -Copyright 2016, the Flutter project authors. All rights reserved. +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: diff --git a/packages/google_sign_in/google_sign_in/README.md b/packages/google_sign_in/google_sign_in/README.md old mode 100755 new mode 100644 index fc877171c4d2..6ed21c0fedd2 --- a/packages/google_sign_in/google_sign_in/README.md +++ b/packages/google_sign_in/google_sign_in/README.md @@ -1,34 +1,44 @@ -# google_sign_in - -[![pub package](https://img.shields.io/pub/v/google_sign_in.svg)](https://pub.dartlang.org/packages/google_sign_in) +[![pub package](https://img.shields.io/pub/v/google_sign_in.svg)](https://pub.dev/packages/google_sign_in) A Flutter plugin for [Google Sign In](https://developers.google.com/identity/). -*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! +_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! + +## Platform integration -## Android integration +### Android integration -To access Google Sign-In, you'll need to make sure to [register your -application](https://developers.google.com/mobile/add?platform=android). +To access Google Sign-In, you'll need to make sure to +[register your application](https://firebase.google.com/docs/android/setup). You don't need to include the google-services.json file in your app unless you are using Google services that require it. You do need to enable the OAuth APIs -that you want, using the [Google Cloud Platform API -manager](https://console.developers.google.com/). For example, if you -want to mimic the behavior of the Google Sign-In sample app, you'll need to -enable the [Google People API](https://developers.google.com/people/). - -Make sure you've filled out all required fields in the console for [OAuth consent screen](https://console.developers.google.com/apis/credentials/consent). Otherwise, you may encounter `APIException` errors. - -## iOS integration - -1. [First register your application](https://developers.google.com/mobile/add?platform=ios). -2. Make sure the file you download in step 1 is named `GoogleService-Info.plist`. -3. Move or copy `GoogleService-Info.plist` into the `[my_project]/ios/Runner` directory. -4. Open Xcode, then right-click on `Runner` directory and select `Add Files to "Runner"`. +that you want, using the +[Google Cloud Platform API manager](https://console.developers.google.com/). For +example, if you want to mimic the behavior of the Google Sign-In sample app, +you'll need to enable the +[Google People API](https://developers.google.com/people/). + +Make sure you've filled out all required fields in the console for +[OAuth consent screen](https://console.developers.google.com/apis/credentials/consent). +Otherwise, you may encounter `APIException` errors. + +### iOS integration + +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`. +3. Move or copy `GoogleService-Info.plist` into the `[my_project]/ios/Runner` + directory. +4. Open Xcode, then right-click on `Runner` directory and select + `Add Files to "Runner"`. 5. Select `GoogleService-Info.plist` from the file manager. -6. A dialog will show up and ask you to select the targets, select the `Runner` target. -7. Then add the `CFBundleURLTypes` attributes below into the `[my_project]/ios/Runner/Info.plist` file. +6. A dialog will show up and ask you to select the targets, select the `Runner` + target. +7. Then add the `CFBundleURLTypes` attributes below into the + `[my_project]/ios/Runner/Info.plist` file. ```xml @@ -49,23 +59,33 @@ Make sure you've filled out all required fields in the console for [OAuth consen ``` -### iOS additional requirement +#### iOS additional requirement -Note that according to https://developer.apple.com/sign-in-with-apple/get-started, -starting June 30, 2020, apps that use login services must also offer a "Sign in -with Apple" option when submitting to the Apple App Store. +Note that according to +https://developer.apple.com/sign-in-with-apple/get-started, starting June 30, +2020, apps that use login services must also offer a "Sign in with Apple" option +when submitting to the Apple App Store. Consider also using an Apple sign in plugin from pub.dev. -The Flutter Favorite [sign_in_with_apple](https://pub.dev/packages/sign_in_with_apple) -plugin could be an option. +The Flutter Favorite +[sign_in_with_apple](https://pub.dev/packages/sign_in_with_apple) plugin could +be an option. + +### Web integration + +For web integration details, see the +[`google_sign_in_web` package](https://pub.dev/packages/google_sign_in_web). ## Usage ### Import the package -To use this plugin, follow the [plugin installation instructions](https://pub.dartlang.org/packages/google_sign_in#pub-pkg-tab-installing). + +To use this plugin, follow the +[plugin installation instructions](https://pub.dev/packages/google_sign_in/install). ### Use the plugin + Add the following import to your Dart code: ```dart @@ -82,6 +102,7 @@ GoogleSignIn _googleSignIn = GoogleSignIn( ], ); ``` + [Full list of available scopes](https://developers.google.com/identity/protocols/googlescopes). You can now use the `GoogleSignIn` class to authenticate in your Dart code, e.g. @@ -98,13 +119,5 @@ 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). - -## 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. - -## Issues and feedback - -Please file [issues](https://github.com/flutter/flutter/issues/new) -to send feedback or report a bug. Thank you! +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). diff --git a/packages/google_sign_in/google_sign_in/android/build.gradle b/packages/google_sign_in/google_sign_in/android/build.gradle old mode 100755 new mode 100644 index 144739559b5c..a112470c3886 --- a/packages/google_sign_in/google_sign_in/android/build.gradle +++ b/packages/google_sign_in/google_sign_in/android/build.gradle @@ -4,7 +4,7 @@ version '1.0-SNAPSHOT' buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -15,14 +15,14 @@ buildscript { rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } apply plugin: 'com.android.library' android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { minSdkVersion 16 diff --git a/packages/google_sign_in/google_sign_in/android/gradle.properties b/packages/google_sign_in/google_sign_in/android/gradle.properties old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/android/settings.gradle b/packages/google_sign_in/google_sign_in/android/settings.gradle old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/android/src/main/AndroidManifest.xml b/packages/google_sign_in/google_sign_in/android/src/main/AndroidManifest.xml old mode 100755 new mode 100644 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/src/main/java/io/flutter/plugins/googlesignin/BackgroundTaskRunner.java old mode 100755 new mode 100644 index e05130178ec4..b13ec7e3412a --- 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/src/main/java/io/flutter/plugins/googlesignin/BackgroundTaskRunner.java @@ -1,6 +1,6 @@ -// Copyright 2017, the Flutter project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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; 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/src/main/java/io/flutter/plugins/googlesignin/Executors.java old mode 100755 new mode 100644 index ee4273873d8d..824c6da8ec9f --- 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/src/main/java/io/flutter/plugins/googlesignin/Executors.java @@ -1,6 +1,6 @@ -// Copyright 2017, the Flutter project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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; 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/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java old mode 100755 new mode 100644 index dab6f4c4db8e..3a63f785aa9f --- 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/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInPlugin.java @@ -1,6 +1,6 @@ -// Copyright 2017, the Flutter project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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; @@ -136,7 +136,8 @@ public void onMethodCall(MethodCall call, Result result) { String signInOption = call.argument("signInOption"); List requestedScopes = call.argument("scopes"); String hostedDomain = call.argument("hostedDomain"); - delegate.init(result, signInOption, requestedScopes, hostedDomain); + String clientId = call.argument("clientId"); + delegate.init(result, signInOption, requestedScopes, hostedDomain, clientId); break; case METHOD_SIGN_IN_SILENTLY: @@ -188,7 +189,11 @@ public void onMethodCall(MethodCall call, Result result) { public interface IDelegate { /** Initializes this delegate so that it is ready to perform other operations. */ public void init( - Result result, String signInOption, List requestedScopes, String hostedDomain); + Result result, + String signInOption, + List requestedScopes, + String hostedDomain, + String clientId); /** * Returns the account information for the user who is signed in to this app. If no user is @@ -309,7 +314,11 @@ private void checkAndSetPendingOperation(String method, Result result, Object da */ @Override public void init( - Result result, String signInOption, List requestedScopes, String hostedDomain) { + Result result, + String signInOption, + List requestedScopes, + String hostedDomain, + String clientId) { try { GoogleSignInOptions.Builder optionsBuilder; @@ -334,7 +343,10 @@ public void init( context .getResources() .getIdentifier("default_web_client_id", "string", context.getPackageName()); - if (clientIdIdentifier != 0) { + if (!Strings.isNullOrEmpty(clientId)) { + optionsBuilder.requestIdToken(clientId); + optionsBuilder.requestServerAuthCode(clientId); + } else if (clientIdIdentifier != 0) { optionsBuilder.requestIdToken(context.getString(clientIdIdentifier)); optionsBuilder.requestServerAuthCode(context.getString(clientIdIdentifier)); } 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/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInWrapper.java index 985903f1853c..5af0b50136ce 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/src/main/java/io/flutter/plugins/googlesignin/GoogleSignInWrapper.java @@ -1,6 +1,6 @@ -// Copyright 2020, the Flutter project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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; diff --git a/packages/google_sign_in/google_sign_in/example/README.md b/packages/google_sign_in/google_sign_in/example/README.md old mode 100755 new mode 100644 index 78b7274ad37f..0e246e11a8be --- a/packages/google_sign_in/google_sign_in/example/README.md +++ b/packages/google_sign_in/google_sign_in/example/README.md @@ -5,4 +5,4 @@ Demonstrates how to use the google_sign_in plugin. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). diff --git a/packages/google_sign_in/google_sign_in/example/android.iml b/packages/google_sign_in/google_sign_in/example/android.iml old mode 100755 new mode 100644 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 old mode 100755 new mode 100644 index e6da1a0aebf5..2952c3b9c463 --- 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 28 + compileSdkVersion 29 lintOptions { disable 'InvalidPackage' diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/main/AndroidManifest.xml b/packages/google_sign_in/google_sign_in/example/android/app/src/main/AndroidManifest.xml old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/.gitignore b/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/.gitignore old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1Activity.java b/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1Activity.java index 5ec19822734c..f61bb72ba9da 100644 --- a/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1Activity.java +++ b/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1Activity.java @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1ActivityTest.java b/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1ActivityTest.java index 3e7250cea0ee..cfd2fcec9ec3 100644 --- a/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1ActivityTest.java +++ b/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1ActivityTest.java @@ -1,4 +1,4 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java b/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java index f9aa77b30e5d..36787ffd9910 100644 --- a/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java +++ b/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java @@ -1,4 +1,4 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_sign_in/google_sign_in/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/google_sign_in/google_sign_in/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/google_sign_in/google_sign_in/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/google_sign_in/google_sign_in/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/google_sign_in/google_sign_in/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/google_sign_in/google_sign_in/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInPluginTests.java b/packages/google_sign_in/google_sign_in/example/android/app/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInPluginTests.java index acc7996ee94b..f1058760e2de 100644 --- a/packages/google_sign_in/google_sign_in/example/android/app/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInPluginTests.java +++ b/packages/google_sign_in/google_sign_in/example/android/app/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInPluginTests.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_sign_in/google_sign_in/example/android/build.gradle b/packages/google_sign_in/google_sign_in/example/android/build.gradle old mode 100755 new mode 100644 index 541636cc492a..e101ac08df55 --- a/packages/google_sign_in/google_sign_in/example/android/build.gradle +++ b/packages/google_sign_in/google_sign_in/example/android/build.gradle @@ -1,7 +1,7 @@ buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -12,7 +12,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } 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 old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/android/settings.gradle b/packages/google_sign_in/google_sign_in/example/android/settings.gradle old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/example.iml b/packages/google_sign_in/google_sign_in/example/example.iml old mode 100755 new mode 100644 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 old mode 100755 new mode 100644 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 new file mode 100644 index 000000000000..7a1522346e37 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/integration_test/google_sign_in_test.dart @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @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'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('Can initialize the plugin', (WidgetTester tester) async { + GoogleSignIn signIn = GoogleSignIn(); + expect(signIn, isNotNull); + }); +} diff --git a/packages/google_sign_in/google_sign_in/example/ios/Flutter/AppFrameworkInfo.plist b/packages/google_sign_in/google_sign_in/example/ios/Flutter/AppFrameworkInfo.plist old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/ios/Flutter/Debug.xcconfig b/packages/google_sign_in/google_sign_in/example/ios/Flutter/Debug.xcconfig old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/ios/Flutter/Release.xcconfig b/packages/google_sign_in/google_sign_in/example/ios/Flutter/Release.xcconfig old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/ios/Podfile b/packages/google_sign_in/google_sign_in/example/ios/Podfile new file mode 100644 index 000000000000..60e9fb54baa5 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/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/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 faaaa58070bd..143457fc5acb 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,8 +16,28 @@ 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; @@ -33,6 +53,9 @@ /* 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 = ""; }; @@ -50,6 +73,12 @@ 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 */ @@ -61,6 +90,21 @@ ); 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 */ @@ -69,6 +113,8 @@ children = ( 5A76713E622F06379AEDEBFA /* Pods-Runner.debug.xcconfig */, F582639B44581540871D9BB0 /* Pods-Runner.release.xcconfig */, + 37E582FF620A90D0EB2C0851 /* Pods-RunnerTests.debug.xcconfig */, + 45D93D4513839BFEA2AA74FE /* Pods-RunnerTests.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -89,6 +135,8 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, + F76AC1A32666D0540040C8BC /* RunnerTests */, + F76AC1B12666D0610040C8BC /* RunnerUITests */, 97C146EF1CF9000F007C117D /* Products */, 840012C8B5EDBCF56B0E4AC1 /* Pods */, CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, @@ -99,6 +147,8 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + F76AC1A22666D0540040C8BC /* RunnerTests.xctest */, + F76AC1B02666D0610040C8BC /* RunnerUITests.xctest */, ); name = Products; sourceTree = ""; @@ -132,10 +182,29 @@ 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 */ @@ -149,7 +218,6 @@ 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, - 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */, 532EA9D341340B1DCD08293D /* [CP] Copy Pods Resources */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, ); @@ -162,6 +230,43 @@ 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 */ @@ -169,11 +274,21 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; + 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" */; @@ -190,6 +305,8 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + F76AC1A12666D0540040C8BC /* RunnerTests */, + F76AC1AF2666D0610040C8BC /* RunnerUITests */, ); }; /* End PBXProject section */ @@ -207,57 +324,75 @@ ); 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 */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 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 = ( ); - name = "Thin Binary"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-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; }; - 532EA9D341340B1DCD08293D /* [CP] Copy Pods Resources */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", - "${PODS_ROOT}/GoogleSignIn/Resources/GoogleSignIn.bundle", ); - name = "[CP] Copy Pods Resources"; + name = "Thin Binary"; 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; + 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"; }; - 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */ = { + 532EA9D341340B1DCD08293D /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${PODS_ROOT}/../Flutter/Flutter.framework", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh", + "${PODS_ROOT}/GoogleSignIn/Resources/GoogleSignIn.bundle", ); - name = "[CP] Embed Pods Frameworks"; + name = "[CP] Copy Pods Resources"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", + "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/GoogleSignIn.bundle", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; showEnvVarsInLog = 0; }; 9740EEB61CF901F6004384FC /* Run Script */ = { @@ -305,8 +440,37 @@ ); 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; @@ -449,7 +613,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.googleSignInExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.googleSignInExample; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -470,8 +634,60 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.googleSignInExample; + 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; }; @@ -496,6 +712,24 @@ 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.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata old mode 100755 new mode 100644 index 21a3cc14c74e..919434a6254f --- a/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,9 +2,6 @@ - - + location = "self:"> diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme old mode 100755 new mode 100644 index 3bb3697ef41c..f85273f21768 --- a/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -37,6 +37,26 @@ + + + + + + + + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner/AppDelegate.h b/packages/google_sign_in/google_sign_in/example/ios/Runner/AppDelegate.h index d9e18e990f2e..0681d288bb70 100644 --- a/packages/google_sign_in/google_sign_in/example/ios/Runner/AppDelegate.h +++ b/packages/google_sign_in/google_sign_in/example/ios/Runner/AppDelegate.h @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_sign_in/google_sign_in/example/ios/Runner/AppDelegate.m b/packages/google_sign_in/google_sign_in/example/ios/Runner/AppDelegate.m index f08675707182..30b87969f44a 100644 --- a/packages/google_sign_in/google_sign_in/example/ios/Runner/AppDelegate.m +++ b/packages/google_sign_in/google_sign_in/example/ios/Runner/AppDelegate.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/google_sign_in/google_sign_in/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/google_sign_in/google_sign_in/example/ios/Runner/Base.lproj/LaunchScreen.storyboard old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner/Base.lproj/Main.storyboard b/packages/google_sign_in/google_sign_in/example/ios/Runner/Base.lproj/Main.storyboard old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner/Info.plist b/packages/google_sign_in/google_sign_in/example/ios/Runner/Info.plist old mode 100755 new mode 100644 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 bec320c0bee0..f97b9ef5c8a1 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_sign_in/google_sign_in/example/ios/RunnerTests/GoogleSignInTests.m b/packages/google_sign_in/google_sign_in/example/ios/RunnerTests/GoogleSignInTests.m new file mode 100644 index 000000000000..adbf61326c8d --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/ios/RunnerTests/GoogleSignInTests.m @@ -0,0 +1,156 @@ +// Copyright 2013 The Flutter Authors. 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 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) GIDSignIn *mockSharedInstance; + +@end + +@implementation FLTGoogleSignInPluginTest + +- (void)setUp { + [super setUp]; + self.mockBinaryMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + self.mockPluginRegistrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar)); + self.mockSharedInstance = [OCMockObject partialMockForObject:[GIDSignIn sharedInstance]]; + OCMStub(self.mockPluginRegistrar.messenger).andReturn(self.mockBinaryMessenger); + self.plugin = [[FLTGoogleSignInPlugin alloc] init]; + [FLTGoogleSignInPlugin registerWithRegistrar:self.mockPluginRegistrar]; +} + +- (void)tearDown { + [((OCMockObject *)self.mockSharedInstance) stopMocking]; + [super tearDown]; +} + +- (void)testRequestScopesResultErrorIfNotSignedIn { + OCMStub(self.mockSharedInstance.currentUser).andReturn(nil); + + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"requestScopes" + arguments:@{@"scopes" : @[ @"mockScope1" ]}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + __block id result; + [self.plugin handleMethodCall:methodCall + result:^(id r) { + [expectation fulfill]; + result = r; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqualObjects([((FlutterError *)result) code], @"sign_in_required"); +} + +- (void)testRequestScopesIfNoMissingScope { + // Mock Google Signin internal calls + GIDGoogleUser *mockUser = OCMClassMock(GIDGoogleUser.class); + OCMStub(self.mockSharedInstance.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"]; + __block id result; + [self.plugin handleMethodCall:methodCall + result:^(id r) { + [expectation fulfill]; + result = r; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertTrue([result boolValue]); +} + +- (void)testRequestScopesRequestsIfNotGranted { + // Mock Google Signin internal calls + GIDGoogleUser *mockUser = OCMClassMock(GIDGoogleUser.class); + OCMStub(self.mockSharedInstance.currentUser).andReturn(mockUser); + NSArray *requestedScopes = @[ @"mockScope1" ]; + OCMStub(mockUser.grantedScopes).andReturn(@[]); + + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"requestScopes" + arguments:@{@"scopes" : requestedScopes}]; + + [self.plugin handleMethodCall:methodCall + result:^(id r){ + }]; + + XCTAssertTrue([self.mockSharedInstance.scopes containsObject:@"mockScope1"]); + OCMVerify([self.mockSharedInstance signIn]); +} + +- (void)testRequestScopesReturnsFalseIfNotGranted { + // Mock Google Signin internal calls + GIDGoogleUser *mockUser = OCMClassMock(GIDGoogleUser.class); + OCMStub(self.mockSharedInstance.currentUser).andReturn(mockUser); + NSArray *requestedScopes = @[ @"mockScope1" ]; + OCMStub(mockUser.grantedScopes).andReturn(@[]); + + OCMStub([self.mockSharedInstance signIn]).andDo(^(NSInvocation *invocation) { + [((NSObject *)self.plugin) signIn:self.mockSharedInstance + didSignInForUser:mockUser + withError:nil]; + }); + + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"requestScopes" + arguments:@{@"scopes" : requestedScopes}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns false"]; + __block id result; + [self.plugin handleMethodCall:methodCall + result:^(id r) { + [expectation fulfill]; + result = r; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertFalse([result boolValue]); +} + +- (void)testRequestScopesReturnsTrueIfGranted { + // Mock Google Signin internal calls + GIDGoogleUser *mockUser = OCMClassMock(GIDGoogleUser.class); + OCMStub(self.mockSharedInstance.currentUser).andReturn(mockUser); + NSArray *requestedScopes = @[ @"mockScope1" ]; + NSMutableArray *availableScopes = [NSMutableArray new]; + OCMStub(mockUser.grantedScopes).andReturn(availableScopes); + + OCMStub([self.mockSharedInstance signIn]).andDo(^(NSInvocation *invocation) { + [availableScopes addObject:@"mockScope1"]; + [((NSObject *)self.plugin) signIn:self.mockSharedInstance + didSignInForUser:mockUser + withError:nil]; + }); + + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"requestScopes" + arguments:@{@"scopes" : requestedScopes}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + __block id result; + [self.plugin handleMethodCall:methodCall + result:^(id r) { + [expectation fulfill]; + result = r; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertTrue([result boolValue]); +} + +@end diff --git a/packages/google_sign_in/google_sign_in/example/ios/RunnerTests/Info.plist b/packages/google_sign_in/google_sign_in/example/ios/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/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/google_sign_in/google_sign_in/example/ios/RunnerUITests/GoogleSignInUITests.m b/packages/google_sign_in/google_sign_in/example/ios/RunnerUITests/GoogleSignInUITests.m new file mode 100644 index 000000000000..52d8da1b5964 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/ios/RunnerUITests/GoogleSignInUITests.m @@ -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 os.log; +@import XCTest; + +@interface GoogleSignInUITests : XCTestCase +@property(nonatomic, strong) XCUIApplication* app; +@end + +@implementation GoogleSignInUITests + +- (void)setUp { + self.continueAfterFailure = NO; + + self.app = [[XCUIApplication alloc] init]; + [self.app launch]; +} + +- (void)testSignInPopUp { + XCUIApplication* app = self.app; + + 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"); + } + [signInButton tap]; + + [self allowSignInPermissions]; +} + +- (void)allowSignInPermissions { + // The "Sign In" system permissions pop up isn't caught by + // addUIInterruptionMonitorWithDescription. + XCUIApplication* springboard = + [[XCUIApplication alloc] initWithBundleIdentifier:@"com.apple.springboard"]; + XCUIElement* permissionAlert = springboard.alerts.firstMatch; + if ([permissionAlert waitForExistenceWithTimeout:5.0]) { + [permissionAlert.buttons[@"Continue"] tap]; + } else { + os_log(OS_LOG_DEFAULT, "Permission alert not detected, continuing."); + } +} + +@end diff --git a/packages/google_sign_in/google_sign_in/example/ios/RunnerUITests/Info.plist b/packages/google_sign_in/google_sign_in/example/ios/RunnerUITests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/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/google_sign_in/google_sign_in/example/lib/main.dart b/packages/google_sign_in/google_sign_in/example/lib/main.dart old mode 100755 new mode 100644 index 6c66d56085db..c677d4e75bc3 --- a/packages/google_sign_in/google_sign_in/example/lib/main.dart +++ b/packages/google_sign_in/google_sign_in/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -12,6 +12,8 @@ import 'package:flutter/material.dart'; import 'package:google_sign_in/google_sign_in.dart'; GoogleSignIn _googleSignIn = GoogleSignIn( + // Optional clientId + // clientId: '479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u.apps.googleusercontent.com', scopes: [ 'email', 'https://www.googleapis.com/auth/contacts.readonly', @@ -33,31 +35,31 @@ class SignInDemo extends StatefulWidget { } class SignInDemoState extends State { - GoogleSignInAccount _currentUser; - String _contactText; + GoogleSignInAccount? _currentUser; + String _contactText = ''; @override void initState() { super.initState(); - _googleSignIn.onCurrentUserChanged.listen((GoogleSignInAccount account) { + _googleSignIn.onCurrentUserChanged.listen((GoogleSignInAccount? account) { setState(() { _currentUser = account; }); if (_currentUser != null) { - _handleGetContact(); + _handleGetContact(_currentUser!); } }); _googleSignIn.signInSilently(); } - Future _handleGetContact() async { + Future _handleGetContact(GoogleSignInAccount user) async { setState(() { _contactText = "Loading contact info..."; }); final http.Response response = await http.get( - 'https://people.googleapis.com/v1/people/me/connections' - '?requestMask.includeField=person.names', - headers: await _currentUser.authHeaders, + Uri.parse('https://people.googleapis.com/v1/people/me/connections' + '?requestMask.includeField=person.names'), + headers: await user.authHeaders, ); if (response.statusCode != 200) { setState(() { @@ -68,7 +70,7 @@ class SignInDemoState extends State { return; } final Map data = json.decode(response.body); - final String namedContact = _pickFirstNamedContact(data); + final String? namedContact = _pickFirstNamedContact(data); setState(() { if (namedContact != null) { _contactText = "I see you know $namedContact!"; @@ -78,14 +80,14 @@ class SignInDemoState extends State { }); } - String _pickFirstNamedContact(Map data) { - final List connections = data['connections']; - final Map contact = connections?.firstWhere( + String? _pickFirstNamedContact(Map data) { + final List? connections = data['connections']; + final Map? contact = connections?.firstWhere( (dynamic contact) => contact['names'] != null, orElse: () => null, ); if (contact != null) { - final Map name = contact['names'].firstWhere( + final Map? name = contact['names'].firstWhere( (dynamic name) => name['displayName'] != null, orElse: () => null, ); @@ -107,26 +109,27 @@ class SignInDemoState extends State { Future _handleSignOut() => _googleSignIn.disconnect(); Widget _buildBody() { - if (_currentUser != null) { + GoogleSignInAccount? user = _currentUser; + if (user != null) { return Column( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ ListTile( leading: GoogleUserCircleAvatar( - identity: _currentUser, + identity: user, ), - title: Text(_currentUser.displayName ?? ''), - subtitle: Text(_currentUser.email ?? ''), + title: Text(user.displayName ?? ''), + subtitle: Text(user.email), ), const Text("Signed in successfully."), - Text(_contactText ?? ''), - RaisedButton( + Text(_contactText), + ElevatedButton( child: const Text('SIGN OUT'), onPressed: _handleSignOut, ), - RaisedButton( + ElevatedButton( child: const Text('REFRESH'), - onPressed: _handleGetContact, + onPressed: () => _handleGetContact(user), ), ], ); @@ -135,7 +138,7 @@ class SignInDemoState extends State { mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ const Text("You are not currently signed in."), - RaisedButton( + ElevatedButton( child: const Text('SIGN IN'), onPressed: _handleSignIn, ), diff --git a/packages/google_sign_in/google_sign_in/example/pubspec.yaml b/packages/google_sign_in/google_sign_in/example/pubspec.yaml old mode 100755 new mode 100644 index ebf8e82719f2..8ecfbb6c4369 --- a/packages/google_sign_in/google_sign_in/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/example/pubspec.yaml @@ -1,23 +1,29 @@ name: google_sign_in_example description: Example of Google Sign-In plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.12.13+hotfix.4" dependencies: flutter: sdk: flutter google_sign_in: + # When depending on this package from a real application you should use: + # google_sign_in: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ - http: ^0.12.0 + http: ^0.13.0 dev_dependencies: - pedantic: ^1.8.0 + pedantic: ^1.10.0 integration_test: - path: ../../../integration_test + sdk: flutter flutter_driver: sdk: flutter flutter: uses-material-design: true - -environment: - sdk: ">=2.0.0-dev.28.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4 <2.0.0" diff --git a/packages/google_sign_in/google_sign_in/example/test_driver/integration_test.dart b/packages/google_sign_in/google_sign_in/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..6a0e6fa82dbe --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/test_driver/integration_test.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. + +// @dart=2.9 + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/google_sign_in/google_sign_in/example/web/index.html b/packages/google_sign_in/google_sign_in/example/web/index.html index bd373458a2f1..5710c936c2ed 100644 --- a/packages/google_sign_in/google_sign_in/example/web/index.html +++ b/packages/google_sign_in/google_sign_in/example/web/index.html @@ -1,4 +1,7 @@ + diff --git a/packages/google_sign_in/google_sign_in/integration_test/google_sign_in_test.dart b/packages/google_sign_in/google_sign_in/integration_test/google_sign_in_test.dart deleted file mode 100644 index 7b2b8d800778..000000000000 --- a/packages/google_sign_in/google_sign_in/integration_test/google_sign_in_test.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:integration_test/integration_test.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:google_sign_in/google_sign_in.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('Can initialize the plugin', (WidgetTester tester) async { - GoogleSignIn signIn = GoogleSignIn(); - expect(signIn, isNotNull); - }); -} diff --git a/packages/google_sign_in/google_sign_in/ios/Assets/.gitkeep b/packages/google_sign_in/google_sign_in/ios/Assets/.gitkeep old mode 100755 new mode 100644 diff --git a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.h b/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.h index 9474e371e176..cb6b51aab1bf 100644 --- a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.h +++ b/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.h @@ -1,6 +1,6 @@ -// Copyright 2017, the Flutter project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. #import 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 index f42ea575d392..9a67e3381d46 100644 --- a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.m +++ b/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.m @@ -1,6 +1,6 @@ -// Copyright 2017, the Flutter project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. 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 @@ -77,10 +77,22 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result ofType:@"plist"]; if (path) { NSMutableDictionary *plist = [[NSMutableDictionary alloc] initWithContentsOfFile:path]; - [GIDSignIn sharedInstance].clientID = plist[kClientIdKey]; + BOOL hasDynamicClientId = + [[call.arguments valueForKey:@"clientId"] isKindOfClass:[NSString class]]; + + if (hasDynamicClientId) { + [GIDSignIn sharedInstance].clientID = [call.arguments valueForKey:@"clientId"]; + } else { + [GIDSignIn sharedInstance].clientID = plist[kClientIdKey]; + } + [GIDSignIn sharedInstance].serverClientID = plist[kServerClientIdKey]; [GIDSignIn sharedInstance].scopes = call.arguments[@"scopes"]; - [GIDSignIn sharedInstance].hostedDomain = call.arguments[@"hostedDomain"]; + if (call.arguments[@"hostedDomain"] == [NSNull null]) { + [GIDSignIn sharedInstance].hostedDomain = nil; + } else { + [GIDSignIn sharedInstance].hostedDomain = call.arguments[@"hostedDomain"]; + } result(nil); } else { result([FlutterError errorWithCode:@"missing-config" diff --git a/packages/google_sign_in/google_sign_in/ios/Tests/GoogleSignInPluginTest.m b/packages/google_sign_in/google_sign_in/ios/Tests/GoogleSignInPluginTest.m deleted file mode 100644 index f3968a15622e..000000000000 --- a/packages/google_sign_in/google_sign_in/ios/Tests/GoogleSignInPluginTest.m +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -@import Flutter; - -@import XCTest; -@import google_sign_in; -@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) GIDSignIn *mockSharedInstance; - -@end - -@implementation FLTGoogleSignInPluginTest - -- (void)setUp { - [super setUp]; - self.mockBinaryMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); - self.mockPluginRegistrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar)); - self.mockSharedInstance = [OCMockObject partialMockForObject:[GIDSignIn sharedInstance]]; - OCMStub(self.mockPluginRegistrar.messenger).andReturn(self.mockBinaryMessenger); - self.plugin = [[FLTGoogleSignInPlugin alloc] init]; - [FLTGoogleSignInPlugin registerWithRegistrar:self.mockPluginRegistrar]; -} - -- (void)tearDown { - [((OCMockObject *)self.mockSharedInstance) stopMocking]; - [super tearDown]; -} - -- (void)testRequestScopesResultErrorIfNotSignedIn { - OCMStub(self.mockSharedInstance.currentUser).andReturn(nil); - - FlutterMethodCall *methodCall = - [FlutterMethodCall methodCallWithMethodName:@"requestScopes" - arguments:@{@"scopes" : @[ @"mockScope1" ]}]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - __block id result; - [self.plugin handleMethodCall:methodCall - result:^(id r) { - [expectation fulfill]; - result = r; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertEqualObjects([((FlutterError *)result) code], @"sign_in_required"); -} - -- (void)testRequestScopesIfNoMissingScope { - // Mock Google Signin internal calls - GIDGoogleUser *mockUser = OCMClassMock(GIDGoogleUser.class); - OCMStub(self.mockSharedInstance.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"]; - __block id result; - [self.plugin handleMethodCall:methodCall - result:^(id r) { - [expectation fulfill]; - result = r; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertTrue([result boolValue]); -} - -- (void)testRequestScopesRequestsIfNotGranted { - // Mock Google Signin internal calls - GIDGoogleUser *mockUser = OCMClassMock(GIDGoogleUser.class); - OCMStub(self.mockSharedInstance.currentUser).andReturn(mockUser); - NSArray *requestedScopes = @[ @"mockScope1" ]; - OCMStub(mockUser.grantedScopes).andReturn(@[]); - - FlutterMethodCall *methodCall = - [FlutterMethodCall methodCallWithMethodName:@"requestScopes" - arguments:@{@"scopes" : requestedScopes}]; - - [self.plugin handleMethodCall:methodCall - result:^(id r){ - }]; - - XCTAssertTrue([self.mockSharedInstance.scopes containsObject:@"mockScope1"]); - OCMVerify([self.mockSharedInstance signIn]); -} - -- (void)testRequestScopesReturnsFalseIfNotGranted { - // Mock Google Signin internal calls - GIDGoogleUser *mockUser = OCMClassMock(GIDGoogleUser.class); - OCMStub(self.mockSharedInstance.currentUser).andReturn(mockUser); - NSArray *requestedScopes = @[ @"mockScope1" ]; - OCMStub(mockUser.grantedScopes).andReturn(@[]); - - OCMStub([self.mockSharedInstance signIn]).andDo(^(NSInvocation *invocation) { - [((NSObject *)self.plugin) signIn:self.mockSharedInstance - didSignInForUser:mockUser - withError:nil]; - }); - - FlutterMethodCall *methodCall = - [FlutterMethodCall methodCallWithMethodName:@"requestScopes" - arguments:@{@"scopes" : requestedScopes}]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns false"]; - __block id result; - [self.plugin handleMethodCall:methodCall - result:^(id r) { - [expectation fulfill]; - result = r; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertFalse([result boolValue]); -} - -- (void)testRequestScopesReturnsTrueIfGranted { - // Mock Google Signin internal calls - GIDGoogleUser *mockUser = OCMClassMock(GIDGoogleUser.class); - OCMStub(self.mockSharedInstance.currentUser).andReturn(mockUser); - NSArray *requestedScopes = @[ @"mockScope1" ]; - NSMutableArray *availableScopes = [NSMutableArray new]; - OCMStub(mockUser.grantedScopes).andReturn(availableScopes); - - OCMStub([self.mockSharedInstance signIn]).andDo(^(NSInvocation *invocation) { - [availableScopes addObject:@"mockScope1"]; - [((NSObject *)self.plugin) signIn:self.mockSharedInstance - didSignInForUser:mockUser - withError:nil]; - }); - - FlutterMethodCall *methodCall = - [FlutterMethodCall methodCallWithMethodName:@"requestScopes" - arguments:@{@"scopes" : requestedScopes}]; - - XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; - __block id result; - [self.plugin handleMethodCall:methodCall - result:^(id r) { - [expectation fulfill]; - result = r; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertTrue([result boolValue]); -} - -@end 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 old mode 100755 new mode 100644 index 38ce53c6e0c2..bf0b75f2957d --- 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 @@ -20,9 +20,4 @@ Enables Google Sign-In in Flutter apps. s.platform = :ios, '8.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } - - s.test_spec 'Tests' do |test_spec| - test_spec.source_files = 'Tests/**/*' - test_spec.dependency 'OCMock','3.5' - end 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 cf7c3a9f2a3a..b45b09c2d7a7 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 @@ -1,6 +1,6 @@ -// Copyright 2017, the Flutter project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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; @@ -22,13 +22,13 @@ class GoogleSignInAuthentication { final GoogleSignInTokenData _data; /// An OpenID Connect ID token that identifies the user. - String get idToken => _data.idToken; + String? get idToken => _data.idToken; /// The OAuth2 access token to access Google services. - String get accessToken => _data.accessToken; + String? get accessToken => _data.accessToken; /// Server auth code used to access Google Login - String get serverAuthCode => _data.serverAuthCode; + String? get serverAuthCode => _data.serverAuthCode; @override String toString() => 'GoogleSignInAuthentication:$_data'; @@ -44,8 +44,7 @@ class GoogleSignInAccount implements GoogleIdentity { email = data.email, id = data.id, photoUrl = data.photoUrl, - _idToken = data.idToken, - _serverAuthCode = data.serverAuthCode { + _idToken = data.idToken { assert(id != null); } @@ -58,7 +57,7 @@ class GoogleSignInAccount implements GoogleIdentity { static const String kUserRecoverableAuthError = 'user_recoverable_auth'; @override - final String displayName; + final String? displayName; @override final String email; @@ -67,10 +66,9 @@ class GoogleSignInAccount implements GoogleIdentity { final String id; @override - final String photoUrl; + final String? photoUrl; - final String _idToken; - final String _serverAuthCode; + final String? _idToken; final GoogleSignIn _googleSignIn; /// Retrieve [GoogleSignInAuthentication] for this account. @@ -99,11 +97,6 @@ class GoogleSignInAccount implements GoogleIdentity { if (response.idToken == null) { response.idToken = _idToken; } - - if (response.serverAuthCode == null) { - response.serverAuthCode = _serverAuthCode; - } - return GoogleSignInAuthentication._(response); } @@ -112,9 +105,11 @@ class GoogleSignInAccount implements GoogleIdentity { /// /// See also https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization. Future> get authHeaders async { - final String token = (await authentication).accessToken; + final String? token = (await authentication).accessToken; return { "Authorization": "Bearer $token", + // TODO(kevmoo): Use the correct value once it's available from authentication + // See https://github.com/flutter/flutter/issues/80905 "X-Goog-AuthUser": "0", }; } @@ -124,7 +119,7 @@ class GoogleSignInAccount implements GoogleIdentity { /// If client runs into 401 errors using a token, it is expected to call /// this method and grab `authHeaders` once again. Future clearAuthCache() async { - final String token = (await authentication).accessToken; + final String token = (await authentication).accessToken!; await GoogleSignInPlatform.instance.clearAuthCache(token: token); } @@ -137,12 +132,11 @@ class GoogleSignInAccount implements GoogleIdentity { email == otherAccount.email && id == otherAccount.id && photoUrl == otherAccount.photoUrl && - _idToken == otherAccount._idToken && - _serverAuthCode == otherAccount._serverAuthCode; + _idToken == otherAccount._idToken; } @override - int get hashCode => hashValues(displayName, email, id, photoUrl, _idToken, _serverAuthCode); + int get hashCode => hashValues(displayName, email, id, photoUrl, _idToken); @override String toString() { @@ -182,7 +176,7 @@ class GoogleSignIn { /// Factory for creating default sign in user experience. factory GoogleSignIn.standard({ List scopes = const [], - String hostedDomain, + String? hostedDomain, }) { return GoogleSignIn( signInOption: SignInOption.standard, @@ -220,22 +214,22 @@ class GoogleSignIn { final List scopes; /// Domain to restrict sign-in to. - final String hostedDomain; + final String? hostedDomain; /// Client ID being used to connect to google sign-in. Only supported on web. - final String clientId; + final String? clientId; - StreamController _currentUserController = - StreamController.broadcast(); + StreamController _currentUserController = + StreamController.broadcast(); /// Subscribe to this stream to be notified when the current user changes. - Stream get onCurrentUserChanged => + Stream get onCurrentUserChanged => _currentUserController.stream; // Future that completes when we've finished calling `init` on the native side - Future _initialization; + Future? _initialization; - Future _callMethod(Function method) async { + Future _callMethod(Function method) async { await _ensureInitialized(); final dynamic response = await method(); @@ -245,7 +239,7 @@ class GoogleSignIn { : null); } - GoogleSignInAccount _setCurrentUser(GoogleSignInAccount currentUser) { + GoogleSignInAccount? _setCurrentUser(GoogleSignInAccount? currentUser) { if (currentUser != _currentUser) { _currentUser = currentUser; _currentUserController.add(_currentUser); @@ -266,7 +260,7 @@ class GoogleSignIn { } /// The most recently scheduled method call. - Future _lastMethodCall; + Future? _lastMethodCall; /// Returns a [Future] that completes with a success after [future], whether /// it completed with a value or an error. @@ -287,15 +281,15 @@ class GoogleSignIn { /// The optional, named parameter [canSkipCall] lets the plugin know that the /// method call may be skipped, if there's already [_currentUser] information. /// This is used from the [signIn] and [signInSilently] methods. - Future _addMethodCall( + Future _addMethodCall( Function method, { bool canSkipCall = false, }) async { - Future response; + Future response; if (_lastMethodCall == null) { response = _callMethod(method); } else { - response = _lastMethodCall.then((_) { + response = _lastMethodCall!.then((_) { // If after the last completed call `currentUser` is not `null` and requested // method can be skipped (`canSkipCall`), re-use the same authenticated user // instead of making extra call to the native side. @@ -311,8 +305,8 @@ class GoogleSignIn { } /// The currently signed in account, or null if the user is signed out. - GoogleSignInAccount get currentUser => _currentUser; - GoogleSignInAccount _currentUser; + GoogleSignInAccount? get currentUser => _currentUser; + GoogleSignInAccount? _currentUser; /// Attempts to sign in a previously authenticated user without interaction. /// @@ -331,7 +325,7 @@ class GoogleSignIn { /// one of [kSignInRequiredError] (when there is no authenticated user) , /// [kNetworkError] (when a network error occurred) or [kSignInFailedError] /// (when an unknown error occurred). - Future signInSilently({ + Future signInSilently({ bool suppressErrors = true, }) async { try { @@ -362,8 +356,8 @@ class GoogleSignIn { /// a Future which resolves to the same user instance. /// /// Re-authentication can be triggered only after [signOut] or [disconnect]. - Future signIn() { - final Future result = + Future signIn() { + final Future result = _addMethodCall(GoogleSignInPlatform.instance.signIn, canSkipCall: true); bool isCanceled(dynamic error) => error is PlatformException && error.code == kSignInCanceledError; @@ -371,12 +365,12 @@ class GoogleSignIn { } /// Marks current user as being in the signed out state. - Future signOut() => + Future signOut() => _addMethodCall(GoogleSignInPlatform.instance.signOut); /// Disconnects the current user from the app and revokes previous /// authentication. - Future disconnect() => + Future disconnect() => _addMethodCall(GoogleSignInPlatform.instance.disconnect); /// Requests the user grants additional Oauth [scopes]. diff --git a/packages/google_sign_in/google_sign_in/lib/src/common.dart b/packages/google_sign_in/google_sign_in/lib/src/common.dart index 14bed4fe114a..068403e74629 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 @@ -1,6 +1,6 @@ -// Copyright 2017, the Flutter project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. /// Encapsulation of the fields that represent a Google user's identity. abstract class GoogleIdentity { @@ -28,10 +28,10 @@ abstract class GoogleIdentity { /// The display name of the signed in user. /// /// Not guaranteed to be present for all users, even when configured. - String get displayName; + String? get displayName; /// The photo url of the signed in user if the user has a profile picture. /// /// Not guaranteed to be present for all users, even when configured. - String get photoUrl; + String? get photoUrl; } diff --git a/packages/google_sign_in/google_sign_in/lib/src/fife.dart b/packages/google_sign_in/google_sign_in/lib/src/fife.dart index 14ecf5fd6083..ff048e249590 100644 --- a/packages/google_sign_in/google_sign_in/lib/src/fife.dart +++ b/packages/google_sign_in/google_sign_in/lib/src/fife.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_sign_in/google_sign_in/lib/testing.dart b/packages/google_sign_in/google_sign_in/lib/testing.dart index 8d62ff463ca6..c4d2da3089a5 100644 --- a/packages/google_sign_in/google_sign_in/lib/testing.dart +++ b/packages/google_sign_in/google_sign_in/lib/testing.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -32,7 +32,7 @@ class FakeSignInBackend { /// This does not represent the signed-in user, but rather an object that will /// be returned when [GoogleSignIn.signIn] or [GoogleSignIn.signInSilently] is /// called. - FakeUser user; + late FakeUser user; /// Handles method calls that would normally be sent to the native backend. /// Returns with the expected values based on the current [user]. @@ -42,7 +42,7 @@ class FakeSignInBackend { // do nothing return null; case 'getTokens': - return { + return { 'idToken': user.idToken, 'accessToken': user.accessToken, }; @@ -72,24 +72,24 @@ class FakeUser { }); /// Will be converted into [GoogleSignInUserData.id]. - final String id; + final String? id; /// Will be converted into [GoogleSignInUserData.email]. - final String email; + final String? email; /// Will be converted into [GoogleSignInUserData.displayName]. - final String displayName; + final String? displayName; /// Will be converted into [GoogleSignInUserData.photoUrl]. - final String photoUrl; + final String? photoUrl; /// Will be converted into [GoogleSignInTokenData.idToken]. - final String idToken; + final String? idToken; /// Will be converted into [GoogleSignInTokenData.accessToken]. - final String accessToken; + final String? accessToken; - Map get _asMap => { + Map get _asMap => { 'id': id, 'email': email, 'displayName': displayName, diff --git a/packages/google_sign_in/google_sign_in/lib/widgets.dart b/packages/google_sign_in/google_sign_in/lib/widgets.dart index 3375628f47b5..18f9973454f6 100644 --- a/packages/google_sign_in/google_sign_in/lib/widgets.dart +++ b/packages/google_sign_in/google_sign_in/lib/widgets.dart @@ -1,10 +1,9 @@ -// Copyright 2017, the Flutter project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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/foundation.dart'; import 'package:flutter/material.dart'; import 'src/common.dart'; @@ -23,7 +22,7 @@ class GoogleUserCircleAvatar extends StatelessWidget { /// in place of a profile photo, or a default profile photo if the user's /// identity does not specify a `displayName`. const GoogleUserCircleAvatar({ - @required this.identity, + required this.identity, this.placeholderPhotoUrl, this.foregroundColor, this.backgroundColor, @@ -42,13 +41,13 @@ class GoogleUserCircleAvatar extends StatelessWidget { /// The color of the text to be displayed if photo is not available. /// /// If a foreground color is not specified, the theme's text color is used. - final Color foregroundColor; + final Color? foregroundColor; /// The color with which to fill the circle. Changing the background color /// will cause the avatar to animate to the new color. /// /// If a background color is not specified, the theme's primary color is used. - final Color backgroundColor; + final Color? backgroundColor; /// The URL of a photo to use if the user's [identity] does not specify a /// `photoUrl`. @@ -57,7 +56,7 @@ class GoogleUserCircleAvatar extends StatelessWidget { /// then this widget will attempt to display the user's first initial as /// determined from the identity's [displayName] field. If that is `null` a /// default (generic) Google profile photo will be displayed. - final String placeholderPhotoUrl; + final String? placeholderPhotoUrl; @override Widget build(BuildContext context) { @@ -68,38 +67,26 @@ class GoogleUserCircleAvatar extends StatelessWidget { ); } - /// Adds correct sizing information to [photoUrl]. - /// - /// Falls back to the default profile photo if [photoUrl] is [null]. - static String _sizedProfileImageUrl(String photoUrl, double size) { - if (photoUrl == null) { - // If the user has no profile photo and no display name, fall back to - // the default profile photo as a last resort. - return 'https://lh3.googleusercontent.com/a/default-user=s${size.round()}-c'; - } - return fife.addSizeDirectiveToUrl(photoUrl, size); - } - Widget _buildClippedImage(BuildContext context, BoxConstraints constraints) { assert(constraints.maxWidth == constraints.maxHeight); // Placeholder to use when there is no photo URL, and while the photo is // loading. Uses the first character of the display name (if it has one), // or the first letter of the email address if it does not. - final List placeholderCharSources = [ + final List placeholderCharSources = [ identity.displayName, identity.email, '-', ]; final String placeholderChar = placeholderCharSources - .firstWhere((String str) => str != null && str.trimLeft().isNotEmpty) + .firstWhere((String? str) => str != null && str.trimLeft().isNotEmpty)! .trimLeft()[0] .toUpperCase(); final Widget placeholder = Center( child: Text(placeholderChar, textAlign: TextAlign.center), ); - final String photoUrl = identity.photoUrl ?? placeholderPhotoUrl; + final String? photoUrl = identity.photoUrl ?? placeholderPhotoUrl; if (photoUrl == null) { return placeholder; } @@ -107,7 +94,7 @@ class GoogleUserCircleAvatar extends StatelessWidget { // Add a sizing directive to the profile photo URL. final double size = MediaQuery.of(context).devicePixelRatio * constraints.maxWidth; - final String sizedPhotoUrl = _sizedProfileImageUrl(photoUrl, size); + final String sizedPhotoUrl = fife.addSizeDirectiveToUrl(photoUrl, size); // Fade the photo in over the top of the placeholder. return SizedBox( diff --git a/packages/google_sign_in/google_sign_in/pubspec.yaml b/packages/google_sign_in/google_sign_in/pubspec.yaml index 2b7aea7cc95c..a57f2197576d 100644 --- a/packages/google_sign_in/google_sign_in/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/pubspec.yaml @@ -1,8 +1,13 @@ name: google_sign_in description: Flutter plugin for Google Sign-In, a secure authentication system for signing in with a Google account on Android and iOS. -homepage: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in -version: 4.5.4 +repository: https://github.com/flutter/plugins/tree/master/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.0.4 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: @@ -16,27 +21,18 @@ flutter: default_package: google_sign_in_web dependencies: - google_sign_in_platform_interface: ^1.1.1 flutter: sdk: flutter - meta: ^1.0.4 - # The design on https://flutter.dev/go/federated-plugins was to leave - # this constraint as "any". We cannot do it right now as it fails pub publish - # validation, so we set a ^ constraint. - # TODO(amirh): Revisit this (either update this part in the design or the pub tool). - # https://github.com/flutter/flutter/issues/46264 - google_sign_in_web: ^0.9.1 + google_sign_in_platform_interface: ^2.0.1 + google_sign_in_web: ^0.10.0 + meta: ^1.3.0 dev_dependencies: - http: ^0.12.0 flutter_driver: sdk: flutter flutter_test: sdk: flutter - pedantic: ^1.8.0 + http: ^0.13.0 integration_test: - path: ../../integration_test - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4 <2.0.0" + sdk: flutter + pedantic: ^1.10.0 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 bfc4937a7c64..c81454ef0a8c 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter 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/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 old mode 100755 new mode 100644 index 5969edbaba76..f642bcd2eaf8 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -41,8 +41,8 @@ void main() { }; final List log = []; - Map responses; - GoogleSignIn googleSignIn; + late Map responses; + late GoogleSignIn googleSignIn; setUp(() { responses = Map.from(kDefaultResponses); @@ -64,17 +64,27 @@ void main() { expect( log, [ - isMethodCall('init', arguments: { - 'signInOption': 'SignInOption.standard', - 'scopes': [], - 'hostedDomain': null, - }), + _isSignInMethodCall(), isMethodCall('signInSilently', arguments: null), ], ); }); test('signIn', () async { + await googleSignIn.signIn(); + expect(googleSignIn.currentUser, isNotNull); + expect( + log, + [ + _isSignInMethodCall(), + isMethodCall('signIn', arguments: null), + ], + ); + }); + + test('signIn prioritize clientId parameter when available', () async { + final fakeClientId = 'fakeClientId'; + googleSignIn = GoogleSignIn(clientId: fakeClientId); await googleSignIn.signIn(); expect(googleSignIn.currentUser, isNotNull); expect( @@ -84,6 +94,7 @@ void main() { 'signInOption': 'SignInOption.standard', 'scopes': [], 'hostedDomain': null, + 'clientId': fakeClientId, }), isMethodCall('signIn', arguments: null), ], @@ -94,11 +105,7 @@ void main() { await googleSignIn.signOut(); expect(googleSignIn.currentUser, isNull); expect(log, [ - isMethodCall('init', arguments: { - 'signInOption': 'SignInOption.standard', - 'scopes': [], - 'hostedDomain': null, - }), + _isSignInMethodCall(), isMethodCall('signOut', arguments: null), ]); }); @@ -109,11 +116,7 @@ void main() { expect( log, [ - isMethodCall('init', arguments: { - 'signInOption': 'SignInOption.standard', - 'scopes': [], - 'hostedDomain': null, - }), + _isSignInMethodCall(), isMethodCall('disconnect', arguments: null), ], ); @@ -126,11 +129,7 @@ void main() { expect( log, [ - isMethodCall('init', arguments: { - 'signInOption': 'SignInOption.standard', - 'scopes': [], - 'hostedDomain': null, - }), + _isSignInMethodCall(), isMethodCall('disconnect', arguments: null), ], ); @@ -140,11 +139,7 @@ void main() { final bool result = await googleSignIn.isSignedIn(); expect(result, isTrue); expect(log, [ - isMethodCall('init', arguments: { - 'signInOption': 'SignInOption.standard', - 'scopes': [], - 'hostedDomain': null, - }), + _isSignInMethodCall(), isMethodCall('isSignedIn', arguments: null), ]); }); @@ -152,18 +147,14 @@ void main() { test('signIn works even if a previous call throws error in other zone', () async { responses['signInSilently'] = Exception('Not a user'); - await runZoned(() async { + await runZonedGuarded(() async { expect(await googleSignIn.signInSilently(), isNull); - }, onError: (dynamic e, dynamic st) {}); + }, (Object e, StackTrace st) {}); expect(await googleSignIn.signIn(), isNotNull); expect( log, [ - isMethodCall('init', arguments: { - 'signInOption': 'SignInOption.standard', - 'scopes': [], - 'hostedDomain': null, - }), + _isSignInMethodCall(), isMethodCall('signInSilently', arguments: null), isMethodCall('signIn', arguments: null), ], @@ -171,27 +162,23 @@ void main() { }); test('concurrent calls of the same method trigger sign in once', () async { - final List> futures = - >[ + final List> futures = + >[ googleSignIn.signInSilently(), googleSignIn.signInSilently(), ]; expect(futures.first, isNot(futures.last), reason: 'Must return new Future'); - final List users = await Future.wait(futures); + final List users = await Future.wait(futures); expect(googleSignIn.currentUser, isNotNull); - expect(users, [ + expect(users, [ googleSignIn.currentUser, googleSignIn.currentUser ]); expect( log, [ - isMethodCall('init', arguments: { - 'signInOption': 'SignInOption.standard', - 'scopes': [], - 'hostedDomain': null, - }), + _isSignInMethodCall(), isMethodCall('signInSilently', arguments: null), ], ); @@ -204,11 +191,7 @@ void main() { expect( log, [ - isMethodCall('init', arguments: { - 'signInOption': 'SignInOption.standard', - 'scopes': [], - 'hostedDomain': null, - }), + _isSignInMethodCall(), isMethodCall('signInSilently', arguments: null), isMethodCall('signIn', arguments: null), ], @@ -216,21 +199,17 @@ void main() { }); test('concurrent calls of different signIn methods', () async { - final List> futures = - >[ + final List> futures = + >[ googleSignIn.signInSilently(), googleSignIn.signIn(), ]; expect(futures.first, isNot(futures.last)); - final List users = await Future.wait(futures); + final List users = await Future.wait(futures); expect( log, [ - isMethodCall('init', arguments: { - 'signInOption': 'SignInOption.standard', - 'scopes': [], - 'hostedDomain': null, - }), + _isSignInMethodCall(), isMethodCall('signInSilently', arguments: null), ], ); @@ -246,8 +225,8 @@ void main() { }); test('signOut/disconnect methods always trigger native calls', () async { - final List> futures = - >[ + final List> futures = + >[ googleSignIn.signOut(), googleSignIn.signOut(), googleSignIn.disconnect(), @@ -257,11 +236,7 @@ void main() { expect( log, [ - isMethodCall('init', arguments: { - 'signInOption': 'SignInOption.standard', - 'scopes': [], - 'hostedDomain': null, - }), + _isSignInMethodCall(), isMethodCall('signOut', arguments: null), isMethodCall('signOut', arguments: null), isMethodCall('disconnect', arguments: null), @@ -271,8 +246,8 @@ void main() { }); test('queue of many concurrent calls', () async { - final List> futures = - >[ + final List> futures = + >[ googleSignIn.signInSilently(), googleSignIn.signOut(), googleSignIn.signIn(), @@ -282,11 +257,7 @@ void main() { expect( log, [ - isMethodCall('init', arguments: { - 'signInOption': 'SignInOption.standard', - 'scopes': [], - 'hostedDomain': null, - }), + _isSignInMethodCall(), isMethodCall('signInSilently', arguments: null), isMethodCall('signOut', arguments: null), isMethodCall('signIn', arguments: null), @@ -333,11 +304,7 @@ void main() { expect( log, [ - isMethodCall('init', arguments: { - 'signInOption': 'SignInOption.standard', - 'scopes': [], - 'hostedDomain': null, - }), + _isSignInMethodCall(), isMethodCall('signInSilently', arguments: null), ], ); @@ -352,11 +319,7 @@ void main() { expect( log, [ - isMethodCall('init', arguments: { - 'signInOption': 'SignInOption.games', - 'scopes': [], - 'hostedDomain': null, - }), + _isSignInMethodCall(signInOption: 'SignInOption.games'), isMethodCall('signInSilently', arguments: null), ], ); @@ -366,7 +329,7 @@ void main() { await googleSignIn.signIn(); log.clear(); - final GoogleSignInAccount user = googleSignIn.currentUser; + final GoogleSignInAccount user = googleSignIn.currentUser!; final GoogleSignInAuthentication auth = await user.authentication; expect(auth.accessToken, '456'); @@ -391,11 +354,7 @@ void main() { expect( log, [ - isMethodCall('init', arguments: { - 'signInOption': 'SignInOption.standard', - 'scopes': [], - 'hostedDomain': null, - }), + _isSignInMethodCall(), isMethodCall('signIn', arguments: null), isMethodCall('requestScopes', arguments: { 'scopes': ['testScope'], @@ -413,11 +372,11 @@ void main() { photoUrl: "https://lh5.googleusercontent.com/photo.jpg", ); - GoogleSignIn googleSignIn; + late GoogleSignIn googleSignIn; setUp(() { final MethodChannelGoogleSignIn platformInstance = - GoogleSignInPlatform.instance; + GoogleSignInPlatform.instance as MethodChannelGoogleSignIn; platformInstance.channel.setMockMethodCallHandler( (FakeSignInBackend()..user = kUserData).handleMethodCall); googleSignIn = GoogleSignIn(); @@ -430,7 +389,7 @@ void main() { test('can sign in and sign out', () async { await googleSignIn.signIn(); - final GoogleSignInAccount user = googleSignIn.currentUser; + final GoogleSignInAccount user = googleSignIn.currentUser!; expect(user.displayName, equals(kUserData.displayName)); expect(user.email, equals(kUserData.email)); @@ -447,3 +406,12 @@ void main() { }); }); } + +Matcher _isSignInMethodCall({String signInOption = 'SignInOption.standard'}) { + return isMethodCall('init', arguments: { + 'signInOption': signInOption, + 'scopes': [], + 'hostedDomain': null, + 'clientId': null, + }); +} diff --git a/packages/google_sign_in/google_sign_in/test_driver/integration_test.dart b/packages/google_sign_in/google_sign_in/test_driver/integration_test.dart deleted file mode 100644 index f07dba382187..000000000000 --- a/packages/google_sign_in/google_sign_in/test_driver/integration_test.dart +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2020, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'package:flutter_driver/flutter_driver.dart'; - -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String data = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - await driver.close(); - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} diff --git a/packages/google_sign_in/google_sign_in_platform_interface/AUTHORS b/packages/google_sign_in/google_sign_in_platform_interface/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_platform_interface/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md b/packages/google_sign_in/google_sign_in_platform_interface/CHANGELOG.md index aa8ad2cff80f..ee43db685339 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,15 @@ +## 2.0.1 + +* Updates `init` function in `MethodChannelGoogleSignIn` to parametrize `clientId` property. + +## 2.0.0 + +* Migrate to null-safety. + +## 1.1.3 + +* Update Flutter SDK constraint. + ## 1.1.2 * Update lower bound of dart dependency to 2.1.0. diff --git a/packages/google_sign_in/google_sign_in_platform_interface/LICENSE b/packages/google_sign_in/google_sign_in_platform_interface/LICENSE index a6d6c0749818..c6823b81eb84 100644 --- a/packages/google_sign_in/google_sign_in_platform_interface/LICENSE +++ b/packages/google_sign_in/google_sign_in_platform_interface/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017 The Chromium Authors. All rights reserved. +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: diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/google_sign_in_platform_interface.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/google_sign_in_platform_interface.dart index 966e93551086..b3007a896bbc 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 @@ -1,9 +1,9 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; -import 'package:meta/meta.dart' show required, visibleForTesting; +import 'package:meta/meta.dart' show visibleForTesting; import 'src/method_channel_google_sign_in.dart'; import 'src/types.dart'; @@ -78,27 +78,33 @@ abstract class GoogleSignInPlatform { /// /// See: /// https://developers.google.com/identity/sign-in/web/reference#gapiauth2initparams - Future init( - {@required String hostedDomain, - List scopes, - SignInOption signInOption, - String clientId}) async { + Future init({ + List scopes = const [], + SignInOption signInOption = SignInOption.standard, + String? hostedDomain, + String? clientId, + }) async { throw UnimplementedError('init() has not been implemented.'); } /// Attempts to reuse pre-existing credentials to sign in again, without user interaction. - Future signInSilently() async { + Future signInSilently() async { throw UnimplementedError('signInSilently() has not been implemented.'); } /// Signs in the user with the options specified to [init]. - Future signIn() async { + Future signIn() async { throw UnimplementedError('signIn() has not been implemented.'); } + /// Signs in the user with the options specified to [init]. + Future grantOfflineAccess() async { + throw UnimplementedError('grantOfflineAccess() has not been implemented.'); + } + /// Returns the Tokens used to authenticate other API calls. Future getTokens( - {@required String email, bool shouldRecoverAuth}) async { + {required String email, bool? shouldRecoverAuth}) async { throw UnimplementedError('getTokens() has not been implemented.'); } @@ -118,7 +124,7 @@ abstract class GoogleSignInPlatform { } /// Clears any cached information that the plugin may be holding on to. - Future clearAuthCache({@required String token}) async { + Future clearAuthCache({required String token}) async { throw UnimplementedError('clearAuthCache() has not been implemented.'); } diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart index 4d2a34fe0fe7..93a8ea1eb733 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 @@ -1,11 +1,11 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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' show required, visibleForTesting; +import 'package:meta/meta.dart' show visibleForTesting; import '../google_sign_in_platform_interface.dart'; import 'types.dart'; @@ -20,40 +20,49 @@ class MethodChannelGoogleSignIn extends GoogleSignInPlatform { const MethodChannel('plugins.flutter.io/google_sign_in'); @override - Future init( - {@required String hostedDomain, - List scopes = const [], - SignInOption signInOption = SignInOption.standard, - String clientId}) { + Future init({ + List scopes = const [], + SignInOption signInOption = SignInOption.standard, + String? hostedDomain, + String? clientId, + }) { return channel.invokeMethod('init', { 'signInOption': signInOption.toString(), 'scopes': scopes, 'hostedDomain': hostedDomain, + 'clientId': clientId, }); } @override - Future signInSilently() { + Future signInSilently() { return channel .invokeMapMethod('signInSilently') .then(getUserDataFromMap); } @override - Future signIn() { + Future signIn() { return channel .invokeMapMethod('signIn') .then(getUserDataFromMap); } + @override + Future grantOfflineAccess() { + return channel + .invokeMapMethod('grantOfflineAccess') + .then(getUserDataFromMap); + } + @override Future getTokens( - {String email, bool shouldRecoverAuth = true}) { + {required String email, bool? shouldRecoverAuth = true}) { return channel .invokeMapMethod('getTokens', { 'email': email, 'shouldRecoverAuth': shouldRecoverAuth, - }).then(getTokenDataFromMap); + }).then((result) => getTokenDataFromMap(result!)); } @override @@ -67,23 +76,23 @@ class MethodChannelGoogleSignIn extends GoogleSignInPlatform { } @override - Future isSignedIn() { - return channel.invokeMethod('isSignedIn'); + Future isSignedIn() async { + return (await channel.invokeMethod('isSignedIn'))!; } @override - Future clearAuthCache({String token}) { + Future clearAuthCache({String? token}) { return channel.invokeMethod( 'clearAuthCache', - {'token': token}, + {'token': token}, ); } @override - Future requestScopes(List scopes) { - return channel.invokeMethod( + Future requestScopes(List scopes) async { + return (await channel.invokeMethod( 'requestScopes', >{'scopes': scopes}, - ); + ))!; } } diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart index cdeb079fc4a4..61231d1b70b9 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -24,15 +24,19 @@ enum SignInOption { /// Holds information about the signed in user. class GoogleSignInUserData { - /// Uses the given data to construct an instance. Any of these parameters - /// could be null. - GoogleSignInUserData( - {this.displayName, this.email, this.id, this.photoUrl, this.idToken, this.serverAuthCode}); + /// Uses the given data to construct an instance. + GoogleSignInUserData({ + required this.email, + required this.id, + this.displayName, + this.photoUrl, + this.idToken, + }); /// The display name of the signed in user. /// /// Not guaranteed to be present for all users, even when configured. - String displayName; + String? displayName; /// The email address of the signed in user. /// @@ -56,19 +60,15 @@ class GoogleSignInUserData { /// The photo url of the signed in user if the user has a profile picture. /// /// Not guaranteed to be present for all users, even when configured. - String photoUrl; + String? photoUrl; /// A token that can be sent to your own server to verify the authentication /// data. - String idToken; - - /// Authorization code required to make API calls from the server. - /// Read more on - String serverAuthCode; + String? idToken; @override int get hashCode => - hashObjects([displayName, email, id, photoUrl, idToken, serverAuthCode]); + hashObjects([displayName, email, id, photoUrl, idToken]); @override bool operator ==(dynamic other) { @@ -79,14 +79,13 @@ class GoogleSignInUserData { otherUserData.email == email && otherUserData.id == id && otherUserData.photoUrl == photoUrl && - otherUserData.idToken == idToken && - otherUserData.serverAuthCode == serverAuthCode; + otherUserData.idToken == idToken; } } /// Holds authentication data after sign in. class GoogleSignInTokenData { - /// Either or both parameters may be null. + /// Build `GoogleSignInTokenData`. GoogleSignInTokenData({ this.idToken, this.accessToken, @@ -94,13 +93,13 @@ class GoogleSignInTokenData { }); /// An OpenID Connect ID token for the authenticated user. - String idToken; + String? idToken; /// The OAuth2 access token used to access Google services. - String accessToken; + String? accessToken; /// Server auth code used to access Google Login - String serverAuthCode; + String? serverAuthCode; @override int get hashCode => hash3(idToken, accessToken, serverAuthCode); diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart index 5c91b637d8df..3aefa193715b 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 @@ -1,18 +1,18 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. 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_sign_in_platform_interface.dart'; /// Converts user data coming from native code into the proper platform interface type. -GoogleSignInUserData getUserDataFromMap(Map data) { +GoogleSignInUserData? getUserDataFromMap(Map? data) { if (data == null) { return null; } return GoogleSignInUserData( + email: data['email']!, + id: data['id']!, displayName: data['displayName'], - email: data['email'], - id: data['id'], photoUrl: data['photoUrl'], idToken: data['idToken'], serverAuthCode: data['serverAuthCode'],); @@ -20,9 +20,6 @@ GoogleSignInUserData getUserDataFromMap(Map data) { /// Converts token data coming from native code into the proper platform interface type. GoogleSignInTokenData getTokenDataFromMap(Map data) { - if (data == null) { - return null; - } return GoogleSignInTokenData( idToken: data['idToken'], accessToken: data['accessToken'], diff --git a/packages/google_sign_in/google_sign_in_platform_interface/pubspec.yaml b/packages/google_sign_in/google_sign_in_platform_interface/pubspec.yaml index 7bc63d84110c..75b3d98b562d 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,22 +1,23 @@ name: google_sign_in_platform_interface description: A common platform interface for the google_sign_in plugin. -homepage: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in_platform_interface +repository: https://github.com/flutter/plugins/tree/master/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: 1.1.2 +version: 2.0.1 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" dependencies: flutter: sdk: flutter - meta: ^1.0.5 - quiver: ">=2.0.0 <3.0.0" + meta: ^1.3.0 + quiver: ^3.0.0 dev_dependencies: flutter_test: sdk: flutter - mockito: ^4.1.1 - pedantic: ^1.8.0 - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + 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 f411b8992821..b3ac51b7fa52 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -15,7 +15,7 @@ void main() { test('Cannot be implemented with `implements`', () { expect(() { GoogleSignInPlatform.instance = ImplementsGoogleSignInPlatform(); - }, throwsAssertionError); + }, throwsA(isA())); }); test('Can be extended', () { @@ -23,14 +23,16 @@ void main() { }); test('Can be mocked with `implements`', () { - final ImplementsGoogleSignInPlatform mock = - ImplementsGoogleSignInPlatform(); - when(mock.isMock).thenReturn(true); - GoogleSignInPlatform.instance = mock; + GoogleSignInPlatform.instance = ImplementsWithIsMock(); }); }); } +class ImplementsWithIsMock extends Mock implements GoogleSignInPlatform { + @override + bool get isMock => true; +} + class ImplementsGoogleSignInPlatform extends Mock implements GoogleSignInPlatform {} diff --git a/packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart b/packages/google_sign_in/google_sign_in_platform_interface/test/method_channel_google_sign_in_test.dart index 5ac34ade1b8d..390c12583a79 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -29,10 +29,12 @@ const Map kDefaultResponses = { 'disconnect': null, 'isSignedIn': true, 'getTokens': kTokenData, + 'requestScopes': true, }; -final GoogleSignInUserData kUser = getUserDataFromMap(kUserData); -final GoogleSignInTokenData kToken = getTokenDataFromMap(kTokenData); +final GoogleSignInUserData? kUser = getUserDataFromMap(kUserData); +final GoogleSignInTokenData? kToken = + getTokenDataFromMap(kTokenData as Map); void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -42,7 +44,8 @@ void main() { final MethodChannel channel = googleSignIn.channel; final List log = []; - Map responses; // Some tests mutate some kDefaultResponses + late Map + responses; // Some tests mutate some kDefaultResponses setUp(() { responses = Map.from(kDefaultResponses); @@ -97,11 +100,12 @@ void main() { hostedDomain: 'example.com', scopes: ['two', 'scopes'], signInOption: SignInOption.games, - clientId: 'UNUSED!'); + clientId: 'fakeClientId'); }: isMethodCall('init', arguments: { 'hostedDomain': 'example.com', 'scopes': ['two', 'scopes'], 'signInOption': 'SignInOption.games', + 'clientId': 'fakeClientId', }), () { googleSignIn.getTokens( diff --git a/packages/google_sign_in/google_sign_in_web/AUTHORS b/packages/google_sign_in/google_sign_in_web/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/google_sign_in/google_sign_in_web/CHANGELOG.md b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md index d71badc53bfc..a5c9e9d2f2bb 100644 --- a/packages/google_sign_in/google_sign_in_web/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in_web/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.10.0 + +* Migrate to null-safety. + +## 0.9.2+1 + +* Update Flutter SDK constraint. + ## 0.9.2 * Throw PlatformExceptions from where the GMaps SDK may throw exceptions: `init()` and `signIn()`. diff --git a/packages/google_sign_in/google_sign_in_web/LICENSE b/packages/google_sign_in/google_sign_in_web/LICENSE index 26351460d9de..c6823b81eb84 100644 --- a/packages/google_sign_in/google_sign_in_web/LICENSE +++ b/packages/google_sign_in/google_sign_in_web/LICENSE @@ -1,4 +1,4 @@ -Copyright 2016, the Flutter project authors. All rights reserved. +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: 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 7a44e998f3c3..faf04de024af 100644 --- a/packages/google_sign_in/google_sign_in_web/README.md +++ b/packages/google_sign_in/google_sign_in_web/README.md @@ -96,7 +96,7 @@ See the [google_sign_in.dart](https://github.com/flutter/plugins/blob/master/pac ## Contributions and Testing -Tests are a crucial to contributions to this package. All new contributions should be reasonably tested. +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. diff --git a/packages/google_sign_in/google_sign_in_web/example/README.md b/packages/google_sign_in/google_sign_in_web/example/README.md new file mode 100644 index 000000000000..0ec01e025570 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/README.md @@ -0,0 +1,21 @@ +# Testing + +This package utilizes the `integration_test` package to run its tests in a web browser. + +See [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) for more info. + +## Running the tests + +Make sure you have updated to the latest Flutter master. + +1. Check what version of Chrome is running on the machine you're running tests on. + +2. Download and install driver for that version from here: + * + +3. Start the driver using `chromedriver --port=4444` + +4. Run tests: `flutter drive -d web-server --browser-name=chrome --driver=test_driver/integration_driver.dart --target=integration_test/TEST_NAME.dart`, or (in Linux): + + * Single: `./run_test.sh integration_test/TEST_NAME.dart` + * All: `./run_test.sh` diff --git a/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_test.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_test.dart new file mode 100644 index 000000000000..e1a97cee6cf7 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/auth2_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 '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(); + + GoogleSignInTokenData expectedTokenData = + GoogleSignInTokenData(idToken: '70k3n', accessToken: 'access_70k3n'); + + 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.init() 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 { + await expectLater( + plugin.init( + hostedDomain: 'foo', + scopes: ['some', 'scope'], + clientId: '1234', + ), + throwsA(isA())); + }); + + testWidgets('init forwards error code from JS', + (WidgetTester tester) async { + try { + await plugin.init( + hostedDomain: 'foo', + scopes: ['some', 'scope'], + clientId: '1234', + ); + fail('plugin.init should have thrown an exception!'); + } catch (e) { + final String code = js_util.getProperty(e, 'code') as String; + 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... + 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); + }); + + group('Successful .init, then', () { + setUp(() async { + await plugin.init( + hostedDomain: 'foo', + scopes: ['some', 'scope'], + clientId: '1234', + ); + await plugin.initialized; + }); + + testWidgets('signInSilently', (WidgetTester tester) async { + GoogleSignInUserData actualUser = (await plugin.signInSilently())!; + + expect(actualUser, expectedUserData); + }); + + testWidgets('signIn', (WidgetTester tester) async { + GoogleSignInUserData actualUser = (await plugin.signIn())!; + + expect(actualUser, expectedUserData); + }); + + testWidgets('getTokens', (WidgetTester tester) async { + GoogleSignInTokenData actualToken = + await plugin.getTokens(email: expectedUserData.email); + + expect(actualToken, expectedTokenData); + }); + + testWidgets('requestScopes', (WidgetTester tester) async { + 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') as String; + expect(code, 'popup_closed_by_user'); + } + }); + }); +} 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 new file mode 100644 index 000000000000..5da42283367f --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_load_test.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 '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/test/test_driver/gapi_mocks/gapi_mocks.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/gapi_mocks.dart similarity index 84% rename from packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_mocks/gapi_mocks.dart rename to packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/gapi_mocks.dart index 9a7d4f403f97..43eb9a55d06b 100644 --- a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_mocks/gapi_mocks.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/gapi_mocks.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_sign_in/google_sign_in_web/test/test_driver/gapi_mocks/src/auth2_init.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/auth2_init.dart similarity index 97% rename from packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_mocks/src/auth2_init.dart rename to packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/auth2_init.dart index 79d798ad2c85..2a085ccf3588 100644 --- a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_mocks/src/auth2_init.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/auth2_init.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_sign_in/google_sign_in_web/test/test_driver/gapi_mocks/src/gapi.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/gapi.dart similarity index 82% rename from packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_mocks/src/gapi.dart rename to packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/gapi.dart index 42d9a8be262c..0e652c647a38 100644 --- a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_mocks/src/gapi.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/gapi.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_sign_in/google_sign_in_web/test/test_driver/gapi_mocks/src/google_user.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/google_user.dart similarity index 92% rename from packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_mocks/src/google_user.dart rename to packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/google_user.dart index f8e794ae48a5..e5e6eb262502 100644 --- a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_mocks/src/google_user.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/google_user.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_sign_in/google_sign_in_web/test/test_driver/gapi_mocks/src/test_iife.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/test_iife.dart similarity index 87% rename from packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_mocks/src/test_iife.dart rename to packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/test_iife.dart index 43a7a044fc1b..c5aac367c1de 100644 --- a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_mocks/src/test_iife.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/test_iife.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/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 new file mode 100644 index 000000000000..1447093d4115 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_utils_test.dart @@ -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 'package:flutter_test/flutter_test.dart'; +import 'package:google_sign_in_web/src/generated/gapiauth2.dart' as gapi; +import 'package:google_sign_in_web/src/utils.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + // The non-null use cases are covered by the auth2_test.dart file. + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('gapiUserToPluginUserData', () { + late FakeGoogleUser fakeUser; + + setUp(() { + fakeUser = FakeGoogleUser(); + }); + + testWidgets('null user -> null response', (WidgetTester tester) async { + expect(gapiUserToPluginUserData(null), isNull); + }); + + testWidgets('not signed-in user -> null response', + (WidgetTester tester) async { + expect(gapiUserToPluginUserData(fakeUser), isNull); + }); + + testWidgets('signed-in, but null profile user -> null response', + (WidgetTester tester) async { + fakeUser.setIsSignedIn(true); + expect(gapiUserToPluginUserData(fakeUser), isNull); + }); + + testWidgets('signed-in, null userId in profile user -> null response', + (WidgetTester tester) async { + fakeUser.setIsSignedIn(true); + fakeUser.setBasicProfile(FakeBasicProfile()); + expect(gapiUserToPluginUserData(fakeUser), isNull); + }); + }); +} + +class FakeGoogleUser extends Fake implements gapi.GoogleUser { + bool _isSignedIn = false; + gapi.BasicProfile? _basicProfile; + + @override + bool isSignedIn() => _isSignedIn; + @override + gapi.BasicProfile? getBasicProfile() => _basicProfile; + + void setIsSignedIn(bool isSignedIn) { + _isSignedIn = isSignedIn; + } + + void setBasicProfile(gapi.BasicProfile basicProfile) { + _basicProfile = basicProfile; + } +} + +class FakeBasicProfile extends Fake implements gapi.BasicProfile { + String? _id; + + @override + String? getId() => _id; +} diff --git a/packages/google_sign_in/google_sign_in_web/test/test_driver/src/test_utils.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/test_utils.dart similarity index 81% rename from packages/google_sign_in/google_sign_in_web/test/test_driver/src/test_utils.dart rename to packages/google_sign_in/google_sign_in_web/example/integration_test/src/test_utils.dart index 5a6c8906682c..89f9b55f3ddf 100644 --- a/packages/google_sign_in/google_sign_in_web/test/test_driver/src/test_utils.dart +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/test_utils.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/google_sign_in/google_sign_in_web/test/lib/main.dart b/packages/google_sign_in/google_sign_in_web/example/lib/main.dart similarity index 100% rename from packages/google_sign_in/google_sign_in_web/test/lib/main.dart rename to packages/google_sign_in/google_sign_in_web/example/lib/main.dart diff --git a/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml b/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml new file mode 100644 index 000000000000..e370ecc561d2 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/pubspec.yaml @@ -0,0 +1,22 @@ +name: google_sign_in_web_integration_tests +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.2.0" + +dependencies: + flutter: + sdk: flutter + google_sign_in_web: + path: ../ + +dev_dependencies: + http: ^0.13.0 + js: ^0.6.3 + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter diff --git a/packages/google_sign_in/google_sign_in_web/example/run_test.sh b/packages/google_sign_in/google_sign_in_web/example/run_test.sh new file mode 100755 index 000000000000..28877dce8d6e --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/run_test.sh @@ -0,0 +1,23 @@ +#!/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." +fi + + + diff --git a/packages/google_sign_in/google_sign_in_web/example/test_driver/integration_test.dart b/packages/google_sign_in/google_sign_in_web/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_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/google_sign_in/google_sign_in_web/example/web/index.html b/packages/google_sign_in/google_sign_in_web/example/web/index.html new file mode 100644 index 000000000000..9e1284771b82 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/web/index.html @@ -0,0 +1,13 @@ + + + + + Browser Tests + + + + + + diff --git a/packages/google_sign_in/google_sign_in_web/ios/google_sign_in_web.podspec b/packages/google_sign_in/google_sign_in_web/ios/google_sign_in_web.podspec deleted file mode 100644 index 5e192172eb4b..000000000000 --- a/packages/google_sign_in/google_sign_in_web/ios/google_sign_in_web.podspec +++ /dev/null @@ -1,21 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'google_sign_in_web' - s.version = '0.8.1' - s.summary = 'No-op implementation of google_sign_in_web web plugin to avoid build issues on iOS' - s.description = <<-DESC - temp fake google_sign_in_web plugin - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in_web' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.ios.deployment_target = '8.0' - end - \ No newline at end of file diff --git a/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart b/packages/google_sign_in/google_sign_in_web/lib/google_sign_in_web.dart index dd82852fa350..f40b42b1881e 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -6,8 +6,8 @@ import 'dart:async'; import 'dart:html' as html; import 'package:flutter/services.dart'; -import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; import 'package:js/js.dart'; import 'package:meta/meta.dart'; @@ -37,8 +37,8 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { _isGapiInitialized = gapi.inject(gapiUrl).then((_) => gapi.init()); } - Future _isGapiInitialized; - Future _isAuthInitialized; + late Future _isGapiInitialized; + late Future _isAuthInitialized; bool _isInitCalled = false; // This method throws if init hasn't been called at some point in the past. @@ -58,7 +58,7 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { return Future.wait([_isGapiInitialized, _isAuthInitialized]); } - String _autoDetectedClientId; + String? _autoDetectedClientId; /// Factory method that initializes the plugin with [GoogleSignInPlatform]. static void registerWith(Registrar registrar) { @@ -66,12 +66,13 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { } @override - Future init( - {@required String hostedDomain, - List scopes = const [], - SignInOption signInOption = SignInOption.standard, - String clientId}) async { - final String appClientId = clientId ?? _autoDetectedClientId; + Future init({ + List scopes = const [], + SignInOption signInOption = SignInOption.standard, + String? hostedDomain, + String? clientId, + }) async { + final String? appClientId = clientId ?? _autoDetectedClientId; assert( appClientId != null, 'ClientID not set. Either set it on a ' @@ -90,7 +91,7 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { hosted_domain: hostedDomain, // The js lib wants a space-separated list of values scope: scopes.join(' '), - client_id: appClientId, + client_id: appClientId!, )); Completer isAuthInitialized = Completer(); @@ -119,18 +120,18 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { } @override - Future signInSilently() async { + Future signInSilently() async { await initialized; return gapiUserToPluginUserData( - await auth2.getAuthInstance().currentUser.get()); + await auth2.getAuthInstance()?.currentUser?.get()); } @override - Future signIn() async { + Future signIn() async { await initialized; try { - return gapiUserToPluginUserData(await auth2.getAuthInstance().signIn()); + return gapiUserToPluginUserData(await auth2.getAuthInstance()?.signIn()); } on auth2.GoogleAuthSignInError catch (reason) { throw PlatformException( code: reason.error, @@ -143,30 +144,33 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { @override Future getTokens( - {@required String email, bool shouldRecoverAuth}) async { + {required String email, bool? shouldRecoverAuth}) async { await initialized; - final auth2.GoogleUser currentUser = + final auth2.GoogleUser? currentUser = auth2.getAuthInstance()?.currentUser?.get(); - final auth2.AuthResponse response = currentUser.getAuthResponse(); + final auth2.AuthResponse? response = currentUser?.getAuthResponse(); return GoogleSignInTokenData( - idToken: response.id_token, accessToken: response.access_token); + idToken: response?.id_token, accessToken: response?.access_token); } @override Future signOut() async { await initialized; - return auth2.getAuthInstance().signOut(); + return auth2.getAuthInstance()?.signOut(); } @override Future disconnect() async { await initialized; - final auth2.GoogleUser currentUser = + final auth2.GoogleUser? currentUser = auth2.getAuthInstance()?.currentUser?.get(); + + if (currentUser == null) return; + return currentUser.disconnect(); } @@ -174,16 +178,19 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { Future isSignedIn() async { await initialized; - final auth2.GoogleUser currentUser = + final auth2.GoogleUser? currentUser = auth2.getAuthInstance()?.currentUser?.get(); + + if (currentUser == null) return false; + return currentUser.isSignedIn(); } @override - Future clearAuthCache({String token}) async { + Future clearAuthCache({required String token}) async { await initialized; - return auth2.getAuthInstance().disconnect(); + return auth2.getAuthInstance()?.disconnect(); } @override @@ -194,14 +201,15 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { if (currentUser == null) return false; - final grantedScopes = currentUser.getGrantedScopes(); + final grantedScopes = currentUser.getGrantedScopes() ?? ''; final missingScopes = scopes.where((scope) => !grantedScopes.contains(scope)); if (missingScopes.isEmpty) return true; - return currentUser - .grant(auth2.SigninOptions(scope: missingScopes.join(" "))) ?? - false; + final response = await currentUser + .grant(auth2.SigninOptions(scope: missingScopes.join(' '))); + + return response != null; } } diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapi.dart b/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapi.dart index 97ae9b48dc1b..1e2db0fe4609 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 @@ -1,73 +1,21 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@JS() -library gapi; - -import "package:js/js.dart"; -import "package:js/js_util.dart" show promiseToFuture; - /// Type definitions for Google API Client /// Project: https://github.com/google/google-api-javascript-client /// Definitions by: Frank M , grant /// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped /// TypeScript Version: 2.3 -/// The OAuth 2.0 token object represents the OAuth 2.0 token and any associated data. -@anonymous -@JS() -abstract class GoogleApiOAuth2TokenObject { - /// The OAuth 2.0 token. Only present in successful responses - external String get access_token; - external set access_token(String v); - - /// Details about the error. Only present in error responses - external String get error; - external set error(String v); +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/gapi - /// The duration, in seconds, the token is valid for. Only present in successful responses - external String get expires_in; - external set expires_in(String v); - external GoogleApiOAuth2TokenSessionState get session_state; - external set session_state(GoogleApiOAuth2TokenSessionState v); +// ignore_for_file: public_member_api_docs, unused_element - /// The Google API scopes related to this token - external String get state; - external set state(String v); - external factory GoogleApiOAuth2TokenObject( - {String access_token, - String error, - String expires_in, - GoogleApiOAuth2TokenSessionState session_state, - String state}); -} - -@anonymous @JS() -abstract class GoogleApiOAuth2TokenSessionState { - external dynamic /*{ - authuser: string, - }*/ - get extraQueryParams; - external set extraQueryParams( - dynamic - /*{ - authuser: string, - }*/ - v); - external factory GoogleApiOAuth2TokenSessionState( - {dynamic - /*{ - authuser: string, - }*/ - extraQueryParams}); -} +library gapi; -/// Fix for #8215 -/// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/8215 -/// Usage example: -/// https://developers.google.com/identity/sign-in/web/session-state +import 'package:js/js.dart'; // Module gapi typedef void LoadCallback( @@ -82,366 +30,25 @@ typedef void LoadCallback( abstract class LoadConfig { external LoadCallback get callback; external set callback(LoadCallback v); - external Function get onerror; - external set onerror(Function v); - external num get timeout; - external set timeout(num v); - external Function get ontimeout; - external set ontimeout(Function v); + external Function? get onerror; + external set onerror(Function? v); + external num? get timeout; + external set timeout(num? v); + external Function? get ontimeout; + external set ontimeout(Function? v); external factory LoadConfig( {LoadCallback callback, - Function onerror, - num timeout, - Function ontimeout}); + Function? onerror, + num? timeout, + Function? ontimeout}); } /*type CallbackOrConfig = LoadConfig | LoadCallback;*/ /// Pragmatically initialize gapi class member. /// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiloadlibraries-callbackorconfig -@JS("gapi.load") +@JS('gapi.load') external void load( String apiName, dynamic /*LoadConfig|LoadCallback*/ callback); // End module gapi -// Module gapi.auth -/// Initiates the OAuth 2.0 authorization process. The browser displays a popup window prompting the user authenticate and authorize. After the user authorizes, the popup closes and the callback function fires. -@JS("gapi.auth.authorize") -external void authorize( - dynamic - /*{ - /** - * The application's client ID. - */ - client_id?: string; - /** - * If true, then login uses "immediate mode", which means that the token is refreshed behind the scenes, and no UI is shown to the user. - */ - immediate?: boolean; - /** - * The OAuth 2.0 response type property. Default: token - */ - response_type?: string; - /** - * The auth scope or scopes to authorize. Auth scopes for individual APIs can be found in their documentation. - */ - scope?: any; - /** - * The user to sign in as. -1 to toggle a multi-account chooser, 0 to default to the user's current account, and 1 to automatically sign in if the user is signed into Google Plus. - */ - authuser?: number; - }*/ - params, - dynamic callback(GoogleApiOAuth2TokenObject token)); - -/// Initializes the authorization feature. Call this when the client loads to prevent popup blockers from blocking the auth window on gapi.auth.authorize calls. -@JS("gapi.auth.init") -external void init(dynamic callback()); - -/// Retrieves the OAuth 2.0 token for the application. -@JS("gapi.auth.getToken") -external GoogleApiOAuth2TokenObject getToken(); - -/// Sets the OAuth 2.0 token for the application. -@JS("gapi.auth.setToken") -external void setToken(GoogleApiOAuth2TokenObject token); - -/// Initiates the client-side Google+ Sign-In OAuth 2.0 flow. -/// When the method is called, the OAuth 2.0 authorization dialog is displayed to the user and when they accept, the callback function is called. -@JS("gapi.auth.signIn") -external void signIn( - dynamic - /*{ - /** - * Your OAuth 2.0 client ID that you obtained from the Google Developers Console. - */ - clientid?: string; - /** - * Directs the sign-in button to store user and session information in a session cookie and HTML5 session storage on the user's client for the purpose of minimizing HTTP traffic and distinguishing between multiple Google accounts a user might be signed into. - */ - cookiepolicy?: string; - /** - * A function in the global namespace, which is called when the sign-in button is rendered and also called after a sign-in flow completes. - */ - callback?: () => void; - /** - * If true, all previously granted scopes remain granted in each incremental request, for incremental authorization. The default value true is correct for most use cases; use false only if employing delegated auth, where you pass the bearer token to a less-trusted component with lower programmatic authority. - */ - includegrantedscopes?: boolean; - /** - * If your app will write moments, list the full URI of the types of moments that you intend to write. - */ - requestvisibleactions?: any; - /** - * The OAuth 2.0 scopes for the APIs that you would like to use as a space-delimited list. - */ - scope?: any; - /** - * If you have an Android app, you can drive automatic Android downloads from your web sign-in flow. - */ - apppackagename?: string; - }*/ - params); - -/// Signs a user out of your app without logging the user out of Google. This method will only work when the user is signed in with Google+ Sign-In. -@JS("gapi.auth.signOut") -external void signOut(); -// End module gapi.auth - -// Module gapi.client -@anonymous -@JS() -abstract class RequestOptions { - /// The URL to handle the request - external String get path; - external set path(String v); - - /// The HTTP request method to use. Default is GET - external String get method; - external set method(String v); - - /// URL params in key-value pair form - external dynamic get params; - external set params(dynamic v); - - /// Additional HTTP request headers - external dynamic get headers; - external set headers(dynamic v); - - /// The HTTP request body (applies to PUT or POST). - external dynamic get body; - external set body(dynamic v); - - /// If supplied, the request is executed immediately and no gapi.client.HttpRequest object is returned - external dynamic Function() get callback; - external set callback(dynamic Function() v); - external factory RequestOptions( - {String path, - String method, - dynamic params, - dynamic headers, - dynamic body, - dynamic Function() callback}); -} - -@anonymous -@JS() -abstract class _RequestOptions { - @JS("gapi.client.init") - external Promise client_init( - dynamic - /*{ - /** - * The API Key to use. - */ - apiKey?: string; - /** - * An array of discovery doc URLs or discovery doc JSON objects. - */ - discoveryDocs?: string[]; - /** - * The app's client ID, found and created in the Google Developers Console. - */ - clientId?: string; - /** - * The scopes to request, as a space-delimited string. - */ - scope?: string, - - hosted_domain?: string; - }*/ - args); -} - -extension RequestOptionsExtensions on RequestOptions {} - -@anonymous -@JS() -abstract class TokenObject { - /// The access token to use in requests. - external String get access_token; - external set access_token(String v); - external factory TokenObject({String access_token}); -} - -/// Creates a HTTP request for making RESTful requests. -/// An object encapsulating the various arguments for this method. -@JS("gapi.client.request") -external HttpRequest request(RequestOptions args); - -/// Creates an RPC Request directly. The method name and version identify the method to be executed and the RPC params are provided upon RPC creation. -@JS("gapi.client.rpcRequest") -external RpcRequest rpcRequest(String method, - [String version, dynamic rpcParams]); - -/// Sets the API key for the application. -@JS("gapi.client.setApiKey") -external void setApiKey(String apiKey); - -/// Retrieves the OAuth 2.0 token for the application. -@JS("gapi.client.getToken") -external GoogleApiOAuth2TokenObject client_getToken(); - -/// Sets the authentication token to use in requests. -/// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiclientsettokentokenobject -@JS("gapi.client.setToken") -external void client_setToken(TokenObject /*TokenObject|Null*/ token); - -@anonymous -@JS() -abstract class HttpRequestFulfilled { - external T get result; - external set result(T v); - external String get body; - external set body(String v); - external List get headers; - external set headers(List v); - external num get status; - external set status(num v); - external String get statusText; - external set statusText(String v); - external factory HttpRequestFulfilled( - {T result, - String body, - List headers, - num status, - String statusText}); -} - -@anonymous -@JS() -abstract class _HttpRequestFulfilled { - /*external Promise client_load(String name, String version);*/ - /*external void client_load(String name, String version, dynamic callback(), - [String url]); -*/ - @JS("gapi.client.load") - external dynamic /*Promise|void*/ client_load( - String name, String version, - [dynamic callback(), String url]); -} - -extension HttpRequestFulfilledExtensions on HttpRequestFulfilled {} - -@anonymous -@JS() -abstract class HttpRequestRejected { - external dynamic /*dynamic|bool*/ get result; - external set result(dynamic /*dynamic|bool*/ v); - external String get body; - external set body(String v); - external List get headers; - external set headers(List v); - external num get status; - external set status(num v); - external String get statusText; - external set statusText(String v); - external factory HttpRequestRejected( - {dynamic /*dynamic|bool*/ result, - String body, - List headers, - num status, - String statusText}); -} - -/// HttpRequest supports promises. -/// See Google API Client JavaScript Using Promises https://developers.google.com/api-client-library/javascript/features/promises -@JS("gapi.client.HttpRequestPromise") -class HttpRequestPromise {} - -@JS("gapi.client.HttpRequestPromise") -abstract class _HttpRequestPromise { - /// Taken and adapted from https://github.com/Microsoft/TypeScript/blob/v2.3.1/lib/lib.es5.d.ts#L1343 - external Promise then/**/( - [dynamic /*TResult1|PromiseLike Function(HttpRequestFulfilled)|dynamic|Null*/ onfulfilled, - dynamic /*TResult2|PromiseLike Function(HttpRequestRejected)|dynamic|Null*/ onrejected, - dynamic opt_context]); -} - -extension HttpRequestPromiseExtensions on HttpRequestPromise { - Future then( - [dynamic /*TResult1|PromiseLike Function(HttpRequestFulfilled)|dynamic|Null*/ onfulfilled, - dynamic /*TResult2|PromiseLike Function(HttpRequestRejected)|dynamic|Null*/ onrejected, - dynamic opt_context]) { - final Object t = this; - final _HttpRequestPromise tt = t; - return promiseToFuture(tt.then(onfulfilled, onrejected, opt_context)); - } -} - -/// An object encapsulating an HTTP request. This object is not instantiated directly, rather it is returned by gapi.client.request. -@JS("gapi.client.HttpRequest") -class HttpRequest extends HttpRequestPromise { - /// Executes the request and runs the supplied callback on response. - external void execute( - dynamic callback( - - /// contains the response parsed as JSON. If the response is not JSON, this field will be false. - T jsonResp, - - /// is the HTTP response. It is JSON, and can be parsed to an object - dynamic - /*{ - body: string; - headers: any[]; - status: number; - statusText: string; - }*/ - rawResp)); -} - -/// Represents an HTTP Batch operation. Individual HTTP requests are added with the add method and the batch is executed using execute. -@JS("gapi.client.HttpBatch") -class HttpBatch { - /// Adds a gapi.client.HttpRequest to the batch. - external void add(HttpRequest httpRequest, - [dynamic - /*{ - /** - * Identifies the response for this request in the map of batch responses. If one is not provided, the system generates a random ID. - */ - id: string; - callback: ( - /** - * is the response for this request only. Its format is defined by the API method being called. - */ - individualResponse: any, - /** - * is the raw batch ID-response map as a string. It contains all responses to all requests in the batch. - */ - rawBatchResponse: any - ) => any - }*/ - opt_params]); - - /// Executes all requests in the batch. The supplied callback is executed on success or failure. - external void execute( - dynamic callback( - - /// is an ID-response map of each requests response. - dynamic responseMap, - - /// is the same response, but as an unparsed JSON-string. - String rawBatchResponse)); -} - -/// Similar to gapi.client.HttpRequest except this object encapsulates requests generated by registered methods. -@JS("gapi.client.RpcRequest") -class RpcRequest { - /// Executes the request and runs the supplied callback with the response. - external void callback( - void callback( - - /// contains the response parsed as JSON. If the response is not JSON, this field will be false. - dynamic jsonResp, - - /// is the same as jsonResp, except it is a raw string that has not been parsed. It is typically used when the response is not JSON. - String rawResp)); -} - -// End module gapi.client -@JS() -abstract class Promise { - external factory Promise( - void executor(void resolve(T result), Function reject)); - external Promise then(void onFulfilled(T result), [Function onRejected]); -} +// Manually removed gapi.auth and gapi.client, unused by this plugin. diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart b/packages/google_sign_in/google_sign_in_web/lib/src/generated/gapiauth2.dart index ed7a2816d55e..d5efc71d469a 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 @@ -1,13 +1,7 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@JS() -library gapiauth2; - -import "package:js/js.dart"; -import "package:js/js_util.dart" show promiseToFuture; - /// Type definitions for non-npm package Google Sign-In API 0.0 /// Project: https://developers.google.com/identity/sign-in/web/ /// Definitions by: Derek Lawless @@ -16,14 +10,24 @@ import "package:js/js_util.dart" show promiseToFuture; /// +// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/gapi.auth2 + +// ignore_for_file: public_member_api_docs, unused_element + +@JS() +library gapiauth2; + +import 'package:js/js.dart'; +import 'package:js/js_util.dart' show promiseToFuture; + @anonymous @JS() class GoogleAuthInitFailureError { external String get error; - external set error(String value); + external set error(String? value); external String get details; - external set details(String value); + external set details(String? value); } @anonymous @@ -33,16 +37,23 @@ class GoogleAuthSignInError { external set error(String value); } +@anonymous +@JS() +class OfflineAccessResponse { + external String? get code; + external set code(String? value); +} + // Module gapi.auth2 /// GoogleAuth is a singleton class that provides methods to allow the user to sign in with a Google account, /// get the user's current sign-in status, get specific data from the user's Google profile, /// request additional scopes, and sign out from the current account. -@JS("gapi.auth2.GoogleAuth") +@JS('gapi.auth2.GoogleAuth') class GoogleAuth { external IsSignedIn get isSignedIn; external set isSignedIn(IsSignedIn v); - external CurrentUser get currentUser; - external set currentUser(CurrentUser v); + external CurrentUser? get currentUser; + external set currentUser(CurrentUser? v); /// Calls the onInit function when the GoogleAuth object is fully initialized, or calls the onFailure function if /// initialization fails. @@ -68,22 +79,20 @@ class GoogleAuth { abstract class _GoogleAuth { external Promise signIn( [dynamic /*SigninOptions|SigninOptionsBuilder*/ options]); - external Promise grantOfflineAccess( - [OfflineAccessOptions options]); + external Promise grantOfflineAccess( + [OfflineAccessOptions? options]); } extension GoogleAuthExtensions on GoogleAuth { Future signIn( [dynamic /*SigninOptions|SigninOptionsBuilder*/ options]) { - final Object t = this; - final _GoogleAuth tt = t; + final _GoogleAuth tt = this as _GoogleAuth; return promiseToFuture(tt.signIn(options)); } - Future grantOfflineAccess( - [OfflineAccessOptions options]) { - final Object t = this; - final _GoogleAuth tt = t; + Future grantOfflineAccess( + [OfflineAccessOptions? options]) { + final _GoogleAuth tt = this as _GoogleAuth; return promiseToFuture(tt.grantOfflineAccess(options)); } } @@ -116,42 +125,52 @@ abstract class SigninOptions { /// The package name of the Android app to install over the air. /// See Android app installs from your web site: /// https://developers.google.com/identity/sign-in/web/android-app-installs - external String get app_package_name; - external set app_package_name(String v); + external String? get app_package_name; + external set app_package_name(String? v); /// Fetch users' basic profile information when they sign in. /// Adds 'profile', 'email' and 'openid' to the requested scopes. /// True if unspecified. - external bool get fetch_basic_profile; - external set fetch_basic_profile(bool v); + external bool? get fetch_basic_profile; + external set fetch_basic_profile(bool? v); /// Specifies whether to prompt the user for re-authentication. /// See OpenID Connect Request Parameters: /// https://openid.net/specs/openid-connect-basic-1_0.html#RequestParameters - external String get prompt; - external set prompt(String v); + external String? get prompt; + external set prompt(String? v); /// The scopes to request, as a space-delimited string. /// Optional if fetch_basic_profile is not set to false. - external String get scope; - external set scope(String v); + external String? get scope; + external set scope(String? v); /// The UX mode to use for the sign-in flow. /// By default, it will open the consent flow in a popup. - external String /*'popup'|'redirect'*/ get ux_mode; - external set ux_mode(String /*'popup'|'redirect'*/ v); + external String? /*'popup'|'redirect'*/ get ux_mode; + external set ux_mode(String? /*'popup'|'redirect'*/ v); /// If using ux_mode='redirect', this parameter allows you to override the default redirect_uri that will be used at the end of the consent flow. /// The default redirect_uri is the current URL stripped of query parameters and hash fragment. - external String get redirect_uri; - external set redirect_uri(String v); + external String? get redirect_uri; + external set redirect_uri(String? v); + + // When your app knows which user it is trying to authenticate, it can provide this parameter as a hint to the authentication server. + // Passing this hint suppresses the account chooser and either pre-fill the email box on the sign-in form, or select the proper session (if the user is using multiple sign-in), + // which can help you avoid problems that occur if your app logs in the wrong user account. The value can be either an email address or the sub string, + // which is equivalent to the user's Google ID. + // https://developers.google.com/identity/protocols/OpenIDConnect?hl=en#authenticationuriparameters + external String? get login_hint; + external set login_hint(String? v); + external factory SigninOptions( {String app_package_name, bool fetch_basic_profile, String prompt, String scope, String /*'popup'|'redirect'*/ ux_mode, - String redirect_uri}); + String redirect_uri, + String login_hint}); } /// Definitions by: John @@ -160,12 +179,12 @@ abstract class SigninOptions { @anonymous @JS() abstract class OfflineAccessOptions { - external String get scope; - external set scope(String v); - external String /*'select_account'|'consent'*/ get prompt; - external set prompt(String /*'select_account'|'consent'*/ v); - external String get app_package_name; - external set app_package_name(String v); + external String? get scope; + external set scope(String? v); + external String? /*'select_account'|'consent'*/ get prompt; + external set prompt(String? /*'select_account'|'consent'*/ v); + external String? get app_package_name; + external set app_package_name(String? v); external factory OfflineAccessOptions( {String scope, String /*'select_account'|'consent'*/ prompt, @@ -178,98 +197,99 @@ abstract class OfflineAccessOptions { @JS() abstract class ClientConfig { /// The app's client ID, found and created in the Google Developers Console. - external String get client_id; - external set client_id(String v); + external String? get client_id; + external set client_id(String? v); /// The domains for which to create sign-in cookies. Either a URI, single_host_origin, or none. /// Defaults to single_host_origin if unspecified. - external String get cookie_policy; - external set cookie_policy(String v); + external String? get cookie_policy; + external set cookie_policy(String? v); /// The scopes to request, as a space-delimited string. Optional if fetch_basic_profile is not set to false. - external String get scope; - external set scope(String v); + external String? get scope; + external set scope(String? v); /// Fetch users' basic profile information when they sign in. Adds 'profile' and 'email' to the requested scopes. True if unspecified. - external bool get fetch_basic_profile; - external set fetch_basic_profile(bool v); + external bool? get fetch_basic_profile; + external set fetch_basic_profile(bool? v); /// The Google Apps domain to which users must belong to sign in. This is susceptible to modification by clients, /// so be sure to verify the hosted domain property of the returned user. Use GoogleUser.getHostedDomain() on the client, /// and the hd claim in the ID Token on the server to verify the domain is what you expected. - external String get hosted_domain; - external set hosted_domain(String v); + external String? get hosted_domain; + external set hosted_domain(String? v); /// Used only for OpenID 2.0 client migration. Set to the value of the realm that you are currently using for OpenID 2.0, /// as described in OpenID 2.0 (Migration). - external String get openid_realm; - external set openid_realm(String v); + external String? get openid_realm; + external set openid_realm(String? v); /// The UX mode to use for the sign-in flow. /// By default, it will open the consent flow in a popup. - external String /*'popup'|'redirect'*/ get ux_mode; - external set ux_mode(String /*'popup'|'redirect'*/ v); + external String? /*'popup'|'redirect'*/ get ux_mode; + external set ux_mode(String? /*'popup'|'redirect'*/ v); /// If using ux_mode='redirect', this parameter allows you to override the default redirect_uri that will be used at the end of the consent flow. /// The default redirect_uri is the current URL stripped of query parameters and hash fragment. - external String get redirect_uri; - external set redirect_uri(String v); + external String? get redirect_uri; + external set redirect_uri(String? v); external factory ClientConfig( {String client_id, String cookie_policy, String scope, bool fetch_basic_profile, - String hosted_domain, + String? hosted_domain, String openid_realm, String /*'popup'|'redirect'*/ ux_mode, String redirect_uri}); } -@JS("gapi.auth2.SigninOptionsBuilder") +@JS('gapi.auth2.SigninOptionsBuilder') class SigninOptionsBuilder { external dynamic setAppPackageName(String name); external dynamic setFetchBasicProfile(bool fetch); external dynamic setPrompt(String prompt); external dynamic setScope(String scope); + external dynamic setLoginHint(String hint); } @anonymous @JS() abstract class BasicProfile { - external String getId(); - external String getName(); - external String getGivenName(); - external String getFamilyName(); - external String getImageUrl(); - external String getEmail(); + external String? getId(); + external String? getName(); + external String? getGivenName(); + external String? getFamilyName(); + external String? getImageUrl(); + external String? getEmail(); } /// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authresponse @anonymous @JS() abstract class AuthResponse { - external String get access_token; - external set access_token(String v); - external String get id_token; - external set id_token(String v); - external String get login_hint; - external set login_hint(String v); - external String get scope; - external set scope(String v); - external num get expires_in; - external set expires_in(num v); - external num get first_issued_at; - external set first_issued_at(num v); - external num get expires_at; - external set expires_at(num v); + external String? get access_token; + external set access_token(String? v); + external String? get id_token; + external set id_token(String? v); + external String? get login_hint; + external set login_hint(String? v); + external String? get scope; + external set scope(String? v); + external num? get expires_in; + external set expires_in(num? v); + external num? get first_issued_at; + external set first_issued_at(num? v); + external num? get expires_at; + external set expires_at(num? v); external factory AuthResponse( - {String access_token, - String id_token, - String login_hint, - String scope, - num expires_in, - num first_issued_at, - num expires_at}); + {String? access_token, + String? id_token, + String? login_hint, + String? scope, + num? expires_in, + num? first_issued_at, + num? expires_at}); } /// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authorizeconfig @@ -280,22 +300,22 @@ abstract class AuthorizeConfig { external set client_id(String v); external String get scope; external set scope(String v); - external String get response_type; - external set response_type(String v); - external String get prompt; - external set prompt(String v); - external String get cookie_policy; - external set cookie_policy(String v); - external String get hosted_domain; - external set hosted_domain(String v); - external String get login_hint; - external set login_hint(String v); - external String get app_package_name; - external set app_package_name(String v); - external String get openid_realm; - external set openid_realm(String v); - external bool get include_granted_scopes; - external set include_granted_scopes(bool v); + external String? get response_type; + external set response_type(String? v); + external String? get prompt; + external set prompt(String? v); + external String? get cookie_policy; + external set cookie_policy(String? v); + external String? get hosted_domain; + external set hosted_domain(String? v); + external String? get login_hint; + external set login_hint(String? v); + external String? get app_package_name; + external set app_package_name(String? v); + external String? get openid_realm; + external set openid_realm(String? v); + external bool? get include_granted_scopes; + external set include_granted_scopes(bool? v); external factory AuthorizeConfig( {String client_id, String scope, @@ -348,34 +368,31 @@ abstract class AuthorizeResponse { @JS() abstract class GoogleUser { /// Get the user's unique ID string. - external String getId(); + external String? getId(); /// Returns true if the user is signed in. external bool isSignedIn(); /// Get the user's Google Apps domain if the user signed in with a Google Apps account. - external String getHostedDomain(); + external String? getHostedDomain(); /// Get the scopes that the user granted as a space-delimited string. - external String getGrantedScopes(); + external String? getGrantedScopes(); /// Get the user's basic profile information. - external BasicProfile getBasicProfile(); + external BasicProfile? getBasicProfile(); /// Get the response object from the user's auth session. + // This returns an empty JS object when the user hasn't attempted to sign in. external AuthResponse getAuthResponse([bool includeAuthorizationData]); /// Returns true if the user granted the specified scopes. external bool hasGrantedScopes(String scopes); - /// Signs in the user. Use this method to request additional scopes for incremental - /// authorization or to sign in a user after the user has signed out. - /// When you use GoogleUser.signIn(), the sign-in flow skips the account chooser step. - /// See GoogleAuth.signIn(). - external dynamic signIn( - [dynamic /*SigninOptions|SigninOptionsBuilder*/ options]); - - /// See GoogleUser.signIn() + // Has the API for grant and grantOfflineAccess changed? + /// Request additional scopes to the user. + /// + /// See GoogleAuth.signIn() for the list of parameters and the error code. external dynamic grant( [dynamic /*SigninOptions|SigninOptionsBuilder*/ options]); @@ -391,35 +408,35 @@ abstract class GoogleUser { @anonymous @JS() abstract class _GoogleUser { + /// Forces a refresh of the access token, and then returns a Promise for the new AuthResponse. external Promise reloadAuthResponse(); } extension GoogleUserExtensions on GoogleUser { Future reloadAuthResponse() { - final Object t = this; - final _GoogleUser tt = t; + final _GoogleUser tt = this as _GoogleUser; return promiseToFuture(tt.reloadAuthResponse()); } } /// Initializes the GoogleAuth object. /// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2initparams -@JS("gapi.auth2.init") +@JS('gapi.auth2.init') external GoogleAuth init(ClientConfig params); /// Returns the GoogleAuth object. You must initialize the GoogleAuth object with gapi.auth2.init() before calling this method. -@JS("gapi.auth2.getAuthInstance") -external GoogleAuth getAuthInstance(); +@JS('gapi.auth2.getAuthInstance') +external GoogleAuth? getAuthInstance(); /// Performs a one time OAuth 2.0 authorization. /// Reference: https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiauth2authorizeparams-callback -@JS("gapi.auth2.authorize") +@JS('gapi.auth2.authorize') external void authorize( AuthorizeConfig params, void callback(AuthorizeResponse response)); // End module gapi.auth2 // Module gapi.signin2 -@JS("gapi.signin2.render") +@JS('gapi.signin2.render') external void render( dynamic id, dynamic diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/load_gapi.dart b/packages/google_sign_in/google_sign_in_web/lib/src/load_gapi.dart index f954ff1dce6b..6d8c566f0412 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -20,7 +20,7 @@ external set gapiOnloadCallback(Function callback); /// This is only exposed for testing. It shouldn't be accessed by users of the /// plugin as it could break at any point. @visibleForTesting -const String kGapiOnloadCallbackFunctionName = "gapiOnloadCallback"; +const String kGapiOnloadCallbackFunctionName = 'gapiOnloadCallback'; String _addOnloadToScript(String url) => url.startsWith('data:') ? url : '$url?onload=$kGapiOnloadCallbackFunctionName'; diff --git a/packages/google_sign_in/google_sign_in_web/lib/src/utils.dart b/packages/google_sign_in/google_sign_in_web/lib/src/utils.dart index 36bb52dce0f3..bcfefc2054b4 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -9,13 +9,22 @@ import 'package:google_sign_in_platform_interface/google_sign_in_platform_interf import 'generated/gapiauth2.dart' as auth2; -/// Injects a bunch of libraries in the and returns a -/// Future that resolves when all load. -Future injectJSLibraries(List libraries, - {html.HtmlElement target /*, Duration timeout */}) { +/// Injects a list of JS [libraries] as `script` tags into a [target] [html.HtmlElement]. +/// +/// If [target] is not provided, it defaults to the web app's `head` tag (see `web/index.html`). +/// [libraries] is a list of URLs that are used as the `src` attribute of `script` tags +/// to which an `onLoad` listener is attached (one per URL). +/// +/// Returns a [Future] that resolves when all of the `script` tags `onLoad` events trigger. +Future injectJSLibraries( + List libraries, { + html.HtmlElement? target, +}) { final List> loading = >[]; final List tags = []; + final html.Element targetElement = target ?? html.querySelector('head')!; + libraries.forEach((String library) { final html.ScriptElement script = html.ScriptElement() ..async = true @@ -25,24 +34,26 @@ Future injectJSLibraries(List libraries, loading.add(script.onLoad.first); tags.add(script); }); - (target ?? html.querySelector('head')).children.addAll(tags); + + targetElement.children.addAll(tags); return Future.wait(loading); } -/// Utility method that converts `currentUser` to the equivalent -/// [GoogleSignInUserData]. +/// Utility method that converts `currentUser` to the equivalent [GoogleSignInUserData]. +/// /// This method returns `null` when the [currentUser] is not signed in. -GoogleSignInUserData gapiUserToPluginUserData(auth2.GoogleUser currentUser) { +GoogleSignInUserData? gapiUserToPluginUserData(auth2.GoogleUser? currentUser) { final bool isSignedIn = currentUser?.isSignedIn() ?? false; - final auth2.BasicProfile profile = currentUser?.getBasicProfile(); + final auth2.BasicProfile? profile = currentUser?.getBasicProfile(); if (!isSignedIn || profile?.getId() == null) { return null; } + return GoogleSignInUserData( displayName: profile?.getName(), - email: profile?.getEmail(), - id: profile?.getId(), + email: profile?.getEmail() ?? '', + id: profile?.getId() ?? '', photoUrl: profile?.getImageUrl(), - idToken: currentUser.getAuthResponse()?.id_token, + idToken: currentUser?.getAuthResponse().id_token, ); } diff --git a/packages/google_sign_in/google_sign_in_web/pubspec.yaml b/packages/google_sign_in/google_sign_in_web/pubspec.yaml index 70758ac6830d..44020fe598c3 100644 --- a/packages/google_sign_in/google_sign_in_web/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_web/pubspec.yaml @@ -1,8 +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. -homepage: https://github.com/flutter/plugins/tree/master/packages/google_sign_in/google_sign_in_web -version: 0.9.2 +repository: https://github.com/flutter/plugins/tree/master/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 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: @@ -12,23 +17,15 @@ flutter: fileName: google_sign_in_web.dart dependencies: - google_sign_in_platform_interface: ^1.1.0 flutter: sdk: flutter flutter_web_plugins: sdk: flutter - meta: ^1.1.7 - js: ^0.6.1 + google_sign_in_platform_interface: ^2.0.0 + js: ^0.6.3 + meta: ^1.3.0 dev_dependencies: flutter_test: sdk: flutter - google_sign_in: ^4.0.14 - pedantic: ^1.8.0 - mockito: ^4.1.1 - integration_test: - path: ../../integration_test - -environment: - sdk: ">=2.6.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4 <2.0.0" + pedantic: ^1.10.0 diff --git a/packages/google_sign_in/google_sign_in_web/test/README.md b/packages/google_sign_in/google_sign_in_web/test/README.md index 7c48d024ba57..7c5b4ad682ba 100644 --- a/packages/google_sign_in/google_sign_in_web/test/README.md +++ b/packages/google_sign_in/google_sign_in_web/test/README.md @@ -1,17 +1,5 @@ -# Running browser_tests +## test -Make sure you have updated to the latest Flutter master. +This package uses integration tests for testing. -1. Check what version of Chrome is running on the machine you're running tests on. - -2. Download and install driver for that version from here: - * - -3. Start the driver using `chromedriver --port=4444` - -4. Change into the `test` directory of your clone. - -5. Run tests: `flutter drive -d web-server --browser-name=chrome --target=test_driver/TEST_NAME_integration.dart`, or (in Linux): - - * Single: `./run_test test_driver/TEST_NAME_integration.dart` - * All: `./run_test` +See `example/README.md` for more info. diff --git a/packages/google_sign_in/google_sign_in_web/test/pubspec.yaml b/packages/google_sign_in/google_sign_in_web/test/pubspec.yaml deleted file mode 100644 index dd0354e81498..000000000000 --- a/packages/google_sign_in/google_sign_in_web/test/pubspec.yaml +++ /dev/null @@ -1,24 +0,0 @@ -name: regular_integration_tests -publish_to: none - -environment: - sdk: ">=2.2.2 <3.0.0" - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - google_sign_in: ^4.5.3 - flutter_driver: - sdk: flutter - flutter_test: - sdk: flutter - http: ^0.12.2 - mockito: ^4.1.1 - integration_test: - path: ../../../integration_test - -dependency_overrides: - google_sign_in_web: - path: ../ diff --git a/packages/google_sign_in/google_sign_in_web/test/run_test b/packages/google_sign_in/google_sign_in_web/test/run_test deleted file mode 100755 index 74a8526a0fa3..000000000000 --- a/packages/google_sign_in/google_sign_in_web/test/run_test +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/bash -if pgrep -lf chromedriver > /dev/null; then - echo "chromedriver is running." - - if [ $# -eq 0 ]; then - echo "No target specified, running all tests..." - find test_driver/ -iname *_integration.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --target='{}' - else - echo "Running test target: $1..." - set -x - flutter drive -d web-server --web-port=7357 --browser-name=chrome --target=$1 - fi - - else - echo "chromedriver is not running." -fi - diff --git a/packages/google_sign_in/google_sign_in_web/test/test_driver/auth2_integration.dart b/packages/google_sign_in/google_sign_in_web/test/test_driver/auth2_integration.dart deleted file mode 100644 index e2f16f2aee43..000000000000 --- a/packages/google_sign_in/google_sign_in_web/test/test_driver/auth2_integration.dart +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/services.dart'; -import 'package:integration_test/integration_test.dart'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; -import 'package:google_sign_in_web/google_sign_in_web.dart'; -import 'gapi_mocks/gapi_mocks.dart' as gapi_mocks; -import 'src/test_utils.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - GoogleSignInTokenData expectedTokenData = - GoogleSignInTokenData(idToken: '70k3n', accessToken: 'access_70k3n'); - - GoogleSignInUserData expectedUserData = GoogleSignInUserData( - displayName: 'Foo Bar', - email: 'foo@example.com', - id: '123', - photoUrl: 'http://example.com/img.jpg', - idToken: expectedTokenData.idToken, - ); - - GoogleSignInPlugin plugin; - - group('plugin.init() 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 { - await expectLater( - plugin.init( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - ), - throwsA(isA())); - }); - - testWidgets('init forwards error code from JS', - (WidgetTester tester) async { - try { - await plugin.init( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - ); - fail('plugin.init should have thrown an exception!'); - } catch (e) { - expect(e.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... - void _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); - }); - - group('Successful .init, then', () { - setUp(() async { - await plugin.init( - hostedDomain: 'foo', - scopes: ['some', 'scope'], - clientId: '1234', - ); - await plugin.initialized; - }); - - testWidgets('signInSilently', (WidgetTester tester) async { - GoogleSignInUserData actualUser = await plugin.signInSilently(); - - expect(actualUser, expectedUserData); - }); - - testWidgets('signIn', (WidgetTester tester) async { - GoogleSignInUserData actualUser = await plugin.signIn(); - - expect(actualUser, expectedUserData); - }); - - testWidgets('getTokens', (WidgetTester tester) async { - GoogleSignInTokenData actualToken = - await plugin.getTokens(email: expectedUserData.email); - - expect(actualToken, expectedTokenData); - }); - - testWidgets('requestScopes', (WidgetTester tester) async { - 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) { - expect(e.code, 'popup_closed_by_user'); - } - }); - }); -} diff --git a/packages/google_sign_in/google_sign_in_web/test/test_driver/auth2_integration_test.dart b/packages/google_sign_in/google_sign_in_web/test/test_driver/auth2_integration_test.dart deleted file mode 100644 index 39444c0daa24..000000000000 --- a/packages/google_sign_in/google_sign_in_web/test/test_driver/auth2_integration_test.dart +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:integration_test/integration_test_driver.dart'; - -Future main() async => integrationDriver(); diff --git a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_load_integration.dart b/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_load_integration.dart deleted file mode 100644 index 540369cae370..000000000000 --- a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_load_integration.dart +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:html' as html; - -import 'package:integration_test/integration_test.dart'; - -import 'package:flutter_test/flutter_test.dart'; -import 'package:google_sign_in_platform_interface/google_sign_in_platform_interface.dart'; -import 'package:google_sign_in_web/google_sign_in_web.dart'; -import 'gapi_mocks/gapi_mocks.dart' as gapi_mocks; -import 'src/test_utils.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - gapiUrl = toBase64Url(gapi_mocks.auth2InitSuccess(GoogleSignInUserData())); - - 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/test/test_driver/gapi_load_integration_test.dart b/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_load_integration_test.dart deleted file mode 100644 index 39444c0daa24..000000000000 --- a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_load_integration_test.dart +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:integration_test/integration_test_driver.dart'; - -Future main() async => integrationDriver(); diff --git a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_utils_integration.dart b/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_utils_integration.dart deleted file mode 100644 index 55b942842b33..000000000000 --- a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_utils_integration.dart +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. -import 'package:flutter_test/flutter_test.dart'; - -import 'package:integration_test/integration_test.dart'; - -import 'package:google_sign_in_web/src/generated/gapiauth2.dart' as gapi; -import 'package:google_sign_in_web/src/utils.dart'; -import 'package:mockito/mockito.dart'; - -class MockGoogleUser extends Mock implements gapi.GoogleUser {} - -class MockBasicProfile extends Mock implements gapi.BasicProfile {} - -void main() { - // The non-null use cases are covered by the auth2_test.dart file. - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('gapiUserToPluginUserData', () { - var mockUser; - - setUp(() { - mockUser = MockGoogleUser(); - }); - - testWidgets('null user -> null response', (WidgetTester tester) async { - expect(gapiUserToPluginUserData(null), isNull); - }); - - testWidgets('not signed-in user -> null response', - (WidgetTester tester) async { - when(mockUser.isSignedIn()).thenReturn(false); - expect(gapiUserToPluginUserData(mockUser), isNull); - }); - - testWidgets('signed-in, but null profile user -> null response', - (WidgetTester tester) async { - when(mockUser.isSignedIn()).thenReturn(true); - expect(gapiUserToPluginUserData(mockUser), isNull); - }); - - testWidgets('signed-in, null userId in profile user -> null response', - (WidgetTester tester) async { - when(mockUser.isSignedIn()).thenReturn(true); - when(mockUser.getBasicProfile()).thenReturn(MockBasicProfile()); - expect(gapiUserToPluginUserData(mockUser), isNull); - }); - }); -} diff --git a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_utils_integration_test.dart b/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_utils_integration_test.dart deleted file mode 100644 index 39444c0daa24..000000000000 --- a/packages/google_sign_in/google_sign_in_web/test/test_driver/gapi_utils_integration_test.dart +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:integration_test/integration_test_driver.dart'; - -Future main() async => integrationDriver(); diff --git a/packages/google_sign_in/google_sign_in_web/test/tests_exist_elsewhere_test.dart b/packages/google_sign_in/google_sign_in_web/test/tests_exist_elsewhere_test.dart new file mode 100644 index 000000000000..442c50144727 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/test/tests_exist_elsewhere_test.dart @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '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/google_sign_in/google_sign_in_web/test/web/index.html b/packages/google_sign_in/google_sign_in_web/test/web/index.html deleted file mode 100644 index 59a832b5de4c..000000000000 --- a/packages/google_sign_in/google_sign_in_web/test/web/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - Browser Tests - - - - - - diff --git a/packages/image_picker/analysis_options.yaml b/packages/image_picker/analysis_options.yaml new file mode 100644 index 000000000000..cda4f6e153e6 --- /dev/null +++ b/packages/image_picker/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options_legacy.yaml diff --git a/packages/image_picker/image_picker/AUTHORS b/packages/image_picker/image_picker/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/image_picker/image_picker/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index f8dbee9417cf..b913bbf29758 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,117 @@ +## 0.8.0+3 + +* Readded request for camera permissions. + +## 0.8.0+2 + +* Fix a rotation problem where when camera is chosen as a source and additional parameters are added. + +## 0.8.0+1 + +* Removed redundant request for camera permissions. + +## 0.8.0 + +* BREAKING CHANGE: Changed storage location for captured images and videos to internal cache on Android, +to comply with new Google Play storage requirements. This means developers are responsible for moving +the image or video to a different location in case more permanent storage is required. Other applications +will no longer be able to access images or videos captured unless they are moved to a publicly accessible location. +* Updated Mockito to fix Android tests. + +## 0.7.5+4 +* Migrate maven repo from jcenter to mavenCentral. + +## 0.7.5+3 +* Localize `UIAlertController` strings. + +## 0.7.5+2 +* Implement `UIAlertController` with a preferredStyle of `UIAlertControllerStyleAlert` since `UIAlertView` is deprecated. + +## 0.7.5+1 + +* Fixes a rotation problem where Select Photos limited access is chosen but the image that is picked +is not included selected photos and image is scaled. + +## 0.7.5 + +* Fixes an issue where image rotation is wrong when Select Photos chose and image is scaled. +* Migrate to PHPicker for iOS 14 and higher versions to pick image from the photo library. +* Implement the limited permission to pick photo from the photo library when Select Photo is chosen. + +## 0.7.4 + +* Update flutter_plugin_android_lifecycle dependency to 2.0.1 to fix an R8 issue + on some versions. + +## 0.7.3 + +* Endorse image_picker_for_web + +## 0.7.2+1 + +* Android: fixes an issue where videos could be wrongly picked with `.jpg` extension. + +## 0.7.2 + +* Run CocoaPods iOS tests in RunnerUITests target + +## 0.7.1 + +* Update platform_plugin_interface version requirement. + +## 0.7.0 + +* Migrate to nullsafety +* Breaking Changes: + * Removed the deprecated methods: `ImagePicker.pickImage`, `ImagePicker.pickVideo`, +`ImagePicker.retrieveLostData` + +## 0.6.7+22 + +* iOS: update XCUITests to separate each test session. + +## 0.6.7+21 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 0.6.7+20 + +* Updated README.md to show the new Android API requirements. + +## 0.6.7+19 + +* Do not copy static field to another static field. + +## 0.6.7+18 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 0.6.7+17 + +* iOS: fix `User-facing text should use localized string macro` warning. + +## 0.6.7+16 + +* Update Flutter SDK constraint. + +## 0.6.7+15 + +* Fix element type in XCUITests to look for staticText type when searching for texts. + * See https://github.com/flutter/flutter/issues/71927 +* Minor update in XCUITests to search for different elements on iOS 14 and above. + +## 0.6.7+14 + +* Set up XCUITests. + +## 0.6.7+13 + +* Update documentation of `getImage()` about HEIC images. + +## 0.6.7+12 + +* Update android compileSdkVersion to 29. + ## 0.6.7+11 * Keep handling deprecated Android v1 classes for backward compatibility. diff --git a/packages/image_picker/image_picker/LICENSE b/packages/image_picker/image_picker/LICENSE index c4e4de2acfcd..0be8bbc3e68d 100644 --- a/packages/image_picker/image_picker/LICENSE +++ b/packages/image_picker/image_picker/LICENSE @@ -1,6 +1,6 @@ image_picker -Copyright 2017, the Flutter project authors. All rights reserved. +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: diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md index 2e062a024597..1de12bc556d9 100755 --- a/packages/image_picker/image_picker/README.md +++ b/packages/image_picker/image_picker/README.md @@ -1,13 +1,13 @@ # Image Picker plugin for Flutter -[![pub package](https://img.shields.io/pub/v/image_picker.svg)](https://pub.dartlang.org/packages/image_picker) +[![pub package](https://img.shields.io/pub/v/image_picker.svg)](https://pub.dev/packages/image_picker) A Flutter plugin for iOS and Android for picking images from the image library, and taking new pictures with the camera. ## Installation -First, add `image_picker` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). +First, add `image_picker` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). ### iOS @@ -19,12 +19,12 @@ Add the following keys to your _Info.plist_ file, located in `/ios ### Android -#### API 29+ No configuration required - the plugin should work out of the box. -#### API < 29 +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. -Add `android:requestLegacyExternalStorage="true"` as an attribute to the `` tag in AndroidManifest.xml. The [attribute](https://developer.android.com/training/data-storage/compatibility) is `false` by default on apps targeting Android Q. +**Note:** Images and videos picked using the camera are saved to your application's local cache, and should therefore be expected to only be around temporarily. +If you require your picked image to be stored permanently, it is your responsibility to move it to a more permanent location. ### Example diff --git a/packages/image_picker/image_picker/android/build.gradle b/packages/image_picker/image_picker/android/build.gradle index 493d72caf1a8..e21b7f1738b4 100755 --- a/packages/image_picker/image_picker/android/build.gradle +++ b/packages/image_picker/image_picker/android/build.gradle @@ -4,7 +4,7 @@ version '1.0-SNAPSHOT' buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -15,17 +15,14 @@ buildscript { rootProject.allprojects { repositories { google() - jcenter() - maven { - url 'https://google.bintray.com/exoplayer/' - } + mavenCentral() } } apply plugin: 'com.android.library' android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { minSdkVersion 16 diff --git a/packages/image_picker/image_picker/android/src/main/AndroidManifest.xml b/packages/image_picker/image_picker/android/src/main/AndroidManifest.xml index f0bc86fbf0ac..5d1773ee03a4 100755 --- a/packages/image_picker/image_picker/android/src/main/AndroidManifest.xml +++ b/packages/image_picker/image_picker/android/src/main/AndroidManifest.xml @@ -1,7 +1,5 @@ - - + package="io.flutter.plugins.imagepicker"> + android:resource="@xml/flutter_image_picker_file_paths" /> - \ No newline at end of file + 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/src/main/java/io/flutter/plugins/imagepicker/ExifDataCopier.java index 08b010072585..eada546f029a 100644 --- a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ExifDataCopier.java +++ b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ExifDataCopier.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter 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/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java index 9ebf1fad826b..1f51a226c7e2 100644 --- a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java +++ b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/FileUtils.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -23,8 +23,10 @@ package io.flutter.plugins.imagepicker; +import android.content.ContentResolver; import android.content.Context; import android.net.Uri; +import android.webkit.MimeTypeMap; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -39,7 +41,7 @@ String getPathFromUri(final Context context, final Uri uri) { OutputStream outputStream = null; boolean success = false; try { - String extension = getImageExtension(uri); + String extension = getImageExtension(context, uri); inputStream = context.getContentResolver().openInputStream(uri); file = File.createTempFile("image_picker", extension, context.getCacheDir()); file.deleteOnExit(); @@ -67,13 +69,18 @@ String getPathFromUri(final Context context, final Uri uri) { } /** @return extension of image with dot, or default .jpg if it none. */ - private static String getImageExtension(Uri uriImage) { + private static String getImageExtension(Context context, Uri uriImage) { String extension = null; try { String imagePath = uriImage.getPath(); - if (imagePath != null && imagePath.lastIndexOf(".") != -1) { - extension = imagePath.substring(imagePath.lastIndexOf(".") + 1); + if (uriImage.getScheme().equals(ContentResolver.SCHEME_CONTENT)) { + final MimeTypeMap mime = MimeTypeMap.getSingleton(); + extension = mime.getExtensionFromMimeType(context.getContentResolver().getType(uriImage)); + } else { + extension = + MimeTypeMap.getFileExtensionFromUrl( + Uri.fromFile(new File(uriImage.getPath())).toString()); } } catch (Exception e) { extension = null; 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/src/main/java/io/flutter/plugins/imagepicker/ImagePickerCache.java index 45ba6de0ee6b..3df0a4108b5c 100644 --- a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerCache.java +++ b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerCache.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter 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/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java index ff7f1534a586..c934b54a1f8e 100644 --- a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java +++ b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -42,10 +42,8 @@ enum CameraDevice { * means that the chooseImageFromGallery() or takeImageWithCamera() method was called at least * twice. In this case, stop executing and finish with an error. * - *

2. Check that a required runtime permission has been granted. The chooseImageFromGallery() - * method checks if the {@link Manifest.permission#READ_EXTERNAL_STORAGE} permission has been - * granted. Similarly, the takeImageWithCamera() method checks that {@link - * Manifest.permission#CAMERA} has been granted. + *

2. Check that a required runtime permission has been granted. The takeImageWithCamera() method + * checks that {@link Manifest.permission#CAMERA} has been granted. * *

The permission check can end up in two different outcomes: * @@ -76,17 +74,15 @@ public class ImagePickerDelegate PluginRegistry.RequestPermissionsResultListener { @VisibleForTesting static final int REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY = 2342; @VisibleForTesting static final int REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA = 2343; - @VisibleForTesting static final int REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION = 2344; @VisibleForTesting static final int REQUEST_CAMERA_IMAGE_PERMISSION = 2345; @VisibleForTesting static final int REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY = 2352; @VisibleForTesting static final int REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA = 2353; - @VisibleForTesting static final int REQUEST_EXTERNAL_VIDEO_STORAGE_PERMISSION = 2354; @VisibleForTesting static final int REQUEST_CAMERA_VIDEO_PERMISSION = 2355; @VisibleForTesting final String fileProviderName; private final Activity activity; - private final File externalFilesDirectory; + @VisibleForTesting final File externalFilesDirectory; private final ImageResizer imageResizer; private final ImagePickerCache cache; private final PermissionManager permissionManager; @@ -257,12 +253,6 @@ public void chooseVideoFromGallery(MethodCall methodCall, MethodChannel.Result r return; } - if (!permissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE)) { - permissionManager.askForPermission( - Manifest.permission.READ_EXTERNAL_STORAGE, REQUEST_EXTERNAL_VIDEO_STORAGE_PERMISSION); - return; - } - launchPickVideoFromGalleryIntent(); } @@ -322,12 +312,6 @@ public void chooseImageFromGallery(MethodCall methodCall, MethodChannel.Result r return; } - if (!permissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE)) { - permissionManager.askForPermission( - Manifest.permission.READ_EXTERNAL_STORAGE, REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION); - return; - } - launchPickImageFromGalleryIntent(); } @@ -424,16 +408,6 @@ public boolean onRequestPermissionsResult( grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED; switch (requestCode) { - case REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION: - if (permissionGranted) { - launchPickImageFromGalleryIntent(); - } - break; - case REQUEST_EXTERNAL_VIDEO_STORAGE_PERMISSION: - if (permissionGranted) { - launchPickVideoFromGalleryIntent(); - } - break; case REQUEST_CAMERA_IMAGE_PERMISSION: if (permissionGranted) { launchTakeImageWithCameraIntent(); @@ -450,10 +424,6 @@ public boolean onRequestPermissionsResult( if (!permissionGranted) { switch (requestCode) { - case REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION: - case REQUEST_EXTERNAL_VIDEO_STORAGE_PERMISSION: - finishWithError("photo_access_denied", "The user did not allow photo access."); - break; case REQUEST_CAMERA_IMAGE_PERMISSION: case REQUEST_CAMERA_VIDEO_PERMISSION: finishWithError("camera_access_denied", "The user did not allow camera access."); 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/src/main/java/io/flutter/plugins/imagepicker/ImagePickerFileProvider.java index ca7f6b064b39..7416665c49c1 100644 --- a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerFileProvider.java +++ b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerFileProvider.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter 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/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 index 74556f9cd6cc..bffc903b531e 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -7,7 +7,6 @@ import android.app.Activity; import android.app.Application; import android.os.Bundle; -import android.os.Environment; import android.os.Handler; import android.os.Looper; import androidx.annotation.NonNull; @@ -216,11 +215,11 @@ private void tearDown() { application = null; } - private final ImagePickerDelegate constructDelegate(final Activity setupActivity) { + @VisibleForTesting + final ImagePickerDelegate constructDelegate(final Activity setupActivity) { final ImagePickerCache cache = new ImagePickerCache(setupActivity); - final File externalFilesDirectory = - setupActivity.getExternalFilesDir(Environment.DIRECTORY_PICTURES); + final File externalFilesDirectory = setupActivity.getCacheDir(); final ExifDataCopier exifDataCopier = new ExifDataCopier(); final ImageResizer imageResizer = new ImageResizer(externalFilesDirectory, exifDataCopier); return new ImagePickerDelegate(setupActivity, externalFilesDirectory, imageResizer, cache); 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/src/main/java/io/flutter/plugins/imagepicker/ImagePickerUtils.java index 65b05e7ac3cc..ba9878925575 100644 --- a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerUtils.java +++ b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerUtils.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter 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/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java index 27a145567a31..2a93785678af 100644 --- a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java +++ b/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImageResizer.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter 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/android/src/main/res/xml/flutter_image_picker_file_paths.xml b/packages/image_picker/image_picker/android/src/main/res/xml/flutter_image_picker_file_paths.xml index 4495c28c86d1..354418bd40ca 100644 --- a/packages/image_picker/image_picker/android/src/main/res/xml/flutter_image_picker_file_paths.xml +++ b/packages/image_picker/image_picker/android/src/main/res/xml/flutter_image_picker_file_paths.xml @@ -1,4 +1,4 @@ - - \ No newline at end of file + + diff --git a/packages/image_picker/image_picker/example/README.md b/packages/image_picker/image_picker/example/README.md index 4a33db1ce92d..129aa856c8f2 100755 --- a/packages/image_picker/image_picker/example/README.md +++ b/packages/image_picker/image_picker/example/README.md @@ -5,4 +5,4 @@ Demonstrates how to use the image_picker plugin. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). diff --git a/packages/image_picker/image_picker/example/android/app/build.gradle b/packages/image_picker/image_picker/example/android/app/build.gradle index f4b1e02ede35..cc77d33eed0d 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 28 + compileSdkVersion 29 testOptions.unitTests.includeAndroidResources = true lintOptions { @@ -60,7 +60,7 @@ flutter { dependencies { testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:2.17.0' + testImplementation 'org.mockito:mockito-core:3.10.0' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' testImplementation 'androidx.test:core:1.2.0' diff --git a/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1Activity.java b/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1Activity.java index 55f3f92eef7c..b9d2808a4486 100644 --- a/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1Activity.java +++ b/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1Activity.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1ActivityTest.java b/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1ActivityTest.java index e7fcd25142ad..7d790563abae 100644 --- a/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1ActivityTest.java +++ b/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1ActivityTest.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java b/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java index 58f8df35dc4f..1ca37ce5feb7 100644 --- a/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java +++ b/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java @@ -1,3 +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. + package io.flutter.plugins.imagepickerexample; import androidx.test.rule.ActivityTestRule; diff --git a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java b/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java index c9fa3381ebe5..32e3ebc6183d 100644 --- a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java +++ b/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -54,4 +54,13 @@ public void FileUtil_GetPathFromUri() throws IOException { String imageStream = new String(bytes, UTF_8); assertTrue(imageStream.equals("imageStream")); } + + @Test + public void FileUtil_getImageExtension() throws IOException { + Uri uri = Uri.parse("content://dummy/dummy.png"); + shadowContentResolver.registerInputStream( + uri, new ByteArrayInputStream("imageStream".getBytes(UTF_8))); + String path = fileUtils.getPathFromUri(context, uri); + assertTrue(path.endsWith(".jpg")); + } } diff --git a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java b/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java index 51733a503a92..92070e7a65c5 100644 --- a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java +++ b/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter 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/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java b/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java index aa9b00521f53..da53b10b50f5 100644 --- a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java +++ b/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java @@ -1,3 +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. + package io.flutter.plugins.imagepicker; import static org.hamcrest.core.IsEqual.equalTo; @@ -5,6 +9,7 @@ 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.verifyNoMoreInteractions; import static org.mockito.Mockito.when; @@ -21,6 +26,8 @@ import org.junit.Before; import org.junit.Test; import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; public class ImagePickerDelegateTest { @@ -96,20 +103,6 @@ public void chooseImageFromGallery_WhenPendingResultExists_FinishesWithAlreadyAc verifyNoMoreInteractions(mockResult); } - @Test - public void chooseImageFromGallery_WhenHasNoExternalStoragePermission_RequestsForPermission() { - when(mockPermissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE)) - .thenReturn(false); - - ImagePickerDelegate delegate = createDelegate(); - delegate.chooseImageFromGallery(mockMethodCall, mockResult); - - verify(mockPermissionManager) - .askForPermission( - Manifest.permission.READ_EXTERNAL_STORAGE, - ImagePickerDelegate.REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION); - } - @Test public void chooseImageFromGallery_WhenHasExternalStoragePermission_LaunchesChooseFromGalleryIntent() { @@ -189,47 +182,21 @@ public void takeImageWithCamera_WhenCameraPermissionNotPresent_RequestsForPermis } @Test - public void - onRequestPermissionsResult_WhenReadExternalStoragePermissionDenied_FinishesWithError() { - ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); - - delegate.onRequestPermissionsResult( - ImagePickerDelegate.REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION, - new String[] {Manifest.permission.READ_EXTERNAL_STORAGE}, - new int[] {PackageManager.PERMISSION_DENIED}); - - verify(mockResult).error("photo_access_denied", "The user did not allow photo access.", null); - verifyNoMoreInteractions(mockResult); - } - - @Test - public void - onRequestChooseImagePermissionsResult_WhenReadExternalStorageGranted_LaunchesChooseImageFromGalleryIntent() { - ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); + public void takeImageWithCamera_WritesImageToCacheDirectory() { + when(mockPermissionManager.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(true); + when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true); - delegate.onRequestPermissionsResult( - ImagePickerDelegate.REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION, - new String[] {Manifest.permission.READ_EXTERNAL_STORAGE}, - new int[] {PackageManager.PERMISSION_GRANTED}); + MockedStatic mockStaticFile = Mockito.mockStatic(File.class); + mockStaticFile + .when(() -> File.createTempFile(any(), any(), any())) + .thenReturn(new File("/tmpfile")); - verify(mockActivity) - .startActivityForResult( - any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY)); - } - - @Test - public void - onRequestChooseVideoPermissionsResult_WhenReadExternalStorageGranted_LaunchesChooseVideoFromGalleryIntent() { - ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); - - delegate.onRequestPermissionsResult( - ImagePickerDelegate.REQUEST_EXTERNAL_VIDEO_STORAGE_PERMISSION, - new String[] {Manifest.permission.READ_EXTERNAL_STORAGE}, - new int[] {PackageManager.PERMISSION_GRANTED}); + ImagePickerDelegate delegate = createDelegate(); + delegate.takeImageWithCamera(mockMethodCall, mockResult); - verify(mockActivity) - .startActivityForResult( - any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_CHOOSE_VIDEO_FROM_GALLERY)); + mockStaticFile.verify( + () -> File.createTempFile(any(), eq(".jpg"), eq(new File("/image_picker_cache"))), + times(1)); } @Test @@ -390,7 +357,7 @@ public void onActivityResult_WhenImageTakenWithCamera_AndNoResizeNeeded_Finishes private ImagePickerDelegate createDelegate() { return new ImagePickerDelegate( mockActivity, - null, + new File("/image_picker_cache"), mockImageResizer, null, null, diff --git a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java b/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java index de1623e93db4..a0ce87f4f2b7 100644 --- a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java +++ b/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java @@ -1,8 +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.imagepicker; +import static org.hamcrest.core.IsEqual.equalTo; +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.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; @@ -11,6 +18,7 @@ import android.app.Application; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; +import java.io.File; import java.util.HashMap; import java.util.Map; import org.junit.Before; @@ -145,6 +153,20 @@ public void onConstructor_WhenContextTypeIsActivity_ShouldNotCrash() { "No exception thrown when ImagePickerPlugin() ran with context instanceof Activity", true); } + @Test + public void constructDelegate_ShouldUseInternalCacheDirectory() { + File mockDirectory = new File("/mockpath"); + when(mockActivity.getCacheDir()).thenReturn(mockDirectory); + + ImagePickerDelegate delegate = plugin.constructDelegate(mockActivity); + + verify(mockActivity, times(1)).getCacheDir(); + assertThat( + "Delegate uses cache directory for storing camera captures", + delegate.externalFilesDirectory, + equalTo(mockDirectory)); + } + 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/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java b/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java index 4968d844f824..73cfef9e88ea 100644 --- a/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java +++ b/packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter 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/example/android/build.gradle b/packages/image_picker/image_picker/example/android/build.gradle index 541636cc492a..e101ac08df55 100755 --- a/packages/image_picker/image_picker/example/android/build.gradle +++ b/packages/image_picker/image_picker/example/android/build.gradle @@ -1,7 +1,7 @@ buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -12,7 +12,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/packages/image_picker/image_picker/example/integration_test/old_image_picker_test.dart b/packages/image_picker/image_picker/example/integration_test/old_image_picker_test.dart new file mode 100644 index 000000000000..120c9e221c24 --- /dev/null +++ b/packages/image_picker/image_picker/example/integration_test/old_image_picker_test.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. + +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); +} diff --git a/packages/image_picker/image_picker/example/ios/Podfile b/packages/image_picker/image_picker/example/ios/Podfile new file mode 100644 index 000000000000..75efae48b439 --- /dev/null +++ b/packages/image_picker/image_picker/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 + inherit! :search_paths + end + target 'RunnerUITests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end 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 106d49cad0c7..547c2be4f914 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,27 +7,44 @@ 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 */; }; - 680049262280D736006DD6AB /* MetaDataUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 680049252280D736006DD6AB /* MetaDataUtilTests.m */; }; - 680049272280D79A006DD6AB /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 680049382280F2B9006DD6AB /* pngImage.png in Resources */ = {isa = PBXBuildFile; fileRef = 680049352280F2B8006DD6AB /* pngImage.png */; }; 680049392280F2B9006DD6AB /* jpgImage.jpg in Resources */ = {isa = PBXBuildFile; fileRef = 680049362280F2B8006DD6AB /* jpgImage.jpg */; }; - 68B9AF72243E4B3F00927CE4 /* ImagePickerPluginTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68B9AF71243E4B3F00927CE4 /* ImagePickerPluginTests.m */; }; - 68F4B464228B3AB500C25614 /* PhotoAssetUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68F4B463228B3AB500C25614 /* PhotoAssetUtilTests.m */; }; + 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 */; }; - 9FC8F0EE229FB90B00C8D58F /* ImageUtilTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 9FC8F0ED229FB90B00C8D58F /* ImageUtilTests.m */; }; + BE7AEE7926403CC8006181AA /* ImagePickerFromLimitedGalleryUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = BE7AEE7826403CC8006181AA /* ImagePickerFromLimitedGalleryUITests.m */; }; F4F7A436CCA4BF276270A3AE /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = EC32F6993F4529982D9519F1 /* libPods-Runner.a */; }; - F78AF3192342D9D7008449C7 /* ImagePickerTestImages.m in Sources */ = {isa = PBXBuildFile; fileRef = F78AF3182342D9D7008449C7 /* ImagePickerTestImages.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 6800491C2280D368006DD6AB /* PBXContainerItemProxy */ = { + 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; + }; + BE7AEE7126403C46006181AA /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 97C146E61CF9000F007C117D /* Project object */; proxyType = 1; @@ -50,18 +67,25 @@ /* 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 = ""; }; + 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 = ""; }; - 680049172280D368006DD6AB /* image_picker_exampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = image_picker_exampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 6800491B2280D368006DD6AB /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 680049252280D736006DD6AB /* MetaDataUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = MetaDataUtilTests.m; path = ../../../ios/Tests/MetaDataUtilTests.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 = ""; }; - 68B9AF71243E4B3F00927CE4 /* ImagePickerPluginTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = ImagePickerPluginTests.m; path = ../../../ios/Tests/ImagePickerPluginTests.m; sourceTree = ""; }; - 68F4B463228B3AB500C25614 /* PhotoAssetUtilTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = PhotoAssetUtilTests.m; path = ../../../ios/Tests/PhotoAssetUtilTests.m; 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 = ""; }; @@ -74,17 +98,31 @@ 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; name = ImageUtilTests.m; path = ../../../ios/Tests/ImageUtilTests.m; 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; }; + BE7AEE6C26403C46006181AA /* RunnerUITestiOS14.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITestiOS14.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 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; name = ImagePickerTestImages.h; path = ../../../ios/Tests/ImagePickerTestImages.h; sourceTree = ""; }; - F78AF3182342D9D7008449C7 /* ImagePickerTestImages.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = ImagePickerTestImages.m; path = ../../../ios/Tests/ImagePickerTestImages.m; sourceTree = ""; }; + 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 */ - 680049142280D368006DD6AB /* Frameworks */ = { + 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; }; @@ -96,21 +134,28 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + BE7AEE6926403C46006181AA /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 680049182280D368006DD6AB /* image_picker_exampleTests */ = { + 334733F32668136400DCC49E /* RunnerTests */ = { isa = PBXGroup; children = ( - 6800491B2280D368006DD6AB /* Info.plist */, 9FC8F0ED229FB90B00C8D58F /* ImageUtilTests.m */, 680049252280D736006DD6AB /* MetaDataUtilTests.m */, 68F4B463228B3AB500C25614 /* PhotoAssetUtilTests.m */, F78AF3172342D9D7008449C7 /* ImagePickerTestImages.h */, F78AF3182342D9D7008449C7 /* ImagePickerTestImages.m */, 68B9AF71243E4B3F00927CE4 /* ImagePickerPluginTests.m */, + 334733F62668136400DCC49E /* Info.plist */, ); - path = image_picker_exampleTests; + path = RunnerTests; sourceTree = ""; }; 680049282280E33D006DD6AB /* TestImages */ = { @@ -123,11 +168,24 @@ path = TestImages; sourceTree = ""; }; + 6801C8372555D726009DAF8D /* RunnerUITests */ = { + isa = PBXGroup; + children = ( + 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 */, ); name = Pods; sourceTree = ""; @@ -149,7 +207,9 @@ 680049282280E33D006DD6AB /* TestImages */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, - 680049182280D368006DD6AB /* image_picker_exampleTests */, + 334733F32668136400DCC49E /* RunnerTests */, + 6801C8372555D726009DAF8D /* RunnerUITests */, + BE7AEE6D26403C46006181AA /* RunnerUITestiOS14 */, 97C146EF1CF9000F007C117D /* Products */, 840012C8B5EDBCF56B0E4AC1 /* Pods */, CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, @@ -160,7 +220,9 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, - 680049172280D368006DD6AB /* image_picker_exampleTests.xctest */, + 6801C8362555D726009DAF8D /* RunnerUITests.xctest */, + BE7AEE6C26403C46006181AA /* RunnerUITestiOS14.xctest */, + 334733F22668136400DCC49E /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -189,10 +251,21 @@ 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; sourceTree = ""; @@ -200,24 +273,44 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ - 680049162280D368006DD6AB /* image_picker_exampleTests */ = { + 334733F12668136400DCC49E /* RunnerTests */ = { isa = PBXNativeTarget; - buildConfigurationList = 6800491E2280D368006DD6AB /* Build configuration list for PBXNativeTarget "image_picker_exampleTests" */; + buildConfigurationList = 334733F92668136400DCC49E /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 680049132280D368006DD6AB /* Sources */, - 680049142280D368006DD6AB /* Frameworks */, - 680049152280D368006DD6AB /* Resources */, + B8739A4353234497CF76B597 /* [CP] Check Pods Manifest.lock */, + 334733EE2668136400DCC49E /* Sources */, + 334733EF2668136400DCC49E /* Frameworks */, + 334733F02668136400DCC49E /* Resources */, ); buildRules = ( ); dependencies = ( - 6800491D2280D368006DD6AB /* PBXTargetDependency */, + 334733F82668136400DCC49E /* PBXTargetDependency */, ); - name = image_picker_exampleTests; - productName = image_picker_exampleTests; - productReference = 680049172280D368006DD6AB /* image_picker_exampleTests.xctest */; + 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" */; @@ -228,7 +321,6 @@ 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, - 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, ); buildRules = ( @@ -240,6 +332,24 @@ productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; + BE7AEE6B26403C46006181AA /* RunnerUITestiOS14 */ = { + isa = PBXNativeTarget; + buildConfigurationList = BE7AEE7526403C46006181AA /* Build configuration list for PBXNativeTarget "RunnerUITestiOS14" */; + buildPhases = ( + BE7AEE6826403C46006181AA /* Sources */, + BE7AEE6926403C46006181AA /* Frameworks */, + BE7AEE6A26403C46006181AA /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + BE7AEE7226403C46006181AA /* PBXTargetDependency */, + ); + name = RunnerUITestiOS14; + productName = RunnerUITestiOS14; + productReference = BE7AEE6C26403C46006181AA /* RunnerUITestiOS14.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -248,10 +358,15 @@ attributes = { DefaultBuildSystemTypeForWorkspace = Original; LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; + ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { - 680049162280D368006DD6AB = { - CreatedOnToolsVersion = 10.2.1; + 334733F12668136400DCC49E = { + CreatedOnToolsVersion = 12.5; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 6801C8352555D726009DAF8D = { + CreatedOnToolsVersion = 11.7; ProvisioningStyle = Automatic; TestTargetID = 97C146ED1CF9000F007C117D; }; @@ -263,6 +378,11 @@ }; }; }; + BE7AEE6B26403C46006181AA = { + CreatedOnToolsVersion = 12.4; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; @@ -279,17 +399,25 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, - 680049162280D368006DD6AB /* image_picker_exampleTests */, + 334733F12668136400DCC49E /* RunnerTests */, + 6801C8352555D726009DAF8D /* RunnerUITests */, + BE7AEE6B26403C46006181AA /* RunnerUITestiOS14 */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ - 680049152280D368006DD6AB /* Resources */ = { + 334733F02668136400DCC49E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6801C8342555D726009DAF8D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 680049272280D79A006DD6AB /* Assets.xcassets in Resources */, 9FC8F0EC229FA68500C8D58F /* gifImage.gif in Resources */, 680049382280F2B9006DD6AB /* pngImage.png in Resources */, 680049392280F2B9006DD6AB /* jpgImage.jpg in Resources */, @@ -307,6 +435,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + BE7AEE6A26403C46006181AA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -324,22 +459,26 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */ = { + 4F8C1F500AF4DCAB62651A1E /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${PODS_ROOT}/../Flutter/Flutter.framework", + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( ); - name = "[CP] Embed Pods Frameworks"; outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Flutter.framework", + "$(DERIVED_FILE_DIR)/Pods-RunnerUITests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\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; }; 9740EEB61CF901F6004384FC /* Run Script */ = { @@ -374,18 +513,48 @@ 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 */ - 680049132280D368006DD6AB /* Sources */ = { + 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 = ( - 9FC8F0EE229FB90B00C8D58F /* ImageUtilTests.m in Sources */, - F78AF3192342D9D7008449C7 /* ImagePickerTestImages.m in Sources */, - 680049262280D736006DD6AB /* MetaDataUtilTests.m in Sources */, - 68B9AF72243E4B3F00927CE4 /* ImagePickerPluginTests.m in Sources */, - 68F4B464228B3AB500C25614 /* PhotoAssetUtilTests.m in Sources */, + 6801C8392555D726009DAF8D /* ImagePickerFromGalleryUITests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -399,13 +568,31 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + BE7AEE6826403C46006181AA /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + BE7AEE7926403CC8006181AA /* ImagePickerFromLimitedGalleryUITests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 6800491D2280D368006DD6AB /* PBXTargetDependency */ = { + 334733F82668136400DCC49E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 334733F72668136400DCC49E /* PBXContainerItemProxy */; + }; + 6801C83C2555D726009DAF8D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 6801C83B2555D726009DAF8D /* PBXContainerItemProxy */; + }; + BE7AEE7226403C46006181AA /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 6800491C2280D368006DD6AB /* PBXContainerItemProxy */; + targetProxy = BE7AEE7126403C46006181AA /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ @@ -429,60 +616,85 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ - 6800491F2280D368006DD6AB /* Debug */ = { + 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_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - INFOPLIST_FILE = image_picker_exampleTests/Info.plist; + "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.transformTest.image-picker-exampleTests"; + PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerUITests; PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Runner; }; name = Debug; }; - 680049202280D368006DD6AB /* Release */ = { + 6801C83E2555D726009DAF8D /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* 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_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - INFOPLIST_FILE = image_picker_exampleTests/Info.plist; + "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.transformTest.image-picker-exampleTests"; + PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerUITests; PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Runner; }; name = Release; }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -539,7 +751,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -605,7 +816,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.imagePickerExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.imagePickerExample; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -627,19 +838,75 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.imagePickerExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.imagePickerExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; + BE7AEE7326403C46006181AA /* 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; + DEVELOPMENT_TEAM = ""; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = RunnerUITestiOS14/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + 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.baseflow.RunnerUITestiOS14; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Runner; + }; + name = Debug; + }; + BE7AEE7426403C46006181AA /* 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; + DEVELOPMENT_TEAM = NHAKRD9N7D; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = RunnerUITestiOS14/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 14.1; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.baseflow.RunnerUITestiOS14; PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Runner; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ - 6800491E2280D368006DD6AB /* Build configuration list for PBXNativeTarget "image_picker_exampleTests" */ = { + 334733F92668136400DCC49E /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { isa = XCConfigurationList; buildConfigurations = ( - 6800491F2280D368006DD6AB /* Debug */, - 680049202280D368006DD6AB /* Release */, + 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; @@ -662,6 +929,15 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + BE7AEE7526403C46006181AA /* Build configuration list for PBXNativeTarget "RunnerUITestiOS14" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + BE7AEE7326403C46006181AA /* Debug */, + BE7AEE7426403C46006181AA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; diff --git a/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 21a3cc14c74e..919434a6254f 100755 --- a/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,9 +2,6 @@ - - + location = "self:"> 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 7a9aea57bd9d..b100e5cd18d7 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 @@ + + + + + + + + diff --git a/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerUITests.xcscheme b/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerUITests.xcscheme new file mode 100644 index 000000000000..1a97d9638346 --- /dev/null +++ b/packages/image_picker/image_picker/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerUITests.xcscheme @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/image_picker/image_picker/example/ios/Runner/AppDelegate.h b/packages/image_picker/image_picker/example/ios/Runner/AppDelegate.h index d9e18e990f2e..0681d288bb70 100644 --- a/packages/image_picker/image_picker/example/ios/Runner/AppDelegate.h +++ b/packages/image_picker/image_picker/example/ios/Runner/AppDelegate.h @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/example/ios/Runner/AppDelegate.m b/packages/image_picker/image_picker/example/ios/Runner/AppDelegate.m index a4b51c88eb60..b790a0a52635 100644 --- a/packages/image_picker/image_picker/example/ios/Runner/AppDelegate.m +++ b/packages/image_picker/image_picker/example/ios/Runner/AppDelegate.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/example/ios/Runner/main.m b/packages/image_picker/image_picker/example/ios/Runner/main.m index bec320c0bee0..f97b9ef5c8a1 100644 --- a/packages/image_picker/image_picker/example/ios/Runner/main.m +++ b/packages/image_picker/image_picker/example/ios/Runner/main.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/Tests/ImagePickerPluginTests.m b/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m similarity index 98% rename from packages/image_picker/image_picker/ios/Tests/ImagePickerPluginTests.m rename to packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m index c8d5a2bb5368..04ba4b98e241 100644 --- a/packages/image_picker/image_picker/ios/Tests/ImagePickerPluginTests.m +++ b/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/Tests/ImagePickerTestImages.h b/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerTestImages.h similarity index 86% rename from packages/image_picker/image_picker/ios/Tests/ImagePickerTestImages.h rename to packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerTestImages.h index 7173e1c455ba..1074a5c62455 100644 --- a/packages/image_picker/image_picker/ios/Tests/ImagePickerTestImages.h +++ b/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerTestImages.h @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/Tests/ImagePickerTestImages.m b/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerTestImages.m similarity index 99% rename from packages/image_picker/image_picker/ios/Tests/ImagePickerTestImages.m rename to packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerTestImages.m index 53559f0b637b..a0bae7b8f91c 100644 --- a/packages/image_picker/image_picker/ios/Tests/ImagePickerTestImages.m +++ b/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerTestImages.m @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/example/ios/RunnerTests/ImageUtilTests.m b/packages/image_picker/image_picker/example/ios/RunnerTests/ImageUtilTests.m new file mode 100644 index 000000000000..b793d6e1f3e0 --- /dev/null +++ b/packages/image_picker/image_picker/example/ios/RunnerTests/ImageUtilTests.m @@ -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. + +#import "ImagePickerTestImages.h" + +@import image_picker; +@import XCTest; + +@interface ImageUtilTests : XCTestCase +@end + +@implementation ImageUtilTests + +- (void)testScaledImage_ShouldBeScaled { + UIImage *image = [UIImage imageWithData:ImagePickerTestImages.JPGTestData]; + UIImage *newImage = [FLTImagePickerImageUtil scaledImage:image + maxWidth:@3 + maxHeight:@2 + isMetadataAvailable:YES]; + + XCTAssertEqual(newImage.size.width, 3); + XCTAssertEqual(newImage.size.height, 2); +} + +- (void)testScaledImage_ShouldBeScaledWithNoMetadata { + UIImage *image = [UIImage imageWithData:ImagePickerTestImages.JPGTestData]; + UIImage *newImage = [FLTImagePickerImageUtil scaledImage:image + maxWidth:@3 + maxHeight:@2 + isMetadataAvailable:NO]; + + XCTAssertEqual(newImage.size.width, 3); + XCTAssertEqual(newImage.size.height, 2); +} + +- (void)testScaledImage_ShouldBeCorrectRotation { + UIImage *image = [UIImage imageWithData:ImagePickerTestImages.JPGTestData]; + UIImage *newImage = [FLTImagePickerImageUtil scaledImage:image + maxWidth:@3 + maxHeight:@2 + isMetadataAvailable:YES]; + + XCTAssertEqual(newImage.imageOrientation, UIImageOrientationUp); +} + +- (void)testScaledGIFImage_ShouldBeScaled { + // gif image that frame size is 3 and the duration is 1 second. + GIFInfo *info = [FLTImagePickerImageUtil scaledGIFImage:ImagePickerTestImages.GIFTestData + maxWidth:@3 + maxHeight:@2]; + + NSArray *images = info.images; + NSTimeInterval duration = info.interval; + + XCTAssertEqual(images.count, 3); + XCTAssertEqual(duration, 1); + + for (UIImage *newImage in images) { + XCTAssertEqual(newImage.size.width, 3); + XCTAssertEqual(newImage.size.height, 2); + } +} + +@end diff --git a/packages/image_picker/image_picker/example/ios/RunnerTests/Info.plist b/packages/image_picker/image_picker/example/ios/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/image_picker/image_picker/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/image_picker/image_picker/ios/Tests/MetaDataUtilTests.m b/packages/image_picker/image_picker/example/ios/RunnerTests/MetaDataUtilTests.m similarity index 98% rename from packages/image_picker/image_picker/ios/Tests/MetaDataUtilTests.m rename to packages/image_picker/image_picker/example/ios/RunnerTests/MetaDataUtilTests.m index 120ba3890a0e..e1dbfad77b5d 100644 --- a/packages/image_picker/image_picker/ios/Tests/MetaDataUtilTests.m +++ b/packages/image_picker/image_picker/example/ios/RunnerTests/MetaDataUtilTests.m @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/Tests/PhotoAssetUtilTests.m b/packages/image_picker/image_picker/example/ios/RunnerTests/PhotoAssetUtilTests.m similarity index 92% rename from packages/image_picker/image_picker/ios/Tests/PhotoAssetUtilTests.m rename to packages/image_picker/image_picker/example/ios/RunnerTests/PhotoAssetUtilTests.m index 7491c907724c..b81b29f73cef 100644 --- a/packages/image_picker/image_picker/ios/Tests/PhotoAssetUtilTests.m +++ b/packages/image_picker/image_picker/example/ios/RunnerTests/PhotoAssetUtilTests.m @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -17,6 +17,18 @@ - (void)getAssetFromImagePickerInfoShouldReturnNilIfNotAvailable { XCTAssertNil([FLTImagePickerPhotoAssetUtil getAssetFromImagePickerInfo:mockData]); } +- (void)testGetAssetFromPHPickerResultShouldReturnNilIfNotAvailable API_AVAILABLE(ios(14)) { + if (@available(iOS 14, *)) { + PHPickerResult *mockData; + [mockData.itemProvider + loadObjectOfClass:[UIImage class] + completionHandler:^(__kindof id _Nullable image, + NSError *_Nullable error) { + XCTAssertNil([FLTImagePickerPhotoAssetUtil getAssetFromPHPickerResult:mockData]); + }]; + } +} + - (void)testSaveImageWithOriginalImageData_ShouldSaveWithTheCorrectExtentionAndMetaData { // test jpg NSData *dataJPG = ImagePickerTestImages.JPGTestData; diff --git a/packages/image_picker/image_picker/example/ios/RunnerUITestiOS14/ImagePickerFromLimitedGalleryUITests.m b/packages/image_picker/image_picker/example/ios/RunnerUITestiOS14/ImagePickerFromLimitedGalleryUITests.m new file mode 100644 index 000000000000..86cad03d27cf --- /dev/null +++ b/packages/image_picker/image_picker/example/ios/RunnerUITestiOS14/ImagePickerFromLimitedGalleryUITests.m @@ -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. + +#import +#import + +const int kLimitedElementWaitingTime = 30; + +@interface ImagePickerFromLimitedGalleryUITests : XCTestCase + +@property(nonatomic, strong) XCUIApplication* app; + +@end + +@implementation ImagePickerFromLimitedGalleryUITests + +- (void)setUp { + [super setUp]; + // Delete the app if already exists, to test permission popups + + self.continueAfterFailure = NO; + self.app = [[XCUIApplication alloc] init]; + [self.app launch]; + __weak typeof(self) weakSelf = self; + [self addUIInterruptionMonitorWithDescription:@"Permission popups" + handler:^BOOL(XCUIElement* _Nonnull interruptingElement) { + if (@available(iOS 14, *)) { + XCUIElement* limitedPhotoPermission = + [interruptingElement.buttons elementBoundByIndex:0]; + if (![limitedPhotoPermission + waitForExistenceWithTimeout: + kLimitedElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", + weakSelf.app.debugDescription); + XCTFail(@"Failed due to not able to find " + @"selectPhotos butt on with %@ seconds", + @(kLimitedElementWaitingTime)); + } + [limitedPhotoPermission tap]; + } else { + XCUIElement* ok = interruptingElement.buttons[@"OK"]; + if (![ok waitForExistenceWithTimeout: + kLimitedElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", + weakSelf.app.debugDescription); + XCTFail(@"Failed due to not able to find ok button " + @"with %@ seconds", + @(kLimitedElementWaitingTime)); + } + [ok tap]; + } + return YES; + }]; +} + +- (void)tearDown { + [super tearDown]; + [self.app terminate]; +} + +- (void)testSelectingFromGallery { + [self launchPickerAndSelect]; +} + +- (void)launchPickerAndSelect { + // Find and tap on the pick from gallery button. + NSPredicate* predicateToFindImageFromGalleryButton = + [NSPredicate predicateWithFormat:@"label == %@", @"image_picker_example_from_gallery"]; + + XCUIElement* imageFromGalleryButton = + [self.app.otherElements elementMatchingPredicate:predicateToFindImageFromGalleryButton]; + if (![imageFromGalleryButton waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); + XCTFail(@"Failed due to not able to find image from gallery button with %@ seconds", + @(kLimitedElementWaitingTime)); + } + + XCTAssertTrue(imageFromGalleryButton.exists); + [imageFromGalleryButton tap]; + + // Find and tap on the `pick` button. + NSPredicate* predicateToFindPickButton = + [NSPredicate predicateWithFormat:@"label == %@", @"PICK"]; + + 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..."); + } + + XCTAssertTrue(pickButton.exists); + [pickButton tap]; + + // There is a known bug where the permission popups interruption won't get fired until a tap + // happened in the app. We expect a permission popup so we do a tap here. + [self.app tap]; + + // Find an image and tap on it. (IOS 14 UI, images are showing directly) + XCUIElement* aImage; + if (@available(iOS 14, *)) { + aImage = [self.app.scrollViews.firstMatch.images elementBoundByIndex:1]; + } else { + XCUIElement* selectedPhotosCell = [self.app.cells + elementMatchingPredicate:[NSPredicate + predicateWithFormat:@"label == %@", @"Selected Photos"]]; + if (![selectedPhotosCell waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); + XCTFail(@"Failed due to not able to find \"Selected Photos\" cell with %@ seconds", + @(kLimitedElementWaitingTime)); + } + [selectedPhotosCell tap]; + aImage = [self.app.collectionViews elementMatchingType:XCUIElementTypeCollectionView + identifier:@"PhotosGridView"] + .cells.firstMatch; + } + os_log_error(OS_LOG_DEFAULT, "description before picking image %@", self.app.debugDescription); + if (![aImage waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); + XCTFail(@"Failed due to not able to find an image with %@ seconds", + @(kLimitedElementWaitingTime)); + } + XCTAssertTrue(aImage.exists); + [aImage tap]; + + // Find and tap on the `Done` button. + NSPredicate* predicateToFindDoneButton = + [NSPredicate predicateWithFormat:@"label == %@", @"Done"]; + + 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..."); + } + + XCTAssertTrue(doneButton.exists); + [doneButton tap]; + + // Find an image and tap on it to have access to selected photos. + if (@available(iOS 14, *)) { + aImage = [self.app.scrollViews.firstMatch.images elementBoundByIndex:1]; + } else { + XCUIElement* selectedPhotosCell = [self.app.cells + elementMatchingPredicate:[NSPredicate + predicateWithFormat:@"label == %@", @"Selected Photos"]]; + if (![selectedPhotosCell waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); + XCTFail(@"Failed due to not able to find \"Selected Photos\" cell with %@ seconds", + @(kLimitedElementWaitingTime)); + } + [selectedPhotosCell tap]; + aImage = [self.app.collectionViews elementMatchingType:XCUIElementTypeCollectionView + identifier:@"PhotosGridView"] + .cells.firstMatch; + } + os_log_error(OS_LOG_DEFAULT, "description before picking image %@", self.app.debugDescription); + if (![aImage waitForExistenceWithTimeout:kLimitedElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); + XCTFail(@"Failed due to not able to find an image with %@ seconds", + @(kLimitedElementWaitingTime)); + } + XCTAssertTrue(aImage.exists); + [aImage tap]; + + // Find the picked image. + NSPredicate* predicateToFindPickedImage = + [NSPredicate predicateWithFormat:@"label == %@", @"image_picker_example_picked_image"]; + + 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", + @(kLimitedElementWaitingTime)); + } + + XCTAssertTrue(pickedImage.exists); +} + +@end diff --git a/packages/image_picker/image_picker/example/ios/RunnerUITestiOS14/Info.plist b/packages/image_picker/image_picker/example/ios/RunnerUITestiOS14/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/image_picker/image_picker/example/ios/RunnerUITestiOS14/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m b/packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m new file mode 100644 index 000000000000..4b2163d00577 --- /dev/null +++ b/packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromGalleryUITests.m @@ -0,0 +1,203 @@ +// Copyright 2013 The Flutter Authors. 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 + +const int kElementWaitingTime = 30; + +@interface ImagePickerFromGalleryUITests : XCTestCase + +@property(nonatomic, strong) XCUIApplication* app; + +@end + +@implementation ImagePickerFromGalleryUITests + +- (void)setUp { + [super setUp]; + // Delete the app if already exists, to test permission popups + + self.continueAfterFailure = NO; + self.app = [[XCUIApplication alloc] init]; + [self.app launch]; + __weak typeof(self) weakSelf = self; + [self addUIInterruptionMonitorWithDescription:@"Permission popups" + handler:^BOOL(XCUIElement* _Nonnull interruptingElement) { + if (@available(iOS 14, *)) { + XCUIElement* allPhotoPermission = + interruptingElement + .buttons[@"Allow Access to All Photos"]; + if (![allPhotoPermission waitForExistenceWithTimeout: + kElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", + weakSelf.app.debugDescription); + XCTFail(@"Failed due to not able to find " + @"allPhotoPermission button with %@ seconds", + @(kElementWaitingTime)); + } + [allPhotoPermission tap]; + } else { + XCUIElement* ok = interruptingElement.buttons[@"OK"]; + if (![ok waitForExistenceWithTimeout: + kElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", + weakSelf.app.debugDescription); + XCTFail(@"Failed due to not able to find ok button " + @"with %@ seconds", + @(kElementWaitingTime)); + } + [ok tap]; + } + return YES; + }]; +} + +- (void)tearDown { + [super tearDown]; + [self.app terminate]; +} + +- (void)testPickingFromGallery { + [self launchPickerAndPick]; +} + +- (void)testCancel { + [self launchPickerAndCancel]; +} + +- (void)launchPickerAndCancel { + // Find and tap on the pick from gallery button. + NSPredicate* predicateToFindImageFromGalleryButton = + [NSPredicate predicateWithFormat:@"label == %@", @"image_picker_example_from_gallery"]; + + XCUIElement* imageFromGalleryButton = + [self.app.otherElements elementMatchingPredicate:predicateToFindImageFromGalleryButton]; + if (![imageFromGalleryButton waitForExistenceWithTimeout:kElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); + XCTFail(@"Failed due to not able to find image from gallery button with %@ seconds", + @(kElementWaitingTime)); + } + + XCTAssertTrue(imageFromGalleryButton.exists); + [imageFromGalleryButton tap]; + + // Find and tap on the `pick` button. + NSPredicate* predicateToFindPickButton = + [NSPredicate predicateWithFormat:@"label == %@", @"PICK"]; + + 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)); + } + + XCTAssertTrue(pickButton.exists); + [pickButton tap]; + + // There is a known bug where the permission popups interruption won't get fired until a tap + // happened in the app. We expect a permission popup so we do a tap here. + [self.app tap]; + + // Find and tap on the `Cancel` button. + NSPredicate* predicateToFindCancelButton = + [NSPredicate predicateWithFormat:@"label == %@", @"Cancel"]; + + XCUIElement* cancelButton = + [self.app.buttons elementMatchingPredicate:predicateToFindCancelButton]; + if (![cancelButton waitForExistenceWithTimeout:kElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); + XCTFail(@"Failed due to not able to find Cancel button with %@ seconds", + @(kElementWaitingTime)); + } + + XCTAssertTrue(cancelButton.exists); + [cancelButton tap]; + + // Find the "not picked image text". + XCUIElement* imageNotPickedText = [self.app.staticTexts + elementMatchingPredicate:[NSPredicate + predicateWithFormat:@"label == %@", + @"You have not yet picked an image."]]; + if (![imageNotPickedText waitForExistenceWithTimeout:kElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); + XCTFail(@"Failed due to not able to find imageNotPickedText with %@ seconds", + @(kElementWaitingTime)); + } + + XCTAssertTrue(imageNotPickedText.exists); +} + +- (void)launchPickerAndPick { + // Find and tap on the pick from gallery button. + NSPredicate* predicateToFindImageFromGalleryButton = + [NSPredicate predicateWithFormat:@"label == %@", @"image_picker_example_from_gallery"]; + + XCUIElement* imageFromGalleryButton = + [self.app.otherElements elementMatchingPredicate:predicateToFindImageFromGalleryButton]; + if (![imageFromGalleryButton waitForExistenceWithTimeout:kElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); + XCTFail(@"Failed due to not able to find image from gallery button with %@ seconds", + @(kElementWaitingTime)); + } + + XCTAssertTrue(imageFromGalleryButton.exists); + [imageFromGalleryButton tap]; + + // Find and tap on the `pick` button. + NSPredicate* predicateToFindPickButton = + [NSPredicate predicateWithFormat:@"label == %@", @"PICK"]; + + 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)); + } + + XCTAssertTrue(pickButton.exists); + [pickButton tap]; + + // There is a known bug where the permission popups interruption won't get fired until a tap + // happened in the app. We expect a permission popup so we do a tap here. + [self.app tap]; + + // Find an image and tap on it. (IOS 14 UI, images are showing directly) + XCUIElement* aImage; + if (@available(iOS 14, *)) { + aImage = [self.app.scrollViews.firstMatch.images elementBoundByIndex:1]; + } else { + XCUIElement* allPhotosCell = [self.app.cells + elementMatchingPredicate:[NSPredicate predicateWithFormat:@"label == %@", @"All Photos"]]; + if (![allPhotosCell waitForExistenceWithTimeout:kElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); + XCTFail(@"Failed due to not able to find \"All Photos\" cell with %@ seconds", + @(kElementWaitingTime)); + } + [allPhotosCell tap]; + aImage = [self.app.collectionViews elementMatchingType:XCUIElementTypeCollectionView + identifier:@"PhotosGridView"] + .cells.firstMatch; + } + os_log_error(OS_LOG_DEFAULT, "description before picking image %@", self.app.debugDescription); + if (![aImage waitForExistenceWithTimeout:kElementWaitingTime]) { + os_log_error(OS_LOG_DEFAULT, "%@", self.app.debugDescription); + XCTFail(@"Failed due to not able to find an image with %@ seconds", @(kElementWaitingTime)); + } + XCTAssertTrue(aImage.exists); + [aImage tap]; + + // Find the picked image. + NSPredicate* predicateToFindPickedImage = + [NSPredicate predicateWithFormat:@"label == %@", @"image_picker_example_picked_image"]; + + 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)); + } + + XCTAssertTrue(pickedImage.exists); +} + +@end diff --git a/packages/image_picker/image_picker/example/ios/RunnerUITests/Info.plist b/packages/image_picker/image_picker/example/ios/RunnerUITests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/image_picker/image_picker/example/ios/RunnerUITests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/packages/image_picker/image_picker/example/lib/main.dart b/packages/image_picker/image_picker/example/lib/main.dart index ece8c45d9c8e..ff9f4a03cebc 100755 --- a/packages/image_picker/image_picker/example/lib/main.dart +++ b/packages/image_picker/image_picker/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -29,60 +29,63 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); + MyHomePage({Key? key, this.title}) : super(key: key); - final String title; + final String? title; @override _MyHomePageState createState() => _MyHomePageState(); } class _MyHomePageState extends State { - PickedFile _imageFile; + PickedFile? _imageFile; dynamic _pickImageError; bool isVideo = false; - VideoPlayerController _controller; - VideoPlayerController _toBeDisposed; - String _retrieveDataError; + VideoPlayerController? _controller; + VideoPlayerController? _toBeDisposed; + String? _retrieveDataError; final ImagePicker _picker = ImagePicker(); final TextEditingController maxWidthController = TextEditingController(); final TextEditingController maxHeightController = TextEditingController(); final TextEditingController qualityController = TextEditingController(); - Future _playVideo(PickedFile file) async { + Future _playVideo(PickedFile? file) async { if (file != null && mounted) { await _disposeVideoController(); + late VideoPlayerController controller; if (kIsWeb) { - _controller = VideoPlayerController.network(file.path); - // In web, most browsers won't honor a programmatic call to .play - // if the video has a sound track (and is not muted). - // Mute the video so it auto-plays in web! - // This is not needed if the call to .play is the result of user - // interaction (clicking on a "play" button, for example). - await _controller.setVolume(0.0); + controller = VideoPlayerController.network(file.path); } else { - _controller = VideoPlayerController.file(File(file.path)); - await _controller.setVolume(1.0); + controller = VideoPlayerController.file(File(file.path)); } - await _controller.initialize(); - await _controller.setLooping(true); - await _controller.play(); + _controller = controller; + // In web, most browsers won't honor a programmatic call to .play + // if the video has a sound track (and is not muted). + // Mute the video so it auto-plays in web! + // This is not needed if the call to .play is the result of user + // interaction (clicking on a "play" button, for example). + final double volume = kIsWeb ? 0.0 : 1.0; + await controller.setVolume(volume); + await controller.initialize(); + await controller.setLooping(true); + await controller.play(); setState(() {}); } } - void _onImageButtonPressed(ImageSource source, {BuildContext context}) async { + void _onImageButtonPressed(ImageSource source, + {BuildContext? context}) async { if (_controller != null) { - await _controller.setVolume(0.0); + await _controller!.setVolume(0.0); } if (isVideo) { - final PickedFile file = await _picker.getVideo( + final PickedFile? file = await _picker.getVideo( source: source, maxDuration: const Duration(seconds: 10)); await _playVideo(file); } else { - await _displayPickImageDialog(context, - (double maxWidth, double maxHeight, int quality) async { + await _displayPickImageDialog(context!, + (double? maxWidth, double? maxHeight, int? quality) async { try { final pickedFile = await _picker.getImage( source: source, @@ -105,8 +108,8 @@ class _MyHomePageState extends State { @override void deactivate() { if (_controller != null) { - _controller.setVolume(0.0); - _controller.pause(); + _controller!.setVolume(0.0); + _controller!.pause(); } super.deactivate(); } @@ -122,14 +125,14 @@ class _MyHomePageState extends State { Future _disposeVideoController() async { if (_toBeDisposed != null) { - await _toBeDisposed.dispose(); + await _toBeDisposed!.dispose(); } _toBeDisposed = _controller; _controller = null; } Widget _previewVideo() { - final Text retrieveError = _getRetrieveErrorWidget(); + final Text? retrieveError = _getRetrieveErrorWidget(); if (retrieveError != null) { return retrieveError; } @@ -146,7 +149,7 @@ class _MyHomePageState extends State { } Widget _previewImage() { - final Text retrieveError = _getRetrieveErrorWidget(); + final Text? retrieveError = _getRetrieveErrorWidget(); if (retrieveError != null) { return retrieveError; } @@ -154,9 +157,11 @@ class _MyHomePageState extends State { if (kIsWeb) { // Why network? // See https://pub.dev/packages/image_picker#getting-ready-for-the-web-platform - return Image.network(_imageFile.path); + return Image.network(_imageFile!.path); } else { - return Image.file(File(_imageFile.path)); + return Semantics( + child: Image.file(File(_imageFile!.path)), + label: 'image_picker_example_picked_image'); } } else if (_pickImageError != null) { return Text( @@ -187,7 +192,7 @@ class _MyHomePageState extends State { }); } } else { - _retrieveDataError = response.exception.code; + _retrieveDataError = response.exception!.code; } } @@ -195,7 +200,7 @@ class _MyHomePageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(widget.title), + title: Text(widget.title!), ), body: Center( child: !kIsWeb && defaultTargetPlatform == TargetPlatform.android @@ -231,14 +236,17 @@ class _MyHomePageState extends State { floatingActionButton: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ - FloatingActionButton( - onPressed: () { - isVideo = false; - _onImageButtonPressed(ImageSource.gallery, context: context); - }, - heroTag: 'image0', - tooltip: 'Pick Image from gallery', - child: const Icon(Icons.photo_library), + 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_library), + ), ), Padding( padding: const EdgeInsets.only(top: 16.0), @@ -283,9 +291,9 @@ class _MyHomePageState extends State { ); } - Text _getRetrieveErrorWidget() { + Text? _getRetrieveErrorWidget() { if (_retrieveDataError != null) { - final Text result = Text(_retrieveDataError); + final Text result = Text(_retrieveDataError!); _retrieveDataError = null; return result; } @@ -322,22 +330,22 @@ class _MyHomePageState extends State { ], ), actions: [ - FlatButton( + TextButton( child: const Text('CANCEL'), onPressed: () { Navigator.of(context).pop(); }, ), - FlatButton( + TextButton( child: const Text('PICK'), onPressed: () { - double width = maxWidthController.text.isNotEmpty + double? width = maxWidthController.text.isNotEmpty ? double.parse(maxWidthController.text) : null; - double height = maxHeightController.text.isNotEmpty + double? height = maxHeightController.text.isNotEmpty ? double.parse(maxHeightController.text) : null; - int quality = qualityController.text.isNotEmpty + int? quality = qualityController.text.isNotEmpty ? int.parse(qualityController.text) : null; onPick(width, height, quality); @@ -350,27 +358,27 @@ class _MyHomePageState extends State { } typedef void OnPickImageCallback( - double maxWidth, double maxHeight, int quality); + double? maxWidth, double? maxHeight, int? quality); class AspectRatioVideo extends StatefulWidget { AspectRatioVideo(this.controller); - final VideoPlayerController controller; + final VideoPlayerController? controller; @override AspectRatioVideoState createState() => AspectRatioVideoState(); } class AspectRatioVideoState extends State { - VideoPlayerController get controller => widget.controller; + VideoPlayerController? get controller => widget.controller; bool initialized = false; void _onVideoControllerUpdate() { if (!mounted) { return; } - if (initialized != controller.value.initialized) { - initialized = controller.value.initialized; + if (initialized != controller!.value.isInitialized) { + initialized = controller!.value.isInitialized; setState(() {}); } } @@ -378,12 +386,12 @@ class AspectRatioVideoState extends State { @override void initState() { super.initState(); - controller.addListener(_onVideoControllerUpdate); + controller!.addListener(_onVideoControllerUpdate); } @override void dispose() { - controller.removeListener(_onVideoControllerUpdate); + controller!.removeListener(_onVideoControllerUpdate); super.dispose(); } @@ -392,8 +400,8 @@ class AspectRatioVideoState extends State { if (initialized) { return Center( child: AspectRatio( - aspectRatio: controller.value?.aspectRatio, - child: VideoPlayer(controller), + aspectRatio: controller!.value.aspectRatio, + child: VideoPlayer(controller!), ), ); } else { diff --git a/packages/image_picker/image_picker/example/pubspec.yaml b/packages/image_picker/image_picker/example/pubspec.yaml index 0ff2f280e2ab..422bd5a4120d 100755 --- a/packages/image_picker/image_picker/example/pubspec.yaml +++ b/packages/image_picker/image_picker/example/pubspec.yaml @@ -1,26 +1,30 @@ name: image_picker_example description: Demonstrates how to use the image_picker plugin. -author: Flutter Team +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.10.0" dependencies: - video_player: ^0.10.3 + video_player: ^2.1.4 flutter: sdk: flutter - flutter_plugin_android_lifecycle: ^1.0.2 + flutter_plugin_android_lifecycle: ^2.0.1 image_picker: + # When depending on this package from a real application you should use: + # image_picker: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ - image_picker_for_web: ^0.1.0 dev_dependencies: flutter_driver: sdk: flutter integration_test: - path: ../../../integration_test - pedantic: ^1.8.0 + sdk: flutter + pedantic: ^1.10.0 flutter: uses-material-design: true - -environment: - sdk: ">=2.0.0-dev.28.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" diff --git a/packages/image_picker/image_picker/example/test_driver/integration_test.dart b/packages/image_picker/image_picker/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/image_picker/image_picker/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/example/test_driver/test/integration_test.dart b/packages/image_picker/image_picker/example/test_driver/test/integration_test.dart deleted file mode 100644 index 7a2c21338786..000000000000 --- a/packages/image_picker/image_picker/example/test_driver/test/integration_test.dart +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'package:flutter_driver/flutter_driver.dart'; - -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String data = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - await driver.close(); - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} diff --git a/packages/image_picker/image_picker/example/web/index.html b/packages/image_picker/image_picker/example/web/index.html index 787bbc72f6b1..b05fdf840323 100644 --- a/packages/image_picker/image_picker/example/web/index.html +++ b/packages/image_picker/image_picker/example/web/index.html @@ -1,4 +1,7 @@ + diff --git a/packages/image_picker/image_picker/integration_test/old_image_picker_test.dart b/packages/image_picker/image_picker/integration_test/old_image_picker_test.dart deleted file mode 100644 index d21a4e0cdfa3..000000000000 --- a/packages/image_picker/image_picker/integration_test/old_image_picker_test.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:integration_test/integration_test.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); -} diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerImageUtil.h b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerImageUtil.h index e809744f76d9..b0edd03e5076 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerImageUtil.h +++ b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerImageUtil.h @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -20,7 +20,8 @@ NS_ASSUME_NONNULL_BEGIN + (UIImage *)scaledImage:(UIImage *)image maxWidth:(NSNumber *)maxWidth - maxHeight:(NSNumber *)maxHeight; + maxHeight:(NSNumber *)maxHeight + isMetadataAvailable:(BOOL)isMetadataAvailable; // Resize all gif animation frames. + (GIFInfo *)scaledGIFImage:(NSData *)data diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerImageUtil.m b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerImageUtil.m index ab765208d0bc..7b454072ecff 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerImageUtil.m +++ b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerImageUtil.m @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -30,7 +30,8 @@ @implementation FLTImagePickerImageUtil : NSObject + (UIImage *)scaledImage:(UIImage *)image maxWidth:(NSNumber *)maxWidth - maxHeight:(NSNumber *)maxHeight { + maxHeight:(NSNumber *)maxHeight + isMetadataAvailable:(BOOL)isMetadataAvailable { double originalWidth = image.size.width; double originalHeight = image.size.height; @@ -69,6 +70,19 @@ + (UIImage *)scaledImage:(UIImage *)image } } + if (!isMetadataAvailable) { + UIImage *imageToScale = [UIImage imageWithCGImage:image.CGImage + scale:1 + orientation:image.imageOrientation]; + + UIGraphicsBeginImageContextWithOptions(CGSizeMake(width, height), NO, 1.0); + [imageToScale drawInRect:CGRectMake(0, 0, width, height)]; + + UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return scaledImage; + } + // Scaling the image always rotate itself based on the current imageOrientation of the original // Image. Set to orientationUp for the orignal image before scaling, so the scaled image doesn't // mess up with the pixels. @@ -130,7 +144,7 @@ + (GIFInfo *)scaledGIFImage:(NSData *)data } UIImage *image = [UIImage imageWithCGImage:imageRef scale:1.0 orientation:UIImageOrientationUp]; - image = [self scaledImage:image maxWidth:maxWidth maxHeight:maxHeight]; + image = [self scaledImage:image maxWidth:maxWidth maxHeight:maxHeight isMetadataAvailable:YES]; [images addObject:image]; diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.h b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.h index 9f7c19aae1b4..d5a20ffc6d2e 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.h +++ b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.h @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter 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/Classes/FLTImagePickerMetaDataUtil.m b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.m index 9786f61e1e67..1419584a4675 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.m +++ b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.m @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter 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/Classes/FLTImagePickerPhotoAssetUtil.h b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.h index 1e6fda2cf786..0016765a0fe0 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.h +++ b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.h @@ -1,9 +1,10 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. 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 "FLTImagePickerImageUtil.h" @@ -13,6 +14,8 @@ NS_ASSUME_NONNULL_BEGIN + (nullable PHAsset *)getAssetFromImagePickerInfo:(NSDictionary *)info; ++ (nullable PHAsset *)getAssetFromPHPickerResult:(PHPickerResult *)result API_AVAILABLE(ios(14)); + // Save image with correct meta data and extention copied from the original asset. // maxWidth and maxHeight are used only for GIF images. + (NSString *)saveImageWithOriginalImageData:(NSData *)originalImageData diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m index f6727334060a..ab881790d5ab 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m +++ b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPhotoAssetUtil.m @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -23,6 +23,12 @@ + (PHAsset *)getAssetFromImagePickerInfo:(NSDictionary *)info { return result.firstObject; } ++ (PHAsset *)getAssetFromPHPickerResult:(PHPickerResult *)result API_AVAILABLE(ios(14)) { + PHFetchResult *fetchResult = [PHAsset fetchAssetsWithLocalIdentifiers:@[ result.assetIdentifier ] + options:nil]; + return fetchResult.firstObject; +} + + (NSString *)saveImageWithOriginalImageData:(NSData *)originalImageData image:(UIImage *)image maxWidth:(NSNumber *)maxWidth @@ -92,11 +98,11 @@ + (NSString *)saveImageWithMetaData:(NSDictionary *)metaData CGImageDestinationRef destination = CGImageDestinationCreateWithURL( (CFURLRef)[NSURL fileURLWithPath:path], kUTTypeGIF, gifInfo.images.count, NULL); - NSDictionary *frameProperties = [NSDictionary - dictionaryWithObject:[NSDictionary - dictionaryWithObject:[NSNumber numberWithFloat:gifInfo.interval] - forKey:(NSString *)kCGImagePropertyGIFDelayTime] - forKey:(NSString *)kCGImagePropertyGIFDictionary]; + NSDictionary *frameProperties = @{ + (__bridge NSString *)kCGImagePropertyGIFDictionary : @{ + (__bridge NSString *)kCGImagePropertyGIFDelayTime : @(gifInfo.interval), + }, + }; NSMutableDictionary *gifMetaProperties = [NSMutableDictionary dictionaryWithDictionary:metaData]; NSMutableDictionary *gifProperties = @@ -105,7 +111,7 @@ + (NSString *)saveImageWithMetaData:(NSDictionary *)metaData gifProperties = [NSMutableDictionary dictionary]; } - gifProperties[(NSString *)kCGImagePropertyGIFLoopCount] = [NSNumber numberWithFloat:0]; + gifProperties[(__bridge NSString *)kCGImagePropertyGIFLoopCount] = @0; CGImageDestinationSetProperties(destination, (CFDictionaryRef)gifMetaProperties); diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.h b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.h index b6d8687a32e3..ffd23cd3df6a 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.h +++ b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.h @@ -1,8 +1,9 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. 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 FLTImagePickerPlugin : NSObject diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m index 00fdec245aaf..e3df6413e9a8 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m +++ b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -7,23 +7,32 @@ #import #import #import +#import +#import #import #import "FLTImagePickerImageUtil.h" #import "FLTImagePickerMetaDataUtil.h" #import "FLTImagePickerPhotoAssetUtil.h" -@interface FLTImagePickerPlugin () +@interface FLTImagePickerPlugin () @property(copy, nonatomic) FlutterResult result; +@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 { - NSDictionary *_arguments; UIImagePickerController *_imagePickerController; UIImagePickerControllerCameraDevice _device; } @@ -58,6 +67,47 @@ - (UIViewController *)viewControllerWithWindow:(UIWindow *)window { return topController; } +- (void)pickImageWithPHPicker:(bool)single API_AVAILABLE(ios(14)) { + PHPickerConfiguration *config = + [[PHPickerConfiguration alloc] initWithPhotoLibrary:PHPhotoLibrary.sharedPhotoLibrary]; + if (!single) { + config.selectionLimit = 0; // Setting to zero allow us to pick unlimited photos + } + config.filter = [PHPickerFilter imagesFilter]; + + _pickerViewController = [[PHPickerViewController alloc] initWithConfiguration:config]; + _pickerViewController.delegate = self; + + [self checkPhotoAuthorizationForAccessLevel]; +} + +- (void)pickImageWithUIImagePicker { + _imagePickerController = [[UIImagePickerController alloc] init]; + _imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; + _imagePickerController.delegate = self; + _imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ]; + + int imageSource = [[_arguments objectForKey:@"source"] intValue]; + + switch (imageSource) { + case SOURCE_CAMERA: { + NSInteger cameraDevice = [[_arguments objectForKey:@"cameraDevice"] intValue]; + _device = (cameraDevice == 1) ? UIImagePickerControllerCameraDeviceFront + : UIImagePickerControllerCameraDeviceRear; + [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" @@ -67,32 +117,26 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } if ([@"pickImage" isEqualToString:call.method]) { - _imagePickerController = [[UIImagePickerController alloc] init]; - _imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; - _imagePickerController.delegate = self; - _imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ]; - self.result = result; _arguments = call.arguments; - int imageSource = [[_arguments objectForKey:@"source"] intValue]; - switch (imageSource) { - case SOURCE_CAMERA: { - NSInteger cameraDevice = [[_arguments objectForKey:@"cameraDevice"] intValue]; - _device = (cameraDevice == 1) ? UIImagePickerControllerCameraDeviceFront - : UIImagePickerControllerCameraDeviceRear; - [self checkCameraAuthorization]; - break; + if (imageSource == SOURCE_GALLERY) { // Capture is not possible with PHPicker + if (@available(iOS 14, *)) { + // PHPicker is used + [self pickImageWithPHPicker:true]; + } else { + // UIImagePicker is used + [self pickImageWithUIImagePicker]; } - case SOURCE_GALLERY: - [self checkPhotoAuthorization]; - break; - default: - result([FlutterError errorWithCode:@"invalid_source" - message:@"Invalid image source." - details:nil]); - break; + } else { + [self pickImageWithUIImagePicker]; + } + } else if ([@"pickMultiImage" isEqualToString:call.method]) { + if (@available(iOS 14, *)) { + self.result = result; + _arguments = call.arguments; + [self pickImageWithPHPicker:false]; } } else if ([@"pickVideo" isEqualToString:call.method]) { _imagePickerController = [[UIImagePickerController alloc] init]; @@ -146,11 +190,20 @@ - (void)showCamera { animated:YES completion:nil]; } else { - [[[UIAlertView alloc] initWithTitle:@"Error" - message:@"Camera not available." - delegate:nil - cancelButtonTitle:@"OK" - otherButtonTitles:nil] show]; + 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; @@ -167,19 +220,16 @@ - (void)checkCameraAuthorization { case AVAuthorizationStatusNotDetermined: { [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) { - if (granted) { - dispatch_async(dispatch_get_main_queue(), ^{ - if (granted) { - [self showCamera]; - } - }); - } else { - dispatch_async(dispatch_get_main_queue(), ^{ + dispatch_async(dispatch_get_main_queue(), ^{ + if (granted) { + [self showCamera]; + } else { [self errorNoCameraAccess:AVAuthorizationStatusDenied]; - }); - } + } + }); }]; - }; break; + break; + } case AVAuthorizationStatusDenied: case AVAuthorizationStatusRestricted: default: @@ -193,18 +243,49 @@ - (void)checkPhotoAuthorization { switch (status) { case PHAuthorizationStatusNotDetermined: { [PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) { - if (status == PHAuthorizationStatusAuthorized) { - dispatch_async(dispatch_get_main_queue(), ^{ - [self showPhotoLibrary]; - }); - } else { - [self errorNoPhotoAccess:status]; - } + dispatch_async(dispatch_get_main_queue(), ^{ + if (status == PHAuthorizationStatusAuthorized) { + [self showPhotoLibrary:UIImagePickerClassType]; + } else { + [self errorNoPhotoAccess:status]; + } + }); }]; break; } case PHAuthorizationStatusAuthorized: - [self showPhotoLibrary]; + [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: @@ -246,12 +327,85 @@ - (void)errorNoPhotoAccess:(PHAuthorizationStatus)status { } } -- (void)showPhotoLibrary { +- (void)showPhotoLibrary:(ImagePickerClassType)imagePickerClassType { // No need to check if SourceType is available. It always is. - _imagePickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; - [[self viewControllerWithWindow:nil] presentViewController:_imagePickerController - animated:YES - completion:nil]; + 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)picker:(PHPickerViewController *)picker + didFinishPicking:(NSArray *)results API_AVAILABLE(ios(14)) { + [picker dismissViewControllerAnimated:YES completion:nil]; + + NSNumber *maxWidth = [_arguments objectForKey:@"maxWidth"]; + NSNumber *maxHeight = [_arguments objectForKey:@"maxHeight"]; + NSNumber *imageQuality = [_arguments objectForKey:@"imageQuality"]; + NSNumber *desiredImageQuality = [self getDesiredImageQuality:imageQuality]; + + for (PHPickerResult *result in results) { + [result.itemProvider + loadObjectOfClass:[UIImage class] + completionHandler:^(__kindof id _Nullable image, + NSError *_Nullable error) { + if ([image isKindOfClass:[UIImage class]]) { + __block UIImage *localImage = image; + dispatch_async(dispatch_get_main_queue(), ^{ + PHAsset *originalAsset = + [FLTImagePickerPhotoAssetUtil getAssetFromPHPickerResult:result]; + + if (maxWidth != (id)[NSNull null] || maxHeight != (id)[NSNull null]) { + localImage = [FLTImagePickerImageUtil scaledImage:localImage + maxWidth:maxWidth + maxHeight:maxHeight + isMetadataAvailable:originalAsset != nil]; + } + + if (!originalAsset) { + // Image picked without an original asset (e.g. User took a photo directly) + [self saveImageWithPickerInfo:nil + image:localImage + 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:localImage + maxWidth:maxWidth + maxHeight:maxHeight + imageQuality:desiredImageQuality]; + }]; + } + }); + } + }]; + } } - (void)imagePickerController:(UIImagePickerController *)picker @@ -295,40 +449,35 @@ - (void)imagePickerController:(UIImagePickerController *)picker 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]; - if (![imageQuality isKindOfClass:[NSNumber class]]) { - imageQuality = @1; - } else if (imageQuality.intValue < 0 || imageQuality.intValue > 100) { - imageQuality = [NSNumber numberWithInt:1]; - } else { - imageQuality = @([imageQuality floatValue] / 100); - } + PHAsset *originalAsset = [FLTImagePickerPhotoAssetUtil getAssetFromImagePickerInfo:info]; if (maxWidth != (id)[NSNull null] || maxHeight != (id)[NSNull null]) { - image = [FLTImagePickerImageUtil scaledImage:image maxWidth:maxWidth maxHeight:maxHeight]; + image = [FLTImagePickerImageUtil scaledImage:image + maxWidth:maxWidth + maxHeight:maxHeight + isMetadataAvailable:YES]; } - PHAsset *originalAsset = [FLTImagePickerPhotoAssetUtil getAssetFromImagePickerInfo:info]; if (!originalAsset) { // Image picked without an original asset (e.g. User took a photo directly) - [self saveImageWithPickerInfo:info image:image imageQuality:imageQuality]; + [self saveImageWithPickerInfo:info image:image imageQuality:desiredImageQuality]; } else { - __weak typeof(self) weakSelf = self; [[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. - [weakSelf saveImageWithOriginalImageData:imageData - image:image - maxWidth:maxWidth - maxHeight:maxHeight - imageQuality:imageQuality]; + [self saveImageWithOriginalImageData:imageData + image:image + maxWidth:maxWidth + maxHeight:maxHeight + imageQuality:desiredImageQuality]; }]; } } diff --git a/packages/image_picker/image_picker/ios/Tests/ImageUtilTests.m b/packages/image_picker/image_picker/ios/Tests/ImageUtilTests.m deleted file mode 100644 index 126795f8bdc9..000000000000 --- a/packages/image_picker/image_picker/ios/Tests/ImageUtilTests.m +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "ImagePickerTestImages.h" - -@import image_picker; -@import XCTest; - -@interface ImageUtilTests : XCTestCase -@end - -@implementation ImageUtilTests - -- (void)testScaledImage_ShouldBeScaled { - UIImage *image = [UIImage imageWithData:ImagePickerTestImages.JPGTestData]; - UIImage *newImage = [FLTImagePickerImageUtil scaledImage:image maxWidth:@3 maxHeight:@2]; - - XCTAssertEqual(newImage.size.width, 3); - XCTAssertEqual(newImage.size.height, 2); -} - -- (void)testScaledGIFImage_ShouldBeScaled { - // gif image that frame size is 3 and the duration is 1 second. - GIFInfo *info = [FLTImagePickerImageUtil scaledGIFImage:ImagePickerTestImages.GIFTestData - maxWidth:@3 - maxHeight:@2]; - - NSArray *images = info.images; - NSTimeInterval duration = info.interval; - - XCTAssertEqual(images.count, 3); - XCTAssertEqual(duration, 1); - - for (UIImage *newImage in images) { - XCTAssertEqual(newImage.size.width, 3); - XCTAssertEqual(newImage.size.height, 2); - } -} - -@end diff --git a/packages/image_picker/image_picker/ios/image_picker.podspec b/packages/image_picker/image_picker/ios/image_picker.podspec index 5c13cef272dd..0d33b79e61f0 100644 --- a/packages/image_picker/image_picker/ios/image_picker.podspec +++ b/packages/image_picker/image_picker/ios/image_picker.podspec @@ -18,9 +18,5 @@ Downloaded by pub (not CocoaPods). 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' } - - s.test_spec 'Tests' do |test_spec| - test_spec.source_files = 'Tests/**/*' - end + 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 ff9aa2cbecc3..77c26d40346a 100755 --- a/packages/image_picker/image_picker/lib/image_picker.dart +++ b/packages/image_picker/image_picker/lib/image_picker.dart @@ -1,11 +1,10 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package import 'dart:async'; -import 'dart:io'; import 'package:flutter/foundation.dart'; @@ -18,7 +17,6 @@ export 'package:image_picker_platform_interface/image_picker_platform_interface. ImageSource, CameraDevice, LostData, - LostDataResponse, PickedFile, RetrieveType; @@ -27,47 +25,7 @@ export 'package:image_picker_platform_interface/image_picker_platform_interface. class ImagePicker { /// The platform interface that drives this plugin @visibleForTesting - static ImagePickerPlatform platform = ImagePickerPlatform.instance; - - /// Returns a [File] object pointing to the image that was picked. - /// - /// The returned [File] is intended to be used within a single APP session. Do not save the file path and use it across sessions. - /// - /// The `source` argument controls where the image comes from. This can - /// be either [ImageSource.camera] or [ImageSource.gallery]. - /// - /// If specified, the image will be at most `maxWidth` wide and - /// `maxHeight` tall. Otherwise the image will be returned at it's - /// original width and height. - /// The `imageQuality` argument modifies the quality of the image, ranging from 0-100 - /// where 100 is the original/max quality. If `imageQuality` is null, the image with - /// the original quality will be returned. Compression is only supportted for certain - /// image types such as JPEG. If compression is not supported for the image that is picked, - /// an warning message will be logged. - /// - /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. - /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. - /// Defaults to [CameraDevice.rear]. - /// - /// In Android, the MainActivity can be destroyed for various reasons. If that happens, the result will be lost - /// in this call. You can then call [retrieveLostData] when your app relaunches to retrieve the lost data. - @Deprecated('Use imagePicker.getImage() method instead.') - static Future pickImage( - {@required ImageSource source, - double maxWidth, - double maxHeight, - int imageQuality, - CameraDevice preferredCameraDevice = CameraDevice.rear}) async { - String path = await platform.pickImagePath( - source: source, - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: imageQuality, - preferredCameraDevice: preferredCameraDevice, - ); - - return path == null ? null : File(path); - } + static ImagePickerPlatform get platform => ImagePickerPlatform.instance; /// Returns a [PickedFile] object wrapping the image that was picked. /// @@ -76,6 +34,9 @@ class ImagePicker { /// The `source` argument controls where the image comes from. This can /// be either [ImageSource.camera] or [ImageSource.gallery]. /// + /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and above only support HEIC images if used + /// in addition to a size modification, of which the usage is explained below. + /// /// If specified, the image will be at most `maxWidth` wide and /// `maxHeight` tall. Otherwise the image will be returned at it's /// original width and height. @@ -93,11 +54,11 @@ class ImagePicker { /// /// In Android, the MainActivity can be destroyed for various reasons. If that happens, the result will be lost /// in this call. You can then call [getLostData] when your app relaunches to retrieve the lost data. - Future getImage({ - @required ImageSource source, - double maxWidth, - double maxHeight, - int imageQuality, + Future getImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { return platform.pickImage( @@ -109,36 +70,6 @@ class ImagePicker { ); } - /// Returns a [File] object pointing to the video that was picked. - /// - /// The returned [File] is intended to be used within a single APP session. Do not save the file path and use it across sessions. - /// - /// The [source] argument controls where the video comes from. This can - /// be either [ImageSource.camera] or [ImageSource.gallery]. - /// - /// The [maxDuration] argument specifies the maximum duration of the captured video. If no [maxDuration] is specified, - /// the maximum duration will be infinite. - /// - /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. - /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. - /// Defaults to [CameraDevice.rear]. - /// - /// In Android, the MainActivity can be destroyed for various fo reasons. If that happens, the result will be lost - /// in this call. You can then call [retrieveLostData] when your app relaunches to retrieve the lost data. - @Deprecated('Use imagePicker.getVideo() method instead.') - static Future pickVideo( - {@required ImageSource source, - CameraDevice preferredCameraDevice = CameraDevice.rear, - Duration maxDuration}) async { - String path = await platform.pickVideoPath( - source: source, - preferredCameraDevice: preferredCameraDevice, - maxDuration: maxDuration, - ); - - return path == null ? null : File(path); - } - /// Returns a [PickedFile] object wrapping the video that was picked. /// /// The returned [PickedFile] is intended to be used within a single APP session. Do not save the file path and use it across sessions. @@ -155,10 +86,10 @@ class ImagePicker { /// /// In Android, the MainActivity can be destroyed for various fo reasons. If that happens, the result will be lost /// in this call. You can then call [getLostData] when your app relaunches to retrieve the lost data. - Future getVideo({ - @required ImageSource source, + Future getVideo({ + required ImageSource source, CameraDevice preferredCameraDevice = CameraDevice.rear, - Duration maxDuration, + Duration? maxDuration, }) { return platform.pickVideo( source: source, @@ -167,23 +98,6 @@ class ImagePicker { ); } - /// Retrieve the lost image file when [pickImage] or [pickVideo] failed because the MainActivity is destroyed. (Android only) - /// - /// Image or video can be lost if the MainActivity is destroyed. And there is no guarantee that the MainActivity is always alive. - /// Call this method to retrieve the lost data and process the data according to your APP's business logic. - /// - /// Returns a [LostDataResponse] if successfully retrieved the lost data. The [LostDataResponse] can represent either a - /// successful image/video selection, or a failure. - /// - /// Calling this on a non-Android platform will throw [UnimplementedError] exception. - /// - /// See also: - /// * [LostDataResponse], for what's included in the response. - /// * [Android Activity Lifecycle](https://developer.android.com/reference/android/app/Activity.html), for more information on MainActivity destruction. - static Future retrieveLostData() { - return platform.retrieveLostDataAsDartIoFile(); - } - /// Retrieve the lost [PickedFile] when [selectImage] or [selectVideo] failed because the MainActivity is destroyed. (Android only) /// /// Image or video can be lost if the MainActivity is destroyed. And there is no guarantee that the MainActivity is always alive. diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index 6eaa0d685bd8..bf42015e3193 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -1,8 +1,13 @@ name: image_picker description: Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. -homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker -version: 0.6.7+11 +repository: https://github.com/flutter/plugins/tree/master/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.0+3 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: @@ -12,21 +17,19 @@ flutter: pluginClass: ImagePickerPlugin ios: pluginClass: FLTImagePickerPlugin + web: + default_package: image_picker_for_web dependencies: flutter: sdk: flutter - flutter_plugin_android_lifecycle: ^1.0.2 - image_picker_platform_interface: ^1.1.0 + flutter_plugin_android_lifecycle: ^2.0.1 + image_picker_platform_interface: ^2.0.0 + image_picker_for_web: ^2.0.0 dev_dependencies: - video_player: ^0.10.3 flutter_test: sdk: flutter - integration_test: - path: ../../integration_test - pedantic: ^1.8.0 - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + 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_test.dart b/packages/image_picker/image_picker/test/image_picker_test.dart index 0ada1b261363..f56d47ff262b 100644 --- a/packages/image_picker/image_picker/test/image_picker_test.dart +++ b/packages/image_picker/image_picker/test/image_picker_test.dart @@ -1,10 +1,13 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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/image_picker.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:mockito/mockito.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -26,6 +29,15 @@ void main() { log.clear(); }); + test('ImagePicker platform instance overrides the actual platform used', + () { + final ImagePickerPlatform savedPlatform = ImagePickerPlatform.instance; + final MockPlatform mockPlatform = MockPlatform(); + ImagePickerPlatform.instance = mockPlatform; + expect(ImagePicker.platform, mockPlatform); + ImagePickerPlatform.instance = savedPlatform; + }); + group('#pickImage', () { test('passes the image source argument correctly', () async { await picker.getImage(source: ImageSource.camera); @@ -298,7 +310,7 @@ void main() { }); final LostData response = await picker.getLostData(); expect(response.type, RetrieveType.image); - expect(response.file.path, '/example/path'); + expect(response.file!.path, '/example/path'); }); test('retrieveLostData get error response', () async { @@ -311,8 +323,8 @@ void main() { }); final LostData response = await picker.getLostData(); expect(response.type, RetrieveType.video); - expect(response.exception.code, 'test_error_code'); - expect(response.exception.message, 'test_error_message'); + expect(response.exception!.code, 'test_error_code'); + expect(response.exception!.message, 'test_error_message'); }); test('retrieveLostData get null response', () async { @@ -336,3 +348,7 @@ void main() { }); }); } + +class MockPlatform extends Mock + with MockPlatformInterfaceMixin + implements ImagePickerPlatform {} diff --git a/packages/image_picker/image_picker/test/old_image_picker_test.dart b/packages/image_picker/image_picker/test/old_image_picker_test.dart deleted file mode 100644 index 8d4e068a261c..000000000000 --- a/packages/image_picker/image_picker/test/old_image_picker_test.dart +++ /dev/null @@ -1,340 +0,0 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// ignore_for_file: deprecated_member_use, deprecated_member_use_from_same_package - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:image_picker/image_picker.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('$ImagePicker', () { - const MethodChannel channel = - MethodChannel('plugins.flutter.io/image_picker'); - - final List log = []; - - setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - return ''; - }); - - log.clear(); - }); - - group('#pickImage', () { - test('passes the image source argument correctly', () async { - await ImagePicker.pickImage(source: ImageSource.camera); - await ImagePicker.pickImage(source: ImageSource.gallery); - - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 1, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - ], - ); - }); - - test('passes the width and height arguments correctly', () async { - await ImagePicker.pickImage(source: ImageSource.camera); - await ImagePicker.pickImage( - source: ImageSource.camera, - maxWidth: 10.0, - ); - await ImagePicker.pickImage( - source: ImageSource.camera, - maxHeight: 10.0, - ); - await ImagePicker.pickImage( - source: ImageSource.camera, - maxWidth: 10.0, - maxHeight: 20.0, - ); - await ImagePicker.pickImage( - source: ImageSource.camera, maxWidth: 10.0, imageQuality: 70); - await ImagePicker.pickImage( - source: ImageSource.camera, maxHeight: 10.0, imageQuality: 70); - await ImagePicker.pickImage( - source: ImageSource.camera, - maxWidth: 10.0, - maxHeight: 20.0, - imageQuality: 70); - - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': 70, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': 70, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': 70, - 'cameraDevice': 0 - }), - ], - ); - }); - - test('does not accept a negative width or height argument', () { - expect( - ImagePicker.pickImage(source: ImageSource.camera, maxWidth: -1.0), - throwsArgumentError, - ); - - expect( - ImagePicker.pickImage(source: ImageSource.camera, maxHeight: -1.0), - throwsArgumentError, - ); - }); - - test('handles a null image path response gracefully', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) => null); - - expect( - await ImagePicker.pickImage(source: ImageSource.gallery), isNull); - expect(await ImagePicker.pickImage(source: ImageSource.camera), isNull); - }); - - test('camera position defaults to back', () async { - await ImagePicker.pickImage(source: ImageSource.camera); - - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0, - }), - ], - ); - }); - - test('camera position can set to front', () async { - await ImagePicker.pickImage( - source: ImageSource.camera, - preferredCameraDevice: CameraDevice.front); - - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 1, - }), - ], - ); - }); - }); - - group('#pickVideo', () { - test('passes the image source argument correctly', () async { - await ImagePicker.pickVideo(source: ImageSource.camera); - await ImagePicker.pickVideo(source: ImageSource.gallery); - - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'cameraDevice': 0, - 'maxDuration': null, - }), - isMethodCall('pickVideo', arguments: { - 'source': 1, - 'cameraDevice': 0, - 'maxDuration': null, - }), - ], - ); - }); - - test('passes the duration argument correctly', () async { - await ImagePicker.pickVideo(source: ImageSource.camera); - await ImagePicker.pickVideo( - source: ImageSource.camera, - maxDuration: const Duration(seconds: 10)); - await ImagePicker.pickVideo( - source: ImageSource.camera, - maxDuration: const Duration(minutes: 1)); - await ImagePicker.pickVideo( - source: ImageSource.camera, maxDuration: const Duration(hours: 1)); - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': null, - 'cameraDevice': 0, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 10, - 'cameraDevice': 0, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 60, - 'cameraDevice': 0, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 3600, - 'cameraDevice': 0, - }), - ], - ); - }); - - test('handles a null video path response gracefully', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) => null); - - expect( - await ImagePicker.pickVideo(source: ImageSource.gallery), isNull); - expect(await ImagePicker.pickVideo(source: ImageSource.camera), isNull); - }); - - test('camera position defaults to back', () async { - await ImagePicker.pickVideo(source: ImageSource.camera); - - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'cameraDevice': 0, - 'maxDuration': null, - }), - ], - ); - }); - - test('camera position can set to front', () async { - await ImagePicker.pickVideo( - source: ImageSource.camera, - preferredCameraDevice: CameraDevice.front); - - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': null, - 'cameraDevice': 1, - }), - ], - ); - }); - }); - - group('#retrieveLostData', () { - test('retrieveLostData get success response', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'image', - 'path': '/example/path', - }; - }); - final LostDataResponse response = await ImagePicker.retrieveLostData(); - expect(response.type, RetrieveType.image); - expect(response.file.path, '/example/path'); - }); - - test('retrieveLostData get error response', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'video', - 'errorCode': 'test_error_code', - 'errorMessage': 'test_error_message', - }; - }); - final LostDataResponse response = await ImagePicker.retrieveLostData(); - expect(response.type, RetrieveType.video); - expect(response.exception.code, 'test_error_code'); - expect(response.exception.message, 'test_error_message'); - }); - - test('retrieveLostData get null response', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return null; - }); - expect((await ImagePicker.retrieveLostData()).isEmpty, true); - }); - - test('retrieveLostData get both path and error should throw', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'video', - 'errorCode': 'test_error_code', - 'errorMessage': 'test_error_message', - 'path': '/example/path', - }; - }); - expect(ImagePicker.retrieveLostData(), throwsAssertionError); - }); - }); - }); -} diff --git a/packages/image_picker/image_picker_for_web/AUTHORS b/packages/image_picker/image_picker_for_web/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md index 604314240a1e..7b2c4077e28d 100644 --- a/packages/image_picker/image_picker_for_web/CHANGELOG.md +++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md @@ -1,3 +1,12 @@ +# 2.0.0 + +* Migrate to null safety. +* Add doc comments to point out that some arguments aren't supported on the web. + +# 0.1.0+3 + +* Update Flutter SDK constraint. + # 0.1.0+2 * Adds Video MIME Types for the safari browser for acception diff --git a/packages/image_picker/image_picker_for_web/LICENSE b/packages/image_picker/image_picker_for_web/LICENSE index 507569823f1b..c6823b81eb84 100644 --- a/packages/image_picker/image_picker_for_web/LICENSE +++ b/packages/image_picker/image_picker_for_web/LICENSE @@ -1,4 +1,4 @@ -Copyright 2019 The Chromium Authors. All rights reserved. +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: diff --git a/packages/image_picker/image_picker_for_web/README.md b/packages/image_picker/image_picker_for_web/README.md index 81452e290984..8c9f2c73b8fe 100644 --- a/packages/image_picker/image_picker_for_web/README.md +++ b/packages/image_picker/image_picker_for_web/README.md @@ -2,10 +2,10 @@ A web implementation of [`image_picker`][1]. -## Browser Support +## Limitations on the web platform Since Web Browsers don't offer direct access to their users' file system, -this plugin provides a `PickedFile` abstraction to make access access uniform +this plugin provides a `PickedFile` abstraction to make access uniform across platforms. The web version of the plugin puts network-accessible URIs as the `path` @@ -42,6 +42,12 @@ In order to "take a photo", some mobile browsers offer a [`capture` attribute](h Each browser may implement `capture` any way they please, so it may (or may not) make a difference in your users' experience. +### pickImage() +The arguments `maxWidth`, `maxHeight` and `imageQuality` are not supported on the web. + +### pickVideo() +The argument `maxDuration` is not supported on the web. + ## Usage ### Import the package diff --git a/packages/image_picker/image_picker_for_web/ios/image_picker_for_web.podspec b/packages/image_picker/image_picker_for_web/ios/image_picker_for_web.podspec deleted file mode 100644 index 23fb795d1cc2..000000000000 --- a/packages/image_picker/image_picker_for_web/ios/image_picker_for_web.podspec +++ /dev/null @@ -1,20 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'image_picker_for_web' - s.version = '0.0.1' - s.summary = 'No-op implementation of image_picker_for_web plugin to avoid build issues on iOS' - s.description = <<-DESC -temp fake image_picker_for_web plugin - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_for_web' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.ios.deployment_target = '8.0' -end diff --git a/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart index e50b4aad3c8d..2fb66380e1d8 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 @@ -1,3 +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 'dart:async'; import 'dart:html' as html; @@ -13,14 +17,14 @@ final String _kAcceptVideoMimeType = 'video/3gpp,video/x-m4v,video/mp4,video/*'; /// /// This class implements the `package:image_picker` functionality for the web. class ImagePickerPlugin extends ImagePickerPlatform { - final ImagePickerPluginTestOverrides _overrides; + final ImagePickerPluginTestOverrides? _overrides; bool get _hasOverrides => _overrides != null; - html.Element _target; + late html.Element _target; /// A constructor that allows tests to override the function that creates file inputs. ImagePickerPlugin({ - @visibleForTesting ImagePickerPluginTestOverrides overrides, + @visibleForTesting ImagePickerPluginTestOverrides? overrides, }) : _overrides = overrides { _target = _ensureInitialized(_kImagePickerInputsDomId); } @@ -30,25 +34,49 @@ class ImagePickerPlugin extends ImagePickerPlatform { ImagePickerPlatform.instance = ImagePickerPlugin(); } + /// Returns a [PickedFile] with the image that was picked. + /// + /// The `source` argument controls where the image comes from. This can + /// be either [ImageSource.camera] or [ImageSource.gallery]. + /// + /// Note that the `maxWidth`, `maxHeight` and `imageQuality` arguments are not supported on the web. If any of these arguments is supplied, it'll be silently ignored by the web version of the plugin. + /// + /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. + /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. + /// Defaults to [CameraDevice.rear]. + /// + /// If no images were picked, the return value is null. @override Future pickImage({ - @required ImageSource source, - double maxWidth, - double maxHeight, - int imageQuality, + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { - String capture = computeCaptureAttribute(source, preferredCameraDevice); + String? capture = computeCaptureAttribute(source, preferredCameraDevice); return pickFile(accept: _kAcceptImageMimeType, capture: capture); } + /// Returns a [PickedFile] containing the video that was picked. + /// + /// The [source] argument controls where the video comes from. This can + /// be either [ImageSource.camera] or [ImageSource.gallery]. + /// + /// Note that the `maxDuration` argument is not supported on the web. If the argument is supplied, it'll be silently ignored by the web version of the plugin. + /// + /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. + /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. + /// Defaults to [CameraDevice.rear]. + /// + /// If no images were picked, the return value is null. @override Future pickVideo({ - @required ImageSource source, + required ImageSource source, CameraDevice preferredCameraDevice = CameraDevice.rear, - Duration maxDuration, + Duration? maxDuration, }) { - String capture = computeCaptureAttribute(source, preferredCameraDevice); + String? capture = computeCaptureAttribute(source, preferredCameraDevice); return pickFile(accept: _kAcceptVideoMimeType, capture: capture); } @@ -59,10 +87,11 @@ class ImagePickerPlugin extends ImagePickerPlatform { /// See https://caniuse.com/#feat=html-media-capture @visibleForTesting Future pickFile({ - String accept, - String capture, + String? accept, + String? capture, }) { - html.FileUploadInputElement input = createInputElement(accept, capture); + html.FileUploadInputElement input = + createInputElement(accept, capture) as html.FileUploadInputElement; _injectAndActivate(input); return _getSelectedFile(input); } @@ -73,25 +102,26 @@ class ImagePickerPlugin extends ImagePickerPlatform { /// /// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#capture @visibleForTesting - String computeCaptureAttribute(ImageSource source, CameraDevice device) { + String? computeCaptureAttribute(ImageSource source, CameraDevice device) { if (source == ImageSource.camera) { return (device == CameraDevice.front) ? 'user' : 'environment'; } return null; } - html.File _getFileFromInput(html.FileUploadInputElement input) { + html.File? _getFileFromInput(html.FileUploadInputElement input) { if (_hasOverrides) { - return _overrides.getFileFromInput(input); + return _overrides!.getFileFromInput(input); } - return input?.files?.first; + return input.files?.first; } /// Handles the OnChange event from a FileUploadInputElement object /// Returns the objectURL of the selected file. - String _handleOnChangeEvent(html.Event event) { - final html.FileUploadInputElement input = event?.target; - final html.File file = _getFileFromInput(input); + String? _handleOnChangeEvent(html.Event event) { + final html.FileUploadInputElement input = + event.target as html.FileUploadInputElement; + final html.File? file = _getFileFromInput(input); if (file != null) { return html.Url.createObjectUrl(file); @@ -105,7 +135,7 @@ class ImagePickerPlugin extends ImagePickerPlatform { // Observe the input until we can return something input.onChange.first.then((event) { final objectUrl = _handleOnChangeEvent(event); - if (!_completer.isCompleted) { + if (!_completer.isCompleted && objectUrl != null) { _completer.complete(PickedFile(objectUrl)); } }); @@ -127,7 +157,7 @@ class ImagePickerPlugin extends ImagePickerPlatform { final html.Element targetElement = html.Element.tag('flt-image-picker-inputs')..id = id; - html.querySelector('body').children.add(targetElement); + html.querySelector('body')!.children.add(targetElement); target = targetElement; } return target; @@ -136,9 +166,9 @@ class ImagePickerPlugin extends ImagePickerPlatform { /// Creates an input element that accepts certain file types, and /// allows to `capture` from the device's cameras (where supported) @visibleForTesting - html.Element createInputElement(String accept, String capture) { + html.Element createInputElement(String? accept, String? capture) { if (_hasOverrides) { - return _overrides.createInputElement(accept, capture); + return _overrides!.createInputElement(accept, capture); } html.Element element = html.FileUploadInputElement()..accept = accept; @@ -162,22 +192,22 @@ class ImagePickerPlugin extends ImagePickerPlatform { /// A function that creates a file input with the passed in `accept` and `capture` attributes. @visibleForTesting typedef OverrideCreateInputFunction = html.Element Function( - String accept, - String capture, + String? accept, + String? capture, ); /// A function that extracts a [html.File] from the file `input` passed in. @visibleForTesting typedef OverrideExtractFilesFromInputFunction = html.File Function( - html.Element input, + html.Element? input, ); /// Overrides for some of the functionality above. @visibleForTesting class ImagePickerPluginTestOverrides { /// Override the creation of the input element. - OverrideCreateInputFunction createInputElement; + late OverrideCreateInputFunction createInputElement; /// Override the extraction of the selected file from an input element. - OverrideExtractFilesFromInputFunction getFileFromInput; + late OverrideExtractFilesFromInputFunction getFileFromInput; } diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml index 32e89437415e..768f7e27ce77 100644 --- a/packages/image_picker/image_picker_for_web/pubspec.yaml +++ b/packages/image_picker/image_picker_for_web/pubspec.yaml @@ -1,10 +1,12 @@ name: image_picker_for_web description: Web platform implementation of image_picker -homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_for_web -# 0.1.y+z is compatible with 1.0.0, if you land a breaking change bump -# the version to 2.0.0. -# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.1.0+2 +repository: https://github.com/flutter/plugins/tree/master/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.0.0 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: @@ -14,19 +16,14 @@ flutter: fileName: image_picker_for_web.dart dependencies: - image_picker_platform_interface: ^1.1.0 flutter: sdk: flutter flutter_web_plugins: sdk: flutter - meta: ^1.1.7 - js: ^0.6.0 + image_picker_platform_interface: ^2.0.0 + meta: ^1.3.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 - -environment: - sdk: ">=2.5.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + pedantic: ^1.10.0 diff --git a/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart b/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart index 96d048dd2a8e..fbdd1d38bee6 100644 --- a/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart +++ b/packages/image_picker/image_picker_for_web/test/image_picker_for_web_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -13,12 +13,12 @@ import 'package:image_picker_for_web/image_picker_for_web.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; final String expectedStringContents = "Hello, world!"; -final Uint8List bytes = utf8.encode(expectedStringContents); +final Uint8List bytes = utf8.encode(expectedStringContents) as Uint8List; final html.File textFile = html.File([bytes], "hello.txt"); void main() { // Under test... - ImagePickerPlugin plugin; + late ImagePickerPlugin plugin; setUp(() { plugin = ImagePickerPlugin(); diff --git a/packages/image_picker/image_picker_platform_interface/AUTHORS b/packages/image_picker/image_picker_platform_interface/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md index e82e62028668..e2def7243592 100644 --- a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md +++ b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md @@ -1,3 +1,34 @@ +## 2.1.0 + +* Add `pickMultiImage` method. + +## 2.0.1 + +* Update platform_plugin_interface version requirement. + +## 2.0.0 + +* Migrate to null safety. +* Breaking Changes: + * Removed the deprecated methods: `ImagePickerPlatform.retrieveLostDataAsDartIoFile`,`ImagePickerPlatform.pickImagePath` and `ImagePickerPlatform.pickVideoPath`. + * Removed deprecated class: `LostDataResponse`. + +## 1.1.6 + +* Fix test asset file location. + +## 1.1.5 + +* Update Flutter SDK constraint. + +## 1.1.4 + +* Pass `Uri`s to `package:http` methods, instead of strings, in preparation for a major version update in `http`. + +## 1.1.3 + +* Update documentation of `pickImage()` regarding HEIC images. + ## 1.1.2 * Update documentation of `pickImage()` regarding compression support for specific image types. diff --git a/packages/image_picker/image_picker_platform_interface/LICENSE b/packages/image_picker/image_picker_platform_interface/LICENSE index a6d6c0749818..c6823b81eb84 100644 --- a/packages/image_picker/image_picker_platform_interface/LICENSE +++ b/packages/image_picker/image_picker_platform_interface/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017 The Chromium Authors. All rights reserved. +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: 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 6e7641324805..b384e3845c4b 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 @@ -1,2 +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. + export 'package:image_picker_platform_interface/src/platform_interface/image_picker_platform.dart'; export 'package:image_picker_platform_interface/src/types/types.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 71704b63ced4..e0f46457a8b8 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 @@ -1,13 +1,12 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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/services.dart'; -import 'package:meta/meta.dart' show required, visibleForTesting; +import 'package:meta/meta.dart' show visibleForTesting; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; @@ -20,14 +19,14 @@ class MethodChannelImagePicker extends ImagePickerPlatform { MethodChannel get channel => _channel; @override - Future pickImage({ - @required ImageSource source, - double maxWidth, - double maxHeight, - int imageQuality, + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) async { - String path = await pickImagePath( + String? path = await _pickImagePath( source: source, maxWidth: maxWidth, maxHeight: maxHeight, @@ -38,14 +37,60 @@ class MethodChannelImagePicker extends ImagePickerPlatform { } @override - Future pickImagePath({ - @required ImageSource source, - double maxWidth, - double maxHeight, - int imageQuality, + Future?> pickMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) async { + final List? paths = await _pickMultiImagePath( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + ); + if (paths == null) return null; + + final List files = []; + for (final path in paths) { + files.add(PickedFile(path)); + } + return files; + } + + Future?> _pickMultiImagePath({ + 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 _pickImagePath({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { - assert(source != null); if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { throw ArgumentError.value( imageQuality, 'imageQuality', 'must be between 0 and 100'); @@ -72,12 +117,12 @@ class MethodChannelImagePicker extends ImagePickerPlatform { } @override - Future pickVideo({ - @required ImageSource source, + Future pickVideo({ + required ImageSource source, CameraDevice preferredCameraDevice = CameraDevice.rear, - Duration maxDuration, + Duration? maxDuration, }) async { - String path = await pickVideoPath( + final String? path = await _pickVideoPath( source: source, maxDuration: maxDuration, preferredCameraDevice: preferredCameraDevice, @@ -85,13 +130,11 @@ class MethodChannelImagePicker extends ImagePickerPlatform { return path != null ? PickedFile(path) : null; } - @override - Future pickVideoPath({ - @required ImageSource source, + Future _pickVideoPath({ + required ImageSource source, CameraDevice preferredCameraDevice = CameraDevice.rear, - Duration maxDuration, + Duration? maxDuration, }) { - assert(source != null); return _channel.invokeMethod( 'pickVideo', { @@ -104,7 +147,7 @@ class MethodChannelImagePicker extends ImagePickerPlatform { @override Future retrieveLostData() async { - final Map result = + final Map? result = await _channel.invokeMapMethod('retrieve'); if (result == null) { @@ -113,23 +156,23 @@ class MethodChannelImagePicker extends ImagePickerPlatform { assert(result.containsKey('path') ^ result.containsKey('errorCode')); - final String type = result['type']; + final String? type = result['type']; assert(type == kTypeImage || type == kTypeVideo); - RetrieveType retrieveType; + RetrieveType? retrieveType; if (type == kTypeImage) { retrieveType = RetrieveType.image; } else if (type == kTypeVideo) { retrieveType = RetrieveType.video; } - PlatformException exception; + PlatformException? exception; if (result.containsKey('errorCode')) { exception = PlatformException( code: result['errorCode'], message: result['errorMessage']); } - final String path = result['path']; + final String? path = result['path']; return LostData( file: path != null ? PickedFile(path) : null, @@ -137,40 +180,4 @@ class MethodChannelImagePicker extends ImagePickerPlatform { type: retrieveType, ); } - - @override - // ignore: deprecated_member_use_from_same_package - Future retrieveLostDataAsDartIoFile() async { - final Map result = - await _channel.invokeMapMethod('retrieve'); - if (result == null) { - // ignore: deprecated_member_use_from_same_package - return LostDataResponse.empty(); - } - assert(result.containsKey('path') ^ result.containsKey('errorCode')); - - final String type = result['type']; - assert(type == kTypeImage || type == kTypeVideo); - - RetrieveType retrieveType; - if (type == kTypeImage) { - retrieveType = RetrieveType.image; - } else if (type == kTypeVideo) { - retrieveType = RetrieveType.video; - } - - PlatformException exception; - if (result.containsKey('errorCode')) { - exception = PlatformException( - code: result['errorCode'], message: result['errorMessage']); - } - - final String path = result['path']; - - // ignore: deprecated_member_use_from_same_package - return LostDataResponse( - file: path == null ? null : File(path), - exception: exception, - type: retrieveType); - } } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart b/packages/image_picker/image_picker_platform_interface/lib/src/platform_interface/image_picker_platform.dart index f33c80bc4995..32af7747185a 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 @@ -1,10 +1,9 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; -import 'package:meta/meta.dart' show required; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'package:image_picker_platform_interface/src/method_channel/method_channel_image_picker.dart'; @@ -39,11 +38,16 @@ abstract class ImagePickerPlatform extends PlatformInterface { _instance = instance; } - /// Returns a [String] containing a path to the image that was picked. + // Next version of the API. + + /// Returns a [PickedFile] with the image that was picked. /// /// The `source` argument controls where the image comes from. This can /// be either [ImageSource.camera] or [ImageSource.gallery]. /// + /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and above only support HEIC images if used + /// in addition to a size modification, of which the usage is explained below. + /// /// If specified, the image will be at most `maxWidth` wide and /// `maxHeight` tall. Otherwise the image will be returned at it's /// original width and height. @@ -51,101 +55,53 @@ abstract class ImagePickerPlatform extends PlatformInterface { /// The `imageQuality` argument modifies the quality of the image, ranging from 0-100 /// where 100 is the original/max quality. If `imageQuality` is null, the image with /// the original quality will be returned. Compression is only supported for certain - /// image types such as JPEG and on Android PNG and WebP, too. If compression is not supported for the image that is picked, + /// image types such as JPEG. If compression is not supported for the image that is picked, /// a warning message will be logged. /// /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. - /// Defaults to [CameraDevice.rear]. + /// Defaults to [CameraDevice.rear]. Note that Android has no documented parameter for an intent to specify if + /// the front or rear camera should be opened, this function is not guaranteed + /// to work on an Android device. /// /// In Android, the MainActivity can be destroyed for various reasons. If that happens, the result will be lost - /// in this call. You can then call [retrieveLostDataAsDartIoFile] when your app relaunches to retrieve the lost data. - @Deprecated('Use pickImage instead.') - Future pickImagePath({ - @required ImageSource source, - double maxWidth, - double maxHeight, - int imageQuality, - CameraDevice preferredCameraDevice = CameraDevice.rear, - }) { - throw UnimplementedError('legacyPickImage() has not been implemented.'); - } - - /// Returns a [String] containing a path to the video that was picked. - /// - /// The [source] argument controls where the video comes from. This can - /// be either [ImageSource.camera] or [ImageSource.gallery]. - /// - /// The [maxDuration] argument specifies the maximum duration of the captured video. If no [maxDuration] is specified, - /// the maximum duration will be infinite. - /// - /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. - /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. - /// Defaults to [CameraDevice.rear]. + /// in this call. You can then call [retrieveLostData] when your app relaunches to retrieve the lost data. /// - /// In Android, the MainActivity can be destroyed for various fo reasons. If that happens, the result will be lost - /// in this call. You can then call [retrieveLostDataAsDartIoFile] when your app relaunches to retrieve the lost data. - @Deprecated('Use pickVideo instead.') - Future pickVideoPath({ - @required ImageSource source, + /// If no images were picked, the return value is null. + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, - Duration maxDuration, }) { - throw UnimplementedError('pickVideoPath() has not been implemented.'); + throw UnimplementedError('pickImage() has not been implemented.'); } - /// Retrieve the lost image file when [pickImagePath] or [pickVideoPath] failed because the MainActivity is destroyed. (Android only) + /// Returns a [List] with the images that were picked. /// - /// Image or video can be lost if the MainActivity is destroyed. And there is no guarantee that the MainActivity is always alive. - /// Call this method to retrieve the lost data and process the data according to your APP's business logic. + /// The images come from the [ImageSource.gallery]. /// - /// Returns a [LostDataResponse] if successfully retrieved the lost data. The [LostDataResponse] can represent either a - /// successful image/video selection, or a failure. - /// - /// Calling this on a non-Android platform will throw [UnimplementedError] exception. - /// - /// See also: - /// * [LostDataResponse], for what's included in the response. - /// * [Android Activity Lifecycle](https://developer.android.com/reference/android/app/Activity.html), for more information on MainActivity destruction. - @Deprecated('Use retrieveLostData instead.') - Future retrieveLostDataAsDartIoFile() { - throw UnimplementedError( - 'retrieveLostDataAsDartIoFile() has not been implemented.'); - } - - // Next version of the API. - - /// Returns a [PickedFile] with the image that was picked. - /// - /// The `source` argument controls where the image comes from. This can - /// be either [ImageSource.camera] or [ImageSource.gallery]. + /// Where iOS supports HEIC images, Android 8 and below doesn't. Android 9 and above only support HEIC images if used + /// in addition to a size modification, of which the usage is explained below. /// /// If specified, the image will be at most `maxWidth` wide and /// `maxHeight` tall. Otherwise the image will be returned at it's /// original width and height. /// - /// The `imageQuality` argument modifies the quality of the image, ranging from 0-100 - /// where 100 is the original/max quality. If `imageQuality` is null, the image with - /// the original quality will be returned. Compression is only supportted for certain + /// The `imageQuality` argument modifies the quality of the images, ranging from 0-100 + /// where 100 is the original/max quality. If `imageQuality` is null, the images with + /// the original quality will be returned. Compression is only supported for certain /// image types such as JPEG. If compression is not supported for the image that is picked, - /// an warning message will be logged. - /// - /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. - /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. - /// Defaults to [CameraDevice.rear]. Note that Android has no documented parameter for an intent to specify if - /// the front or rear camera should be opened, this function is not guaranteed - /// to work on an Android device. + /// a warning message will be logged. /// - /// In Android, the MainActivity can be destroyed for various reasons. If that happens, the result will be lost - /// in this call. You can then call [retrieveLostData] when your app relaunches to retrieve the lost data. - Future pickImage({ - @required ImageSource source, - double maxWidth, - double maxHeight, - int imageQuality, - CameraDevice preferredCameraDevice = CameraDevice.rear, + /// If no images were picked, the return value is null. + Future?> pickMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, }) { - throw UnimplementedError('pickImage() has not been implemented.'); + throw UnimplementedError('pickMultiImage() has not been implemented.'); } /// Returns a [PickedFile] containing the video that was picked. @@ -162,10 +118,12 @@ abstract class ImagePickerPlatform extends PlatformInterface { /// /// In Android, the MainActivity can be destroyed for various fo reasons. If that happens, the result will be lost /// in this call. You can then call [retrieveLostData] when your app relaunches to retrieve the lost data. - Future pickVideo({ - @required ImageSource source, + /// + /// If no images were picked, the return value is null. + Future pickVideo({ + required ImageSource source, CameraDevice preferredCameraDevice = CameraDevice.rear, - Duration maxDuration, + Duration? maxDuration, }) { throw UnimplementedError('pickVideo() has not been implemented.'); } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/camera_device.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/camera_device.dart index 6c70fd451a0e..45dfe3ac96aa 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/camera_device.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/camera_device.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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_platform_interface/lib/src/types/image_source.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/image_source.dart index 37981e3038f1..ed907dc54c48 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/image_source.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/image_source.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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_platform_interface/lib/src/types/lost_data_response.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart deleted file mode 100644 index d82618b23cd1..000000000000 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io'; - -import 'package:flutter/services.dart'; -import 'package:image_picker_platform_interface/src/types/types.dart'; - -/// The response object of [ImagePicker.retrieveLostData]. -/// -/// Only applies to Android. -/// See also: -/// * [ImagePicker.retrieveLostData] for more details on retrieving lost data. -@Deprecated('Use methods that return a LostData object instead.') -class LostDataResponse { - /// Creates an instance with the given [file], [exception], and [type]. Any of - /// the params may be null, but this is never considered to be empty. - LostDataResponse({this.file, this.exception, this.type}); - - /// Initializes an instance with all member params set to null and considered - /// to be empty. - LostDataResponse.empty() - : file = null, - exception = null, - type = null, - _empty = true; - - /// Whether it is an empty response. - /// - /// An empty response should have [file], [exception] and [type] to be null. - bool get isEmpty => _empty; - - /// The file that was lost in a previous [pickImage] or [pickVideo] call due to MainActivity being destroyed. - /// - /// Can be null if [exception] exists. - final File file; - - /// The exception of the last [pickImage] or [pickVideo]. - /// - /// If the last [pickImage] or [pickVideo] threw some exception before the MainActivity destruction, this variable keeps that - /// exception. - /// You should handle this exception as if the [pickImage] or [pickVideo] got an exception when the MainActivity was not destroyed. - /// - /// Note that it is not the exception that caused the destruction of the MainActivity. - final PlatformException exception; - - /// Can either be [RetrieveType.image] or [RetrieveType.video]; - final RetrieveType type; - - bool _empty = false; -} diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart index 285294efcb3d..de259e0611dd 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 @@ -1,3 +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 'dart:convert'; import 'dart:typed_data'; @@ -52,7 +56,7 @@ abstract class PickedFileBase { /// If `end` is present, only up to byte-index `end` will be read. Otherwise, until end of file. /// /// In order to make sure that system resources are freed, the stream must be read to completion or the subscription on the stream must be cancelled. - Stream openRead([int start, int end]) { + Stream openRead([int? start, int? end]) { throw UnimplementedError('openRead() has not been implemented.'); } } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.dart index 0faf531f3f75..24e1931008b6 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 @@ -1,3 +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 'dart:convert'; import 'dart:typed_data'; @@ -10,21 +14,21 @@ import './base.dart'; /// It wraps the bytes of a selected file. class PickedFile extends PickedFileBase { final String path; - final Uint8List _initBytes; + final Uint8List? _initBytes; /// Construct a PickedFile object from its ObjectUrl. /// /// Optionally, this can be initialized with `bytes` /// so no http requests are performed to retrieve files later. - PickedFile(this.path, {Uint8List bytes}) + PickedFile(this.path, {Uint8List? bytes}) : _initBytes = bytes, super(path); Future get _bytes async { if (_initBytes != null) { - return Future.value(UnmodifiableUint8ListView(_initBytes)); + return Future.value(UnmodifiableUint8ListView(_initBytes!)); } - return http.readBytes(path); + return http.readBytes(Uri.parse(path)); } @override @@ -38,7 +42,7 @@ class PickedFile extends PickedFileBase { } @override - Stream openRead([int start, int end]) async* { + Stream openRead([int? start, int? end]) async* { final bytes = await _bytes; yield bytes.sublist(start ?? 0, end ?? bytes.length); } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.dart index dd64558bf044..7037b6b7121a 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 @@ -1,3 +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 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; @@ -29,7 +33,7 @@ class PickedFile extends PickedFileBase { } @override - Stream openRead([int start, int end]) { + Stream openRead([int? start, int? end]) { return _file .openRead(start ?? 0, end) .map((chunk) => Uint8List.fromList(chunk)); diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/lost_data.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/lost_data.dart index b94e69de219e..64f6a1f27538 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/lost_data.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/lost_data.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -31,7 +31,7 @@ class LostData { /// The file that was lost in a previous [pickImage] or [pickVideo] call due to MainActivity being destroyed. /// /// Can be null if [exception] exists. - final PickedFile file; + final PickedFile? file; /// The exception of the last [pickImage] or [pickVideo]. /// @@ -40,10 +40,12 @@ class LostData { /// You should handle this exception as if the [pickImage] or [pickVideo] got an exception when the MainActivity was not destroyed. /// /// Note that it is not the exception that caused the destruction of the MainActivity. - final PlatformException exception; + final PlatformException? exception; /// Can either be [RetrieveType.image] or [RetrieveType.video]; - final RetrieveType type; + /// + /// If the lost data is empty, this will be null. + final RetrieveType? type; bool _empty = false; } diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/picked_file.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/picked_file.dart index b2a614ccb304..c8c9e5a0ac79 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/picked_file.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/picked_file.dart @@ -1,3 +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 'lost_data.dart'; export 'unsupported.dart' if (dart.library.html) 'html.dart' diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/unsupported.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/unsupported.dart index bc10a4890c8d..ad3ed6a4f86a 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/unsupported.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/unsupported.dart @@ -1,3 +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 './base.dart'; /// A PickedFile is a cross-platform, simplified File abstraction. diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/types/retrieve_type.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/retrieve_type.dart index cc32be9711c2..445445e5d7fb 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/retrieve_type.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/retrieve_type.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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_platform_interface/lib/src/types/types.dart b/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart index 9c44fae1aa9d..10e7745f2741 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/types.dart @@ -1,6 +1,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 'camera_device.dart'; export 'image_source.dart'; -export 'lost_data_response.dart'; export 'retrieve_type.dart'; export 'picked_file/picked_file.dart'; diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml index 16300488368b..8e176a09a626 100644 --- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml +++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml @@ -1,23 +1,23 @@ name: image_picker_platform_interface description: A common platform interface for the image_picker plugin. -homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_platform_interface +repository: https://github.com/flutter/plugins/tree/master/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: 1.1.2 +version: 2.1.0 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" dependencies: flutter: sdk: flutter - meta: ^1.1.8 - http: ^0.12.1 - plugin_platform_interface: ^1.0.2 + http: ^0.13.0 + meta: ^1.3.0 + plugin_platform_interface: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - mockito: ^4.1.1 - pedantic: ^1.8.0+1 - -environment: - sdk: ">=2.5.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + pedantic: ^1.10.0 diff --git a/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart b/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart deleted file mode 100644 index ddaad3d32f41..000000000000 --- a/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart +++ /dev/null @@ -1,345 +0,0 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; -import 'package:image_picker_platform_interface/src/method_channel/method_channel_image_picker.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - group('$MethodChannelImagePicker', () { - MethodChannelImagePicker picker = MethodChannelImagePicker(); - - final List log = []; - - setUp(() { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - return ''; - }); - - log.clear(); - }); - - group('#pickImagePath', () { - test('passes the image source argument correctly', () async { - await picker.pickImagePath(source: ImageSource.camera); - await picker.pickImagePath(source: ImageSource.gallery); - - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 1, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - ], - ); - }); - - test('passes the width and height arguments correctly', () async { - await picker.pickImagePath(source: ImageSource.camera); - await picker.pickImagePath( - source: ImageSource.camera, - maxWidth: 10.0, - ); - await picker.pickImagePath( - source: ImageSource.camera, - maxHeight: 10.0, - ); - await picker.pickImagePath( - source: ImageSource.camera, - maxWidth: 10.0, - maxHeight: 20.0, - ); - await picker.pickImagePath( - source: ImageSource.camera, maxWidth: 10.0, imageQuality: 70); - await picker.pickImagePath( - source: ImageSource.camera, maxHeight: 10.0, imageQuality: 70); - await picker.pickImagePath( - source: ImageSource.camera, - maxWidth: 10.0, - maxHeight: 20.0, - imageQuality: 70); - - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': null, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': null, - 'imageQuality': 70, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': 10.0, - 'imageQuality': 70, - 'cameraDevice': 0 - }), - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': 10.0, - 'maxHeight': 20.0, - 'imageQuality': 70, - 'cameraDevice': 0 - }), - ], - ); - }); - - test('does not accept a negative width or height argument', () { - expect( - () => - picker.pickImagePath(source: ImageSource.camera, maxWidth: -1.0), - throwsArgumentError, - ); - - expect( - () => - picker.pickImagePath(source: ImageSource.camera, maxHeight: -1.0), - throwsArgumentError, - ); - }); - - test('handles a null image path response gracefully', () async { - picker.channel - .setMockMethodCallHandler((MethodCall methodCall) => null); - - expect(await picker.pickImagePath(source: ImageSource.gallery), isNull); - expect(await picker.pickImagePath(source: ImageSource.camera), isNull); - }); - - test('camera position defaults to back', () async { - await picker.pickImagePath(source: ImageSource.camera); - - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 0, - }), - ], - ); - }); - - test('camera position can set to front', () async { - await picker.pickImagePath( - source: ImageSource.camera, - preferredCameraDevice: CameraDevice.front); - - expect( - log, - [ - isMethodCall('pickImage', arguments: { - 'source': 0, - 'maxWidth': null, - 'maxHeight': null, - 'imageQuality': null, - 'cameraDevice': 1, - }), - ], - ); - }); - }); - - group('#pickVideoPath', () { - test('passes the image source argument correctly', () async { - await picker.pickVideoPath(source: ImageSource.camera); - await picker.pickVideoPath(source: ImageSource.gallery); - - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'cameraDevice': 0, - 'maxDuration': null, - }), - isMethodCall('pickVideo', arguments: { - 'source': 1, - 'cameraDevice': 0, - 'maxDuration': null, - }), - ], - ); - }); - - test('passes the duration argument correctly', () async { - await picker.pickVideoPath(source: ImageSource.camera); - await picker.pickVideoPath( - source: ImageSource.camera, - maxDuration: const Duration(seconds: 10)); - await picker.pickVideoPath( - source: ImageSource.camera, - maxDuration: const Duration(minutes: 1)); - await picker.pickVideoPath( - source: ImageSource.camera, maxDuration: const Duration(hours: 1)); - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': null, - 'cameraDevice': 0, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 10, - 'cameraDevice': 0, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 60, - 'cameraDevice': 0, - }), - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': 3600, - 'cameraDevice': 0, - }), - ], - ); - }); - - test('handles a null video path response gracefully', () async { - picker.channel - .setMockMethodCallHandler((MethodCall methodCall) => null); - - expect(await picker.pickVideoPath(source: ImageSource.gallery), isNull); - expect(await picker.pickVideoPath(source: ImageSource.camera), isNull); - }); - - test('camera position defaults to back', () async { - await picker.pickVideoPath(source: ImageSource.camera); - - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'cameraDevice': 0, - 'maxDuration': null, - }), - ], - ); - }); - - test('camera position can set to front', () async { - await picker.pickVideoPath( - source: ImageSource.camera, - preferredCameraDevice: CameraDevice.front); - - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'maxDuration': null, - 'cameraDevice': 1, - }), - ], - ); - }); - }); - - group('#retrieveLostDataAsDartIoFile', () { - test('retrieveLostData get success response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'image', - 'path': '/example/path', - }; - }); - // ignore: deprecated_member_use_from_same_package - final LostDataResponse response = - await picker.retrieveLostDataAsDartIoFile(); - expect(response.type, RetrieveType.image); - expect(response.file.path, '/example/path'); - }); - - test('retrieveLostData get error response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'video', - 'errorCode': 'test_error_code', - 'errorMessage': 'test_error_message', - }; - }); - // ignore: deprecated_member_use_from_same_package - final LostDataResponse response = - await picker.retrieveLostDataAsDartIoFile(); - expect(response.type, RetrieveType.video); - expect(response.exception.code, 'test_error_code'); - expect(response.exception.message, 'test_error_message'); - }); - - test('retrieveLostData get null response', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { - return null; - }); - expect((await picker.retrieveLostDataAsDartIoFile()).isEmpty, true); - }); - - test('retrieveLostData get both path and error should throw', () async { - picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'video', - 'errorCode': 'test_error_code', - 'errorMessage': 'test_error_message', - 'path': '/example/path', - }; - }); - expect(picker.retrieveLostDataAsDartIoFile(), throwsAssertionError); - }); - }, skip: isBrowser); - }); -} diff --git a/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart b/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart index e7abe37e4838..83ae6fac9071 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -15,11 +15,13 @@ void main() { MethodChannelImagePicker picker = MethodChannelImagePicker(); final List log = []; + dynamic returnValue = ''; setUp(() { + returnValue = ''; picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { log.add(methodCall); - return ''; + return returnValue; }); log.clear(); @@ -139,6 +141,29 @@ void main() { ); }); + 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), @@ -196,6 +221,127 @@ void main() { }); }); + 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 a 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('#pickVideoPath', () { test('passes the image source argument correctly', () async { await picker.pickVideo(source: ImageSource.camera); @@ -312,7 +458,8 @@ void main() { // ignore: deprecated_member_use_from_same_package final LostData response = await picker.retrieveLostData(); expect(response.type, RetrieveType.image); - expect(response.file.path, '/example/path'); + expect(response.file, isNotNull); + expect(response.file!.path, '/example/path'); }); test('retrieveLostData get error response', () async { @@ -326,8 +473,9 @@ void main() { // ignore: deprecated_member_use_from_same_package final LostData response = await picker.retrieveLostData(); expect(response.type, RetrieveType.video); - expect(response.exception.code, 'test_error_code'); - expect(response.exception.message, 'test_error_message'); + expect(response.exception, isNotNull); + expect(response.exception!.code, 'test_error_code'); + expect(response.exception!.message, 'test_error_message'); }); test('retrieveLostData get null response', () async { diff --git a/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart b/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.dart index 49d84ff88f88..7721f66148e0 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -6,13 +6,12 @@ import 'dart:convert'; import 'dart:html' as html; -import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; final String expectedStringContents = 'Hello, world!'; -final Uint8List bytes = utf8.encode(expectedStringContents); +final List bytes = utf8.encode(expectedStringContents); final html.File textFile = html.File([bytes], 'hello.txt'); final String textFileUrl = html.Url.createObjectUrl(textFile); diff --git a/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart b/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.dart index 94ff759a2fb2..d366204c36bf 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -11,14 +11,17 @@ import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +final pathPrefix = + Directory.current.path.endsWith('test') ? './assets/' : './test/assets/'; +final path = pathPrefix + 'hello.txt'; final String expectedStringContents = 'Hello, world!'; -final Uint8List bytes = utf8.encode(expectedStringContents); -final File textFile = File('./assets/hello.txt'); +final Uint8List bytes = Uint8List.fromList(utf8.encode(expectedStringContents)); +final File textFile = File(path); final String textFilePath = textFile.path; void main() { group('Create with an objectUrl', () { - final pickedFile = PickedFile(textFilePath); + final PickedFile pickedFile = PickedFile(textFilePath); test('Can be read as a string', () async { expect(await pickedFile.readAsString(), equals(expectedStringContents)); diff --git a/packages/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/CHANGELOG.md deleted file mode 100644 index a071e1153c6e..000000000000 --- a/packages/in_app_purchase/CHANGELOG.md +++ /dev/null @@ -1,336 +0,0 @@ - -## 0.3.4+10 - -* Fixed typo 'verity' for 'verify'. - -## 0.3.4+9 - -* [iOS] Fixed: purchase dialog not showing always. -* [iOS] Fixed: completing purchases could fail. -* [iOS] Fixed: restorePurchases caused hang (call never returned). - -## 0.3.4+8 - -* [iOS] Fixed: purchase dialog not showing always. -* [iOS] Fixed: completing purchases could fail. -* [iOS] Fixed: restorePurchases caused hang (call never returned). - -## 0.3.4+7 - -* iOS: Fix typo of the `simulatesAskToBuyInSandbox` key. - -## 0.3.4+6 - -* iOS: Fix the bug that prevent restored subscription transactions from being completed - -## 0.3.4+5 - -* Added necessary README docs for getting started with Android. - -## 0.3.4+4 - -* Update package:e2e -> package:integration_test - -## 0.3.4+3 - -* Fixed typo 'manuelly' for 'manually'. - -## 0.3.4+2 - -* Update package:e2e reference to use the local version in the flutter/plugins - repository. - -## 0.3.4+1 - -* iOS: Fix the bug that `SKPaymentQueueWrapper.transactions` doesn't return all transactions. -* iOS: Fix the app crashes if `InAppPurchaseConnection.instance` is called in the `main()`. - -## 0.3.4 - -* Expose SKError code to client apps. - -## 0.3.3+2 - -* Post-v2 Android embedding cleanups. - -## 0.3.3+1 - -* Update documentations for `InAppPurchase.completePurchase` and update README. - -## 0.3.3 - -* Introduce `SKPaymentQueueWrapper.transactions`. - -## 0.3.2+2 - -* Fix CocoaPods podspec lint warnings. - -## 0.3.2+1 - -* iOS: Fix only transactions with SKPaymentTransactionStatePurchased and SKPaymentTransactionStateFailed can be finished. -* iOS: Only one pending transaction of a given product is allowed. - -## 0.3.2 - -* Remove Android dependencies fallback. -* Require Flutter SDK 1.12.13+hotfix.5 or greater. - -## 0.3.1+2 - -* Fix potential casting crash on Android v1 embedding when registering life cycle callbacks. -* Remove hard-coded legacy xcode build setting. - -## 0.3.1+1 - -* Add `pedantic` to dev_dependency. - -## 0.3.1 - -* Android: Fix a bug where the `BillingClient` is disconnected when app goes to the background. -* Android: Make sure the `BillingClient` object is disconnected before the activity is destroyed. -* Android: Fix minor compiler warning. -* Fix typo in CHANGELOG. - -## 0.3.0+3 - -* Fix pendingCompletePurchase flag status to allow to complete purchases. - -## 0.3.0+2 - -* Update te example app to avoid using deprecated api. - -## 0.3.0+1 - -* Fixing usage example. No functional changes. - -## 0.3.0 - -* Migrate the `Google Play Library` to 2.0.3. - * Introduce a new class `BillingResultWrapper` which contains a detailed result of a BillingClient operation. - * **[Breaking Change]:** All the BillingClient methods that previously return a `BillingResponse` now return a `BillingResultWrapper`, including: `launchBillingFlow`, `startConnection` and `consumeAsync`. - * **[Breaking Change]:** The `SkuDetailsResponseWrapper` now contains a `billingResult` field in place of `billingResponse` field. - * A `billingResult` field is added to the `PurchasesResultWrapper`. - * Other Updates to the "billing_client_wrappers": - * Updates to the `PurchaseWrapper`: Add `developerPayload`, `purchaseState` and `isAcknowledged` fields. - * Updates to the `SkuDetailsWrapper`: Add `originalPrice` and `originalPriceAmountMicros` fields. - * **[Breaking Change]:** The `BillingClient.queryPurchaseHistory` is updated to return a `PurchasesHistoryResult`, which contains a list of `PurchaseHistoryRecordWrapper` instead of `PurchaseWrapper`. A `PurchaseHistoryRecordWrapper` object has the same fields and values as A `PurchaseWrapper` object, except that a `PurchaseHistoryRecordWrapper` object does not contain `isAutoRenewing`, `orderId` and `packageName`. - * Add a new `BillingClient.acknowledgePurchase` API. Starting from this version, the developer has to acknowledge any purchase on Android using this API within 3 days of purchase, or the user will be refunded. Note that if a product is "consumed" via `BillingClient.consumeAsync`, it is implicitly acknowledged. - * **[Breaking Change]:** Added `enablePendingPurchases` in `BillingClientWrapper`. The application has to call this method before calling `BillingClientWrapper.startConnection`. See [enablePendingPurchases](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html#enablependingpurchases) for more information. - * Updates to the "InAppPurchaseConnection": - * **[Breaking Change]:** `InAppPurchaseConnection.completePurchase` now returns a `Future` instead of `Future`. A new optional parameter `{String developerPayload}` has also been added to the API. On Android, this API does not throw an exception anymore, it instead acknowledge the purchase. If a purchase is not completed within 3 days on Android, the user will be refunded. - * **[Breaking Change]:** `InAppPurchaseConnection.consumePurchase` now returns a `Future` instead of `Future`. A new optional parameter `{String developerPayload}` has also been added to the API. - * A new boolean field `pendingCompletePurchase` has been added to the `PurchaseDetails` class. Which can be used as an indicator of whether to call `InAppPurchaseConnection.completePurchase` on the purchase. - * **[Breaking Change]:** Added `enablePendingPurchases` in `InAppPurchaseConnection`. The application has to call this method when initializing the `InAppPurchaseConnection` on Android. See [enablePendingPurchases](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html#enablependingpurchases) for more information. - * Misc: Some documentation updates reflecting the `BillingClient` migration and some documentation fixes. - * Refer to [Google Play Billing Library Release Note](https://developer.android.com/google/play/billing/billing_library_releases_notes#release-2_0) for a detailed information on the update. - -## 0.2.2+6 - -* Correct a comment. - -## 0.2.2+5 - -* Update version of json_annotation to ^3.0.0 and json_serializable to ^3.2.0. Resolve conflicts with other packages e.g. flutter_tools from sdk. - -## 0.2.2+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.2.2+3 - -* Fix failing pedantic lints. None of these fixes should have any change in - functionality. - -## 0.2.2+2 - -* Include lifecycle dependency as a compileOnly one on Android to resolve - potential version conflicts with other transitive libraries. - -## 0.2.2+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.2.2 - -* Support the v2 Android embedder. -* Update to AndroidX. -* Migrate to using the new e2e test binding. -* Add a e2e test. - -## 0.2.1+5 - -* Define clang module for iOS. -* Fix iOS build warning. - -## 0.2.1+4 - -* Update and migrate iOS example project. - -## 0.2.1+3 - -* Android : Improved testability. - -## 0.2.1+2 - -* Android: Require a non-null Activity to use the `launchBillingFlow` method. - -## 0.2.1+1 - -* Remove skipped driver test. - -## 0.2.1 - -* iOS: Add currencyCode to priceLocale on productDetails. - -## 0.2.0+8 - -* Add dependency on `androidx.annotation:annotation:1.0.0`. - -## 0.2.0+7 - -* Make Gradle version compatible with the Android Gradle plugin version. - -## 0.2.0+6 - -* Add missing `hashCode` implementations. - -## 0.2.0+5 - -* iOS: Support unsupported UserInfo value types on NSError. - -## 0.2.0+4 - -* Fixed code error in `README.md` and adjusted links to work on Pub. - -## 0.2.0+3 - -* Update the `README.md` so that the code samples compile with the latest Flutter/Dart version. - -## 0.2.0+2 - -* Fix a google_play_connection purchase update listener regression introduced in 0.2.0+1. - -## 0.2.0+1 - -* Fix an issue the type is not casted before passing to `PurchasesResultWrapper.fromJson`. - -## 0.2.0 - -* [Breaking Change] Rename 'PurchaseError' to 'IAPError'. -* [Breaking Change] Rename 'PurchaseSource' to 'IAPSource'. - -## 0.1.1+3 - -* Expanded description in `pubspec.yaml` and fixed typo in `README.md`. - -## 0.1.1+2 - -* Add missing template type parameter to `invokeMethod` calls. -* Bump minimum Flutter version to 1.5.0. -* Replace invokeMethod with invokeMapMethod wherever necessary. - -## 0.1.1+1 - -* Make `AdditionalSteps`(Used in the unit test) a void function. - -## 0.1.1 - -* Some error messages from iOS are slightly changed. -* `ProductDetailsResponse` returned by `queryProductDetails()` now contains an `PurchaseError` object that represents any error that might occurred during the request. -* If the device is not connected to the internet, `queryPastPurchases()` on iOS now have the error stored in the response instead of throwing. -* Clean up minor iOS warning. -* Example app shows how to handle error when calling `queryProductDetails()` and `queryProductDetails()`. - -## 0.1.0+4 - -* Change the `buy` methods to return `Future` instead of `void` in order - to propagate `launchBillingFlow` failures up through `google_play_connection`. - -## 0.1.0+3 - -* Guard against multiple onSetupFinished() calls. - -## 0.1.0+2 - -* Fix bug where error only purchases updates weren't propagated correctly in - `google_play_connection.dart`. - -## 0.1.0+1 - -* Add more consumable handling to the example app. - -## 0.1.0 - -Beta release. - -* Ability to list products, load previous purchases, and make purchases. -* Simplified Dart API that's been unified for ease of use. -* Platform specific APIs more directly exposing `StoreKit` and `BillingClient`. - -Includes: - -* 5ba657dc [in_app_purchase] Remove extraneous download logic (#1560) -* 01bb8796 [in_app_purchase] Minor doc updates (#1555) -* 1a4d493f [in_app_purchase] Only fetch owned purchases (#1540) -* d63c51cf [in_app_purchase] Add auto-consume errors to PurchaseDetails (#1537) -* 959da97f [in_app_purchase] Minor doc updates (#1536) -* b82ae1a6 [in_app_purchase] Rename the unified API (#1517) -* d1ad723a [in_app_purchase]remove SKDownloadWrapper and related code. (#1474) -* 7c1e8b8a [in_app_purchase]make payment unified APIs (#1421) -* 80233db6 [in_app_purchase] Add references to the original object for PurchaseDetails and ProductDetails (#1448) -* 8c180f0d [in_app_purchase]load purchase (#1380) -* e9f141bc [in_app_purchase] Iap refactor (#1381) -* d3b3d60c add driver test command to cirrus (#1342) -* aee12523 [in_app_purchase] refactoring and tests (#1322) -* 6d7b4592 [in_app_purchase] Adds Dart BillingClient APIs for loading purchases (#1286) -* 5567a9c8 [in_app_purchase]retrieve receipt (#1303) -* 3475f1b7 [in_app_purchase]restore purchases (#1299) -* a533148d [in_app_purchase] payment queue dart ios (#1249) -* 10030840 [in_app_purchase] Minor bugfixes and code cleanup (#1284) -* 347f508d [in_app_purchase] Fix CI formatting errors. (#1281) -* fad02d87 [in_app_purchase] Java API for querying purchases (#1259) -* bc501915 [In_app_purchase]SKProduct related fixes (#1252) -* f92ba3a1 IAP make payment objc (#1231) -* 62b82522 [IAP] Add the Dart API for launchBillingFlow (#1232) -* b40a4acf [IAP] Add Java call for launchBillingFlow (#1230) -* 4ff06cd1 [In_app_purchase]remove categories (#1222) -* 0e72ca56 [In_app_purchase]fix requesthandler crash (#1199) -* 81dff2be Iap getproductlist basic draft (#1169) -* db139b28 Iap iOS add payment dart wrappers (#1178) -* 2e5fbb9b Fix the param map passed down to the platform channel when calling querySkuDetails (#1194) -* 4a84bac1 Mark some packages as unpublishable (#1193) -* 51696552 Add a gradle warning to the AndroidX plugins (#1138) -* 832ab832 Iap add payment objc translators (#1172) -* d0e615cf Revert "IAP add payment translators in objc (#1126)" (#1171) -* 09a5a36e IAP add payment translators in objc (#1126) -* a100fbf9 Expose nslocale and expose currencySymbol instead of currencyCode to match android (#1162) -* 1c982efd Using json serializer for skproduct wrapper and related classes (#1147) -* 3039a261 Iap productlist ios (#1068) -* 2a1593da [IAP] Update dev deps to match flutter_driver (#1118) -* 9f87cbe5 [IAP] Update README (#1112) -* 59e84d85 Migrate independent plugins to AndroidX (#1103) -* a027ccd6 [IAP] Generate boilerplate serializers (#1090) -* 909cf1c2 [IAP] Fetch SkuDetails from Google Play (#1084) -* 6bbaa7e5 [IAP] Add missing license headers (#1083) -* 5347e877 [IAP] Clean up Dart unit tests (#1082) -* fe03e407 [IAP] Check if the payment processor is available (#1057) -* 43ee28cf Fix `Manifest versionCode not found` (#1076) -* 4d702ad7 Supress `strong_mode_implicit_dynamic_method` for `invokeMethod` calls. (#1065) -* 809ccde7 Doc and build script updates to the IAP plugin (#1024) -* 052b71a9 Update the IAP README (#933) -* 54f9c4e2 Upgrade Android Gradle Plugin to 3.2.1 (#916) -* ced3e99d Set all gradle-wrapper versions to 4.10.2 (#915) -* eaa1388b Reconfigure Cirrus to use clang 7 (#905) -* 9b153920 Update gradle dependencies. (#881) -* 1aef7d92 Enable lint unnecessary_new (#701) - -## 0.0.2 - -* Added missing flutter_test package dependency. -* Added missing flutter version requirements. - -## 0.0.1 - -* Initial release. diff --git a/packages/in_app_purchase/LICENSE b/packages/in_app_purchase/LICENSE deleted file mode 100644 index ad33cf3c3ed1..000000000000 --- a/packages/in_app_purchase/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright 2018 The Chromium Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/in_app_purchase/README.md b/packages/in_app_purchase/README.md deleted file mode 100644 index 021811842b2f..000000000000 --- a/packages/in_app_purchase/README.md +++ /dev/null @@ -1,189 +0,0 @@ -# In App Purchase - -A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases -through the App Store (on iOS) and Google Play (on Android). - -## Features - -Add this to your Flutter app to: - -1. Show in app products that are available for sale from the underlying shop. - Includes consumables, permanent upgrades, and subscriptions. -2. Load in app products currently owned by the user according to the underlying - shop. -3. Send your user to the underlying store to purchase your products. - -## Getting Started - -This plugin is in beta. Please use with caution and file any potential issues -you see on our [issue tracker](https://github.com/flutter/flutter/issues/new/choose). - -This plugin relies on the App Store and Google Play for making in app purchases. -It exposes a unified surface, but you'll still need to understand and configure -your app with each store to handle purchases using them. Both have extensive -guides: - -* [In-App Purchase (App Store)](https://developer.apple.com/in-app-purchase/) -* [Google Play Biling Overview](https://developer.android.com/google/play/billing/billing_overview) - -You can check out the [example app README](https://github.com/flutter/plugins/blob/master/packages/in_app_purchase/example/README.md) for steps on how -to configure in app purchases in both stores. - -Once you've configured your in app purchases in their respective stores, you're -able to start using the plugin. There's two basic options available to you to -use. - -1. [in_app_purchase.dart](https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/lib/src/in_app_purchase), - the generic idiommatic Flutter API. This exposes the most basic IAP-related - functionality. The goal is that Flutter apps should be able to use this API - surface on its own for the vast majority of cases. If you use this you should - be able to handle most use cases for loading and making purchases. If you would - like a more platform dependent approach, we also provide the second option as - below. - -2. Dart APIs exposing the underlying platform APIs as directly as possible: - [store_kit_wrappers.dart](https://github.com/flutter/plugins/blob/master/packages/in_app_purchase/lib/src/store_kit_wrappers) and - [billing_client_wrappers.dart](https://github.com/flutter/plugins/blob/master/packages/in_app_purchase/lib/src/billing_client_wrappers). These - API surfaces should expose all the platform-specific behavior and allow for - more fine-tuned control when needed. However if you use this you'll need to - code your purchase handling logic significantly differently depending on - which platform you're on. - -### Initializing the plugin - -```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. - // - // On iOS this is a no-op. - InAppPurchaseConnection.enablePendingPurchases(); - - runApp(MyApp()); -} -``` - -```dart -// Subscribe to any incoming purchases at app initialization. These can -// propagate from either storefront so it's important to listen as soon as -// possible to avoid losing events. -class _MyAppState extends State { - StreamSubscription> _subscription; - - @override - void initState() { - final Stream purchaseUpdates = - InAppPurchaseConnection.instance.purchaseUpdatedStream; - _subscription = purchaseUpdates.listen((purchases) { - _handlePurchaseUpdates(purchases); - }); - super.initState(); - } - - @override - void dispose() { - _subscription.cancel(); - super.dispose(); - } -``` - -### Connecting to the Storefront - -```dart -final bool available = await InAppPurchaseConnection.instance.isAvailable(); -if (!available) { - // The store cannot be reached or accessed. Update the UI accordingly. -} -``` - -### Loading products for sale - -```dart -// Set literals require Dart 2.2. Alternatively, use `Set _kIds = ['product1', 'product2'].toSet()`. -const Set _kIds = {'product1', 'product2'}; -final ProductDetailsResponse response = await InAppPurchaseConnection.instance.queryProductDetails(_kIds); -if (response.notFoundIDs.isNotEmpty) { - // Handle the error. -} -List products = response.productDetails; -``` - -### Loading previous purchases - -```dart -final QueryPurchaseDetailsResponse response = await InAppPurchaseConnection.instance.queryPastPurchases(); -if (response.error != null) { - // Handle the error. -} -for (PurchaseDetails purchase in response.pastPurchases) { - _verifyPurchase(purchase); // Verify the purchase following the best practices for each storefront. - _deliverPurchase(purchase); // Deliver the purchase to the user in your app. - if (Platform.isIOS) { - // Mark that you've delivered the purchase. Only the App Store requires - // this final confirmation. - InAppPurchaseConnection.instance.completePurchase(purchase); - } -} -``` - -Note that the App Store does not have any APIs for querying consumable -products, and Google Play considers consumable products to no longer be owned -once they're marked as consumed and fails to return them here. For restoring -these across devices you'll need to persist them on your own server and query -that as well. - -### Listening to purchase updates - -You should always start listening to purchase update as early as possible to be able -to catch all purchase updates, including the ones from the previous app session. -To listen to the update: - -```dart - Stream purchaseUpdated = - InAppPurchaseConnection.instance.purchaseUpdatedStream; - _subscription = purchaseUpdated.listen((purchaseDetailsList) { - _listenToPurchaseUpdated(purchaseDetailsList); - }, onDone: () { - _subscription.cancel(); - }, onError: (error) { - // handle error here. - }); -``` - -### Making a purchase - -Both storefronts handle consumable and non-consumable products differently. If -you're using `InAppPurchaseConnection`, you need to make a distinction here and -call the right purchase method for each type. - -```dart -final ProductDetails productDetails = ... // Saved earlier from queryPastPurchases(). -final PurchaseParam purchaseParam = PurchaseParam(productDetails: productDetails); -if (_isConsumable(productDetails)) { - InAppPurchaseConnection.instance.buyConsumable(purchaseParam: purchaseParam); -} else { - InAppPurchaseConnection.instance.buyNonConsumable(purchaseParam: purchaseParam); -} -// From here the purchase flow will be handled by the underlying storefront. -// Updates will be delivered to the `InAppPurchaseConnection.instance.purchaseUpdatedStream`. -``` - -### Complete a purchase - -The `InAppPurchaseConnection.purchaseUpdatedStream` will send purchase updates after -you initiate the purchase flow using `InAppPurchaseConnection.buyConsumable` or `InAppPurchaseConnection.buyNonConsumable`. -After delivering the content to the user, you need to call `InAppPurchaseConnection.completePurchase` to tell the `GooglePlay` -and `AppStore` that the purchase has been finished. - -WARNING! Failure to call `InAppPurchaseConnection.completePurchase` and get a successful response within 3 days of the purchase will result a refund. - -## Development - -This plugin uses -[json_serializable](https://pub.dartlang.org/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. diff --git a/packages/in_app_purchase/analysis_options.yaml b/packages/in_app_purchase/analysis_options.yaml deleted file mode 100644 index 8e4af76f0a30..000000000000 --- a/packages/in_app_purchase/analysis_options.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# This is a temporary file 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. - -include: ../../analysis_options.yaml - -analyzer: - errors: - public_member_api_docs: ignore diff --git a/packages/in_app_purchase/android/build.gradle b/packages/in_app_purchase/android/build.gradle deleted file mode 100644 index 96163c0d20bd..000000000000 --- a/packages/in_app_purchase/android/build.gradle +++ /dev/null @@ -1,43 +0,0 @@ -group 'io.flutter.plugins.inapppurchase' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' - } -} - -rootProject.allprojects { - repositories { - google() - jcenter() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 28 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - } -} - -dependencies { - implementation 'androidx.annotation:annotation:1.0.0' - implementation 'com.android.billingclient:billing:2.0.3' - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:2.17.0' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' -} diff --git a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java b/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java deleted file mode 100644 index 335d4b8e12cf..000000000000 --- a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java +++ /dev/null @@ -1,349 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.inapppurchase; - -import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList; -import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesResult; -import static io.flutter.plugins.inapppurchase.Translator.fromSkuDetailsList; - -import android.app.Activity; -import android.app.Application; -import android.content.Context; -import android.os.Bundle; -import android.util.Log; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import com.android.billingclient.api.AcknowledgePurchaseParams; -import com.android.billingclient.api.AcknowledgePurchaseResponseListener; -import com.android.billingclient.api.BillingClient; -import com.android.billingclient.api.BillingClientStateListener; -import com.android.billingclient.api.BillingFlowParams; -import com.android.billingclient.api.BillingResult; -import com.android.billingclient.api.ConsumeParams; -import com.android.billingclient.api.ConsumeResponseListener; -import com.android.billingclient.api.PurchaseHistoryRecord; -import com.android.billingclient.api.PurchaseHistoryResponseListener; -import com.android.billingclient.api.SkuDetails; -import com.android.billingclient.api.SkuDetailsParams; -import com.android.billingclient.api.SkuDetailsResponseListener; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** Handles method channel for the plugin. */ -class MethodCallHandlerImpl - implements MethodChannel.MethodCallHandler, Application.ActivityLifecycleCallbacks { - - private static final String TAG = "InAppPurchasePlugin"; - - @Nullable private BillingClient billingClient; - private final BillingClientFactory billingClientFactory; - - @Nullable private Activity activity; - private final Context applicationContext; - private final MethodChannel methodChannel; - - private HashMap cachedSkus = new HashMap<>(); - - /** Constructs the MethodCallHandlerImpl */ - MethodCallHandlerImpl( - @Nullable Activity activity, - @NonNull Context applicationContext, - @NonNull MethodChannel methodChannel, - @NonNull BillingClientFactory billingClientFactory) { - this.billingClientFactory = billingClientFactory; - this.applicationContext = applicationContext; - this.activity = activity; - this.methodChannel = methodChannel; - } - - /** - * Sets the activity. Should be called as soon as the the activity is available. When the activity - * becomes unavailable, call this method again with {@code null}. - */ - void setActivity(@Nullable Activity activity) { - this.activity = activity; - } - - @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 (this.activity == activity && this.applicationContext != null) { - ((Application) this.applicationContext).unregisterActivityLifecycleCallbacks(this); - endBillingClientConnection(); - } - } - - @Override - public void onActivityStopped(Activity activity) {} - - void onDetachedFromActivity() { - endBillingClientConnection(); - } - - @Override - public void onMethodCall(MethodCall call, MethodChannel.Result result) { - switch (call.method) { - case InAppPurchasePlugin.MethodNames.IS_READY: - isReady(result); - break; - case InAppPurchasePlugin.MethodNames.START_CONNECTION: - startConnection( - (int) call.argument("handle"), - (boolean) call.argument("enablePendingPurchases"), - result); - break; - case InAppPurchasePlugin.MethodNames.END_CONNECTION: - endConnection(result); - break; - case InAppPurchasePlugin.MethodNames.QUERY_SKU_DETAILS: - querySkuDetailsAsync( - (String) call.argument("skuType"), (List) call.argument("skusList"), result); - break; - case InAppPurchasePlugin.MethodNames.LAUNCH_BILLING_FLOW: - launchBillingFlow( - (String) call.argument("sku"), (String) call.argument("accountId"), result); - break; - case InAppPurchasePlugin.MethodNames.QUERY_PURCHASES: - queryPurchases((String) call.argument("skuType"), result); - break; - case InAppPurchasePlugin.MethodNames.QUERY_PURCHASE_HISTORY_ASYNC: - queryPurchaseHistoryAsync((String) call.argument("skuType"), result); - break; - case InAppPurchasePlugin.MethodNames.CONSUME_PURCHASE_ASYNC: - consumeAsync( - (String) call.argument("purchaseToken"), - (String) call.argument("developerPayload"), - result); - break; - case InAppPurchasePlugin.MethodNames.ACKNOWLEDGE_PURCHASE: - acknowledgePurchase( - (String) call.argument("purchaseToken"), - (String) call.argument("developerPayload"), - result); - break; - default: - result.notImplemented(); - } - } - - private void endConnection(final MethodChannel.Result result) { - endBillingClientConnection(); - result.success(null); - } - - private void endBillingClientConnection() { - if (billingClient != null) { - billingClient.endConnection(); - billingClient = null; - } - } - - private void isReady(MethodChannel.Result result) { - if (billingClientError(result)) { - return; - } - - result.success(billingClient.isReady()); - } - - private void querySkuDetailsAsync( - final String skuType, final List skusList, final MethodChannel.Result result) { - if (billingClientError(result)) { - return; - } - - SkuDetailsParams params = - SkuDetailsParams.newBuilder().setType(skuType).setSkusList(skusList).build(); - billingClient.querySkuDetailsAsync( - params, - new SkuDetailsResponseListener() { - @Override - public void onSkuDetailsResponse( - BillingResult billingResult, List skuDetailsList) { - updateCachedSkus(skuDetailsList); - final Map skuDetailsResponse = new HashMap<>(); - skuDetailsResponse.put("billingResult", Translator.fromBillingResult(billingResult)); - skuDetailsResponse.put("skuDetailsList", fromSkuDetailsList(skuDetailsList)); - result.success(skuDetailsResponse); - } - }); - } - - private void launchBillingFlow( - String sku, @Nullable String accountId, MethodChannel.Result result) { - if (billingClientError(result)) { - return; - } - - SkuDetails skuDetails = cachedSkus.get(sku); - if (skuDetails == null) { - result.error( - "NOT_FOUND", - "Details for sku " + sku + " are not available. Has this ID already been fetched?", - null); - return; - } - - if (activity == null) { - result.error( - "ACTIVITY_UNAVAILABLE", - "Details for sku " - + sku - + " are not available. This method must be run with the app in foreground.", - null); - return; - } - - BillingFlowParams.Builder paramsBuilder = - BillingFlowParams.newBuilder().setSkuDetails(skuDetails); - if (accountId != null && !accountId.isEmpty()) { - paramsBuilder.setAccountId(accountId); - } - result.success( - Translator.fromBillingResult( - billingClient.launchBillingFlow(activity, paramsBuilder.build()))); - } - - private void consumeAsync( - String purchaseToken, String developerPayload, final MethodChannel.Result result) { - if (billingClientError(result)) { - return; - } - - ConsumeResponseListener listener = - new ConsumeResponseListener() { - @Override - public void onConsumeResponse(BillingResult billingResult, String outToken) { - result.success(Translator.fromBillingResult(billingResult)); - } - }; - ConsumeParams.Builder paramsBuilder = - ConsumeParams.newBuilder().setPurchaseToken(purchaseToken); - - if (developerPayload != null) { - paramsBuilder.setDeveloperPayload(developerPayload); - } - ConsumeParams params = paramsBuilder.build(); - - billingClient.consumeAsync(params, listener); - } - - private void queryPurchases(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))); - } - - private void queryPurchaseHistoryAsync(String skuType, final MethodChannel.Result result) { - if (billingClientError(result)) { - return; - } - - billingClient.queryPurchaseHistoryAsync( - skuType, - new PurchaseHistoryResponseListener() { - @Override - public void onPurchaseHistoryResponse( - BillingResult billingResult, List purchasesList) { - final Map serialized = new HashMap<>(); - serialized.put("billingResult", Translator.fromBillingResult(billingResult)); - serialized.put( - "purchaseHistoryRecordList", fromPurchaseHistoryRecordList(purchasesList)); - result.success(serialized); - } - }); - } - - private void startConnection( - final int handle, final boolean enablePendingPurchases, final MethodChannel.Result result) { - if (billingClient == null) { - billingClient = - billingClientFactory.createBillingClient( - applicationContext, methodChannel, enablePendingPurchases); - } - - billingClient.startConnection( - new BillingClientStateListener() { - private boolean alreadyFinished = false; - - @Override - public void onBillingSetupFinished(BillingResult billingResult) { - if (alreadyFinished) { - Log.d(TAG, "Tried to call onBilllingSetupFinished multiple times."); - return; - } - alreadyFinished = true; - // Consider the fact that we've finished a success, leave it to the Dart side to validate the responseCode. - result.success(Translator.fromBillingResult(billingResult)); - } - - @Override - public void onBillingServiceDisconnected() { - final Map arguments = new HashMap<>(); - arguments.put("handle", handle); - methodChannel.invokeMethod(InAppPurchasePlugin.MethodNames.ON_DISCONNECT, arguments); - } - }); - } - - private void acknowledgePurchase( - String purchaseToken, @Nullable String developerPayload, final MethodChannel.Result result) { - if (billingClientError(result)) { - return; - } - AcknowledgePurchaseParams params = - AcknowledgePurchaseParams.newBuilder() - .setDeveloperPayload(developerPayload) - .setPurchaseToken(purchaseToken) - .build(); - billingClient.acknowledgePurchase( - params, - new AcknowledgePurchaseResponseListener() { - @Override - public void onAcknowledgePurchaseResponse(BillingResult billingResult) { - result.success(Translator.fromBillingResult(billingResult)); - } - }); - } - - private void updateCachedSkus(@Nullable List skuDetailsList) { - if (skuDetailsList == null) { - return; - } - - for (SkuDetails skuDetails : skuDetailsList) { - cachedSkus.put(skuDetails.getSku(), skuDetails); - } - } - - private boolean billingClientError(MethodChannel.Result result) { - if (billingClient != null) { - return false; - } - - result.error("UNAVAILABLE", "BillingClient is unset. Try reconnecting.", null); - return true; - } -} diff --git a/packages/in_app_purchase/build.yaml b/packages/in_app_purchase/build.yaml deleted file mode 100644 index d7b59734f27e..000000000000 --- a/packages/in_app_purchase/build.yaml +++ /dev/null @@ -1,8 +0,0 @@ -targets: - $default: - builders: - json_serializable: - options: - any_map: true - create_to_json: true - nullable: false \ No newline at end of file diff --git a/packages/in_app_purchase/example/README.md b/packages/in_app_purchase/example/README.md deleted file mode 100644 index 9fcad23d19ae..000000000000 --- a/packages/in_app_purchase/example/README.md +++ /dev/null @@ -1,88 +0,0 @@ -# In App Purchase Example - -Demonstrates how to use the In App Purchase (IAP) Plugin. - -## Getting Started - -This plugin is in beta. Please use with caution and file any potential issues -you see on our [issue tracker](https://github.com/flutter/flutter/issues/new/choose). - -There's a significant amount of setup required for testing in app purchases -successfully, including registering new app IDs and store entries to use for -testing in both the Play Developer Console and App Store Connect. Both Google -Play and the App Store require developers to configure an app with in-app items -for purchase to call their in-app-purchase APIs. Both stores have extensive -documentation on how to do this, and we've also included a high level guide -below. - -* [In-App Purchase (App Store)](https://developer.apple.com/in-app-purchase/) -* [Google Play Biling Overview](https://developer.android.com/google/play/billing/billing_overview) - -### Android - -1. Create a new app in the [Play Developer - Console](https://play.google.com/apps/publish/) (PDC). - -2. Sign up for a merchant's account in the PDC. - -3. Create IAPs in the PDC available for purchase in the app. The example assumes - the following SKU IDs exist: - - - `consumable`: A managed product. - - `upgrade`: A managed product. - - `subscription`: A subscription. - - Make sure that all of the products are set to `ACTIVE`. - -4. Update `APP_ID` in `example/android/app/build.gradle` to match your package - ID in the PDC. - -5. Create an `example/android/keystore.properties` file with all your signing - information. `keystore.example.properties` exists as an example to follow. - It's impossible to use any of the `BillingClient` APIs from an unsigned APK. - See - [here](https://developer.android.com/studio/publish/app-signing#secure-shared-keystore) - and [here](https://developer.android.com/studio/publish/app-signing#sign-apk) - for more information. - -6. Build a signed apk. `flutter build apk` will work for this, the gradle files - in this project have been configured to sign even debug builds. - -7. Upload the signed APK from step 6 to the PDC, and publish that to the alpha - test channel. Add your test account as an approved tester. The - `BillingClient` APIs won't work unless the app has been fully published to - the alpha channel and is being used by an authorized test account. See - [here](https://support.google.com/googleplay/android-developer/answer/3131213) - for more info. - -8. Sign in to the test device with the test account from step #7. Then use - `flutter run` to install the app to the device and test like normal. - -### iOS - -1. Follow ["Workflow for configuring in-app - purchases"](https://help.apple.com/app-store-connect/#/devb57be10e7), a - detailed guide on all the steps needed to enable IAPs for an app. Complete - steps 1 ("Sign a Paid Applications Agreement") and 2 ("Configure in-app - purchases"). - - For step #2, "Configure in-app purchases in App Store Connect," you'll want - to create the following products: - - - A consumable with product ID `consumable` - - An upgrade with product ID `upgrade` - - An auto-renewing subscription with product ID `subscription` - -2. In XCode, `File > Open File` `example/ios/Runner.xcworkspace`. Update the - Bundle ID to match the Bundle ID of the app created in step #1. - -3. [Create a Sandbox tester - account](https://help.apple.com/app-store-connect/#/dev8b997bee1) to test the - in-app purchases with. - -4. Use `flutter run` to install the app and test it. Note that you need to test - it on a real device instead of a simulator, and signing into any production - service (including iTunes!) with the test account will permanently invalidate - it. Sign in to the test account in the example app following the steps in the - [*In-App Purchase Programming - Guide*](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Chapters/ShowUI.html#//apple_ref/doc/uid/TP40008267-CH3-SW11). \ No newline at end of file diff --git a/packages/in_app_purchase/example/android/app/build.gradle b/packages/in_app_purchase/example/android/app/build.gradle deleted file mode 100644 index a383eb4a965b..000000000000 --- a/packages/in_app_purchase/example/android/app/build.gradle +++ /dev/null @@ -1,115 +0,0 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -// Load the build signing secrets from a local `keystore.properties` file. -// TODO(YOU): Create release keys and a `keystore.properties` file. See -// `example/README.md` for more info and `keystore.example.properties` for an -// example. -def keystorePropertiesFile = rootProject.file("keystore.properties") -def keystoreProperties = new Properties() -def configured = true -try { - keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) -} catch (IOException e) { - configured = false - logger.error('Release signing information not found.') -} - -project.ext { - // TODO(YOU): Create release keys and a `keystore.properties` file. See - // `example/README.md` for more info and `keystore.example.properties` for an - // example. - APP_ID = configured ? keystoreProperties['appId'] : "io.flutter.plugins.inapppurchaseexample.DEFAULT_DO_NOT_USE" - KEYSTORE_STORE_FILE = configured ? rootProject.file(keystoreProperties['storeFile']) : null - KEYSTORE_STORE_PASSWORD = keystoreProperties['storePassword'] - KEYSTORE_KEY_ALIAS = keystoreProperties['keyAlias'] - KEYSTORE_KEY_PASSWORD = keystoreProperties['keyPassword'] - VERSION_CODE = configured ? keystoreProperties['versionCode'].toInteger() : 1 - VERSION_NAME = configured ? keystoreProperties['versionName'] : "0.0.1" -} - -if (project.APP_ID == "io.flutter.plugins.inapppurchaseexample.DEFAULT_DO_NOT_USE") { - configured = false - logger.error('Unique package name not set, defaulting to "io.flutter.plugins.inapppurchaseexample.DEFAULT_DO_NOT_USE".') -} - -// Log a final error message if we're unable to create a release key signed -// build for an app configured in the Play Developer Console. Apks built in this -// condition won't be able to call any of the BillingClient APIs. -if (!configured) { - logger.error('The app could not be configured for release signing. In app purchases will not be testable. See `example/README.md` for more info and instructions.') -} - -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.") -} - -apply plugin: 'com.android.application' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -android { - signingConfigs { - release { - storeFile project.KEYSTORE_STORE_FILE - storePassword project.KEYSTORE_STORE_PASSWORD - keyAlias project.KEYSTORE_KEY_ALIAS - keyPassword project.KEYSTORE_KEY_PASSWORD - } - } - - compileSdkVersion 28 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - applicationId project.APP_ID - minSdkVersion 16 - targetSdkVersion 28 - versionCode project.VERSION_CODE - versionName project.VERSION_NAME - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - // Google Play Billing APIs only work with apps signed for production. - debug { - if (configured) { - signingConfig signingConfigs.release - } else { - signingConfig signingConfigs.debug - } - } - release { - if (configured) { - signingConfig signingConfigs.release - } else { - signingConfig signingConfigs.debug - } - } - } - - testOptions { - unitTests.returnDefaultValues = true - } -} - -flutter { - source '../..' -} - -dependencies { - implementation 'com.android.billingclient:billing:1.2' - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:2.17.0' - testImplementation 'org.json:json:20180813' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' -} diff --git a/packages/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/EmbeddingV1Activity.java b/packages/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/EmbeddingV1Activity.java deleted file mode 100644 index f6849a867b0a..000000000000 --- a/packages/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/EmbeddingV1Activity.java +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.inapppurchaseexample; - -import android.os.Bundle; -import dev.flutter.plugins.integration_test.IntegrationTestPlugin; -import io.flutter.plugins.inapppurchase.InAppPurchasePlugin; -import io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin; - -@SuppressWarnings("deprecation") -public class EmbeddingV1Activity extends io.flutter.app.FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - IntegrationTestPlugin.registerWith( - registrarFor("dev.flutter.plugins.integration_test.IntegrationTestPlugin")); - SharedPreferencesPlugin.registerWith( - registrarFor("io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin")); - InAppPurchasePlugin.registerWith( - registrarFor("io.flutter.plugins.inapppurchase.InAppPurchasePlugin")); - } -} diff --git a/packages/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/EmbeddingV1ActivityTest.java b/packages/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/EmbeddingV1ActivityTest.java deleted file mode 100644 index 4b5a14472b4f..000000000000 --- a/packages/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.inapppurchaseexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -@SuppressWarnings("deprecation") -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java b/packages/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java deleted file mode 100644 index 15ec0da9958e..000000000000 --- a/packages/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.inapppurchaseexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import io.flutter.embedding.android.FlutterActivity; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -public class FlutterActivityTest { - @Rule - public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); -} diff --git a/packages/in_app_purchase/example/android/build.gradle b/packages/in_app_purchase/example/android/build.gradle deleted file mode 100644 index 541636cc492a..000000000000 --- a/packages/in_app_purchase/example/android/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' - } -} - -allprojects { - repositories { - google() - jcenter() - } -} - -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/in_app_purchase/example/in_app_purchase_example.iml b/packages/in_app_purchase/example/in_app_purchase_example.iml deleted file mode 100644 index e5c837191e06..000000000000 --- a/packages/in_app_purchase/example/in_app_purchase_example.iml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/in_app_purchase/example/in_app_purchase_example_android.iml b/packages/in_app_purchase/example/in_app_purchase_example_android.iml deleted file mode 100644 index b050030a1b87..000000000000 --- a/packages/in_app_purchase/example/in_app_purchase_example_android.iml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/packages/in_app_purchase/example/ios/Flutter/Debug.xcconfig b/packages/in_app_purchase/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index e8efba114687..000000000000 --- a/packages/in_app_purchase/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/in_app_purchase/example/ios/Flutter/Release.xcconfig b/packages/in_app_purchase/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index 399e9340e6f6..000000000000 --- a/packages/in_app_purchase/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/in_app_purchase/example/ios/Runner.xcodeproj/project.pbxproj b/packages/in_app_purchase/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 65c38e4c31b4..000000000000 --- a/packages/in_app_purchase/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,651 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 688DE35121F2A5A100EA2684 /* TranslatorTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 688DE35021F2A5A100EA2684 /* TranslatorTest.m */; }; - 6896B34621E9363700D37AEF /* ProductRequestHandlerTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 6896B34521E9363700D37AEF /* ProductRequestHandlerTest.m */; }; - 6896B34C21EEB4B800D37AEF /* Stubs.m in Sources */ = {isa = PBXBuildFile; fileRef = 6896B34B21EEB4B800D37AEF /* Stubs.m */; }; - 861D0D93B0757D95C8A69620 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B2AB6BE1D4E2232AB5D4A002 /* libPods-Runner.a */; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - A5279298219369C600FF69E6 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5279297219369C600FF69E6 /* StoreKit.framework */; }; - A59001A721E69658004A3E5E /* InAppPurchasePluginTest.m in Sources */ = {isa = PBXBuildFile; fileRef = A59001A621E69658004A3E5E /* InAppPurchasePluginTest.m */; }; - F78AF3142342BC89008449C7 /* PaymentQueueTest.m in Sources */ = {isa = PBXBuildFile; fileRef = F78AF3132342BC89008449C7 /* PaymentQueueTest.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 = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; - 688DE35021F2A5A100EA2684 /* TranslatorTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = TranslatorTest.m; path = ../../../ios/Tests/TranslatorTest.m; sourceTree = ""; }; - 6896B34521E9363700D37AEF /* ProductRequestHandlerTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = ProductRequestHandlerTest.m; path = ../../../ios/Tests/ProductRequestHandlerTest.m; sourceTree = ""; }; - 6896B34A21EEB4B800D37AEF /* Stubs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = Stubs.h; path = ../../../ios/Tests/Stubs.h; sourceTree = ""; }; - 6896B34B21EEB4B800D37AEF /* Stubs.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = Stubs.m; path = ../../../ios/Tests/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 = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A5279297219369C600FF69E6 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; - A59001A421E69658004A3E5E /* in_app_purchase_pluginTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = in_app_purchase_pluginTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - A59001A621E69658004A3E5E /* InAppPurchasePluginTest.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = InAppPurchasePluginTest.m; path = ../../../ios/Tests/InAppPurchasePluginTest.m; sourceTree = ""; }; - A59001A821E69658004A3E5E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B2AB6BE1D4E2232AB5D4A002 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - BE95F46E12942F78BF67E55B /* 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 = ""; }; - DE7EEEE26E27ACC04BA9951D /* 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 = ""; }; - F78AF3132342BC89008449C7 /* PaymentQueueTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = PaymentQueueTest.m; path = ../../../ios/Tests/PaymentQueueTest.m; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - 861D0D93B0757D95C8A69620 /* libPods-Runner.a in Frameworks */, - A5279298219369C600FF69E6 /* StoreKit.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A59001A121E69658004A3E5E /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 2D4BBB2E0E7B18550E80D50C /* Pods */ = { - isa = PBXGroup; - children = ( - DE7EEEE26E27ACC04BA9951D /* Pods-Runner.debug.xcconfig */, - BE95F46E12942F78BF67E55B /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B80C3931E831B6300D905FE /* App.framework */, - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - A59001A521E69658004A3E5E /* in_app_purchase_pluginTests */, - 97C146EF1CF9000F007C117D /* Products */, - 2D4BBB2E0E7B18550E80D50C /* Pods */, - E4DB99639FAD8ADED6B572FC /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - A59001A421E69658004A3E5E /* in_app_purchase_pluginTests.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 = ""; - }; - A59001A521E69658004A3E5E /* in_app_purchase_pluginTests */ = { - isa = PBXGroup; - children = ( - A59001A621E69658004A3E5E /* InAppPurchasePluginTest.m */, - 6896B34521E9363700D37AEF /* ProductRequestHandlerTest.m */, - F78AF3132342BC89008449C7 /* PaymentQueueTest.m */, - A59001A821E69658004A3E5E /* Info.plist */, - 6896B34A21EEB4B800D37AEF /* Stubs.h */, - 6896B34B21EEB4B800D37AEF /* Stubs.m */, - 688DE35021F2A5A100EA2684 /* TranslatorTest.m */, - ); - path = in_app_purchase_pluginTests; - sourceTree = ""; - }; - E4DB99639FAD8ADED6B572FC /* Frameworks */ = { - isa = PBXGroup; - children = ( - A5279297219369C600FF69E6 /* StoreKit.framework */, - B2AB6BE1D4E2232AB5D4A002 /* 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 = ( - 5DF63B80D489A62B306EA07A /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - AC81012709A36415AE0CF8C4 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; - A59001A321E69658004A3E5E /* in_app_purchase_pluginTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = A59001AD21E69658004A3E5E /* Build configuration list for PBXNativeTarget "in_app_purchase_pluginTests" */; - buildPhases = ( - A59001A021E69658004A3E5E /* Sources */, - A59001A121E69658004A3E5E /* Frameworks */, - A59001A221E69658004A3E5E /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - A59001AA21E69658004A3E5E /* PBXTargetDependency */, - ); - name = in_app_purchase_pluginTests; - productName = in_app_purchase_pluginTests; - productReference = A59001A421E69658004A3E5E /* in_app_purchase_pluginTests.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 Chromium 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 /* in_app_purchase_pluginTests */, - ); - }; -/* 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\" thin"; - }; - 5DF63B80D489A62B306EA07A /* [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"; - }; - AC81012709A36415AE0CF8C4 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - A59001A021E69658004A3E5E /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F78AF3142342BC89008449C7 /* PaymentQueueTest.m in Sources */, - 6896B34621E9363700D37AEF /* ProductRequestHandlerTest.m in Sources */, - 688DE35121F2A5A100EA2684 /* TranslatorTest.m in Sources */, - A59001A721E69658004A3E5E /* InAppPurchasePluginTest.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; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = 1; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.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; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.inAppPurchaseExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; - A59001AB21E69658004A3E5E /* Debug */ = { - isa = XCBuildConfiguration; - 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; - INFOPLIST_FILE = in_app_purchase_pluginTests/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 = "sample.changme.in-app-purchase-pluginTests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; - }; - name = Debug; - }; - A59001AC21E69658004A3E5E /* Release */ = { - isa = XCBuildConfiguration; - 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; - INFOPLIST_FILE = in_app_purchase_pluginTests/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "sample.changme.in-app-purchase-pluginTests"; - 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 "in_app_purchase_pluginTests" */ = { - 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/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/in_app_purchase/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a16ed0f..000000000000 --- a/packages/in_app_purchase/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/packages/in_app_purchase/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/in_app_purchase/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index e1fad2d518ae..000000000000 --- a/packages/in_app_purchase/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/in_app_purchase/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/in_app_purchase/example/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14c74e..000000000000 --- a/packages/in_app_purchase/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/packages/in_app_purchase/example/ios/Runner/AppDelegate.h b/packages/in_app_purchase/example/ios/Runner/AppDelegate.h deleted file mode 100644 index 36e21bbf9cf4..000000000000 --- a/packages/in_app_purchase/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,6 +0,0 @@ -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/in_app_purchase/example/ios/Runner/AppDelegate.m b/packages/in_app_purchase/example/ios/Runner/AppDelegate.m deleted file mode 100644 index 59a72e90be12..000000000000 --- a/packages/in_app_purchase/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,13 +0,0 @@ -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf03016f..000000000000 Binary files a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca8..000000000000 Binary files a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cde12118dda..000000000000 Binary files a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb8..000000000000 Binary files a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index dcdc2306c285..000000000000 Binary files a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee..000000000000 Binary files a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a9..000000000000 Binary files a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7..000000000000 Binary files a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 6a84f41e14e2..000000000000 Binary files a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index d0e1f5853602..000000000000 Binary files a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/packages/in_app_purchase/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/in_app_purchase/example/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f2e259c7c939..000000000000 --- a/packages/in_app_purchase/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/in_app_purchase/example/ios/Runner/Base.lproj/Main.storyboard b/packages/in_app_purchase/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516fb38..000000000000 --- a/packages/in_app_purchase/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/in_app_purchase/example/ios/Runner/main.m b/packages/in_app_purchase/example/ios/Runner/main.m deleted file mode 100644 index dff6597e4513..000000000000 --- a/packages/in_app_purchase/example/ios/Runner/main.m +++ /dev/null @@ -1,9 +0,0 @@ -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/in_app_purchase/example/lib/consumable_store.dart b/packages/in_app_purchase/example/lib/consumable_store.dart deleted file mode 100644 index 12121a9d30ce..000000000000 --- a/packages/in_app_purchase/example/lib/consumable_store.dart +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'package:shared_preferences/shared_preferences.dart'; - -// This is just a development prototype for locally storing consumables. Do not -// use this. -class ConsumableStore { - static const String _kPrefKey = 'consumables'; - static Future _writes = Future.value(); - - static Future save(String id) { - _writes = _writes.then((void _) => _doSave(id)); - return _writes; - } - - static Future consume(String id) { - _writes = _writes.then((void _) => _doConsume(id)); - return _writes; - } - - 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(); - cached.add(id); - await prefs.setStringList(_kPrefKey, cached); - } - - static Future _doConsume(String id) async { - List cached = await load(); - SharedPreferences prefs = await SharedPreferences.getInstance(); - cached.remove(id); - await prefs.setStringList(_kPrefKey, cached); - } -} diff --git a/packages/in_app_purchase/example/lib/main.dart b/packages/in_app_purchase/example/lib/main.dart deleted file mode 100644 index 352ad3b3402a..000000000000 --- a/packages/in_app_purchase/example/lib/main.dart +++ /dev/null @@ -1,389 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:io'; -import 'package:flutter/material.dart'; -import 'package:in_app_purchase/in_app_purchase.dart'; -import 'consumable_store.dart'; - -void main() { - // 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. - InAppPurchaseConnection.enablePendingPurchases(); - runApp(MyApp()); -} - -const bool kAutoConsume = true; - -const String _kConsumableId = 'consumable'; -const List _kProductIds = [ - _kConsumableId, - 'upgrade', - 'subscription' -]; - -class MyApp extends StatefulWidget { - @override - _MyAppState createState() => _MyAppState(); -} - -class _MyAppState extends State { - final InAppPurchaseConnection _connection = InAppPurchaseConnection.instance; - StreamSubscription> _subscription; - List _notFoundIds = []; - List _products = []; - List _purchases = []; - List _consumables = []; - bool _isAvailable = false; - bool _purchasePending = false; - bool _loading = true; - String _queryProductError; - - @override - void initState() { - Stream purchaseUpdated = - InAppPurchaseConnection.instance.purchaseUpdatedStream; - _subscription = purchaseUpdated.listen((purchaseDetailsList) { - _listenToPurchaseUpdated(purchaseDetailsList); - }, onDone: () { - _subscription.cancel(); - }, onError: (error) { - // handle error here. - }); - initStoreInfo(); - super.initState(); - } - - Future initStoreInfo() async { - final bool isAvailable = await _connection.isAvailable(); - if (!isAvailable) { - setState(() { - _isAvailable = isAvailable; - _products = []; - _purchases = []; - _notFoundIds = []; - _consumables = []; - _purchasePending = false; - _loading = false; - }); - return; - } - - ProductDetailsResponse productDetailResponse = - await _connection.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 QueryPurchaseDetailsResponse purchaseResponse = - await _connection.queryPastPurchases(); - if (purchaseResponse.error != null) { - // handle query past purchase error.. - } - final List verifiedPurchases = []; - for (PurchaseDetails purchase in purchaseResponse.pastPurchases) { - if (await _verifyPurchase(purchase)) { - verifiedPurchases.add(purchase); - } - } - List consumables = await ConsumableStore.load(); - setState(() { - _isAvailable = isAvailable; - _products = productDetailResponse.productDetails; - _purchases = verifiedPurchases; - _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(), - ], - ), - ); - } 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) { - InAppPurchaseConnection.instance.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 - ? Icon(Icons.check) - : FlatButton( - child: Text(productDetails.price), - color: Colors.green[800], - textColor: Colors.white, - onPressed: () { - PurchaseParam purchaseParam = PurchaseParam( - productDetails: productDetails, - applicationUserName: null, - sandboxTesting: true); - if (productDetails.id == _kConsumableId) { - _connection.buyConsumable( - purchaseParam: purchaseParam, - autoConsume: kAutoConsume || Platform.isIOS); - } else { - _connection.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), - ) - ])); - } - - 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 a purchase 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 (Platform.isAndroid) { - if (!kAutoConsume && purchaseDetails.productID == _kConsumableId) { - await InAppPurchaseConnection.instance - .consumePurchase(purchaseDetails); - } - } - if (purchaseDetails.pendingCompletePurchase) { - await InAppPurchaseConnection.instance - .completePurchase(purchaseDetails); - } - } - }); - } -} diff --git a/packages/in_app_purchase/example/pubspec.yaml b/packages/in_app_purchase/example/pubspec.yaml deleted file mode 100644 index 48359dbc6a06..000000000000 --- a/packages/in_app_purchase/example/pubspec.yaml +++ /dev/null @@ -1,26 +0,0 @@ -name: in_app_purchase_example -description: Demonstrates how to use the in_app_purchase plugin. -author: Flutter Team - -dependencies: - flutter: - sdk: flutter - cupertino_icons: ^0.1.2 - shared_preferences: ^0.5.2 - -dev_dependencies: - test: ^1.5.2 - flutter_driver: - sdk: flutter - in_app_purchase: - path: ../ - integration_test: - path: ../../integration_test - pedantic: ^1.8.0 - -flutter: - uses-material-design: true - -environment: - sdk: ">=2.3.0 <3.0.0" - flutter: ">=1.9.1+hotfix.2 <2.0.0" diff --git a/packages/in_app_purchase/example/test_driver/test/integration_test.dart b/packages/in_app_purchase/example/test_driver/test/integration_test.dart deleted file mode 100644 index 7a2c21338786..000000000000 --- a/packages/in_app_purchase/example/test_driver/test/integration_test.dart +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'package:flutter_driver/flutter_driver.dart'; - -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String data = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - await driver.close(); - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} diff --git a/packages/in_app_purchase/in_app_purchase/AUTHORS b/packages/in_app_purchase/in_app_purchase/AUTHORS new file mode 100644 index 000000000000..78f9e5ad9f6b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/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> +Maurits van Beusekom diff --git a/packages/in_app_purchase/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md new file mode 100644 index 000000000000..d41c0d0d2aee --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md @@ -0,0 +1,485 @@ +## 1.0.5 + +* Add explanation for casting `ProductDetails` and `PurchaseDetails` to platform specific implementations in the readme. + +## 1.0.4 + +* Fix `Restoring previous purchases` link in the README.md. + +## 1.0.3 + +* 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); +* Corrected an error in a example snippet displayed in the README.md. + +## 1.0.2 + +* Fix ignoring "autoConsume" param in "InAppPurchase.instance.buyConsumable". + +## 1.0.1 + +* Migrate maven repository from jcenter to mavenCentral. + +## 1.0.0 + +* Stable release of in_app_purchase plugin. + +## 0.6.0+1 + +* Added a reference to the in-app purchase codelab in the README.md. + +## 0.6.0 + +As part of implementing federated architecture and making the interface compatible for other platforms this version contains the following **breaking changes**: + +* Changes to the platform agnostic interface: + * If you used `InAppPurchaseConnection.instance` to access generic In App Purchase APIs, please use `InAppPurchase.instance` instead; + * The `InAppPurchaseConnection.purchaseUpdatedStream` has been renamed to `InAppPurchase.purchaseStream`; + * The `InAppPurchaseConnection.queryPastPurchases` method has been removed. Instead, you should use `InAppPurchase.restorePurchases`. This method emits each restored purchase on the `InAppPurchase.purchaseStream`, the `PurchaseDetails` object will be marked with a `status` of `PurchaseStatus.restored`; + * The `InAppPurchase.completePurchase` method no longer returns an instance `BillingWrapperResult` class (which was Android specific). Instead it will return a completed `Future` if the method executed successfully, in case of errors it will complete with an `InAppPurchaseException` describing the error. +* Android specific changes: + * The Android specific `InAppPurchaseConnection.consumePurchase` and `InAppPurchaseConnection.enablePendingPurchases` methods have been removed from the platform agnostic interface and moved to the Android specific `InAppPurchaseAndroidPlatformAddition` class: + * `InAppPurchaseAndroidPlatformAddition.enablePendingPurchases` is a static method that should be called when initializing your App. Access the method like this: `InAppPurchaseAndroidPlatformAddition.enablePendingPurchases()` (make sure to add the following import: `import 'package:in_app_purchase_android/in_app_purchase_android.dart';`); + * To use the `InAppPurchaseAndroidPlatformAddition.consumePurchase` method, acquire an instance using the `InAppPurchase.getPlatformAddition` method. For example: + ```dart + // Acquire the InAppPurchaseAndroidPlatformAddition instance. + InAppPurchaseAndroidPlatformAddition androidAddition = InAppPurchase.instance.getPlatformAddition(); + // Consume an Android purchase. + BillingResultWrapper billingResult = await androidAddition.consumePurchase(purchase); + ``` + * The [billing_client_wrappers](https://pub.dev/documentation/in_app_purchase_android/latest/billing_client_wrappers/billing_client_wrappers-library.html) have been moved into the [in_app_purchase_android](https://pub.dev/packages/in_app_purchase_android) package. They are still available through the [in_app_purchase](https://pub.dev/packages/in_app_purchase) plugin but to use them it is necessary to import the correct package when using them: `import 'package:in_app_purchase_android/billing_client_wrappers.dart';`; +* iOS specific changes: + * The iOS specific methods `InAppPurchaseConnection.presentCodeRedemptionSheet` and `InAppPurchaseConnection.refreshPurchaseVerificationData` methods have been removed from the platform agnostic interface and moved into the iOS specific `InAppPurchaseIosPlatformAddition` class. To use them acquire an instance through the `InAppPurchase.getPlatformAddition` method like so: + ```dart + // Acquire the InAppPurchaseIosPlatformAddition instance. + InAppPurchaseIosPlatformAddition iosAddition = InAppPurchase.instance.getPlatformAddition(); + // Present the code redemption sheet. + await iosAddition.presentCodeRedemptionSheet(); + // Refresh purchase verification data. + PurchaseVerificationData? verificationData = await iosAddition.refreshPurchaseVerificationData(); + ``` + * The [store_kit_wrappers](https://pub.dev/documentation/in_app_purchase_ios/latest/store_kit_wrappers/store_kit_wrappers-library.html) have been moved into the [in_app_purchase_ios](https://pub.dev/packages/in_app_purchase_ios) package. They are still available in the [in_app_purchase](https://pub.dev/packages/in_app_purchase) plugin, but to use them it is necessary to import the correct package when using them: `import 'package:in_app_purchase_ios/store_kit_wrappers.dart';`; + * Update the minimum supported Flutter version to 1.20.0. + +## 0.5.2 + +* Added `rawPrice` and `currencyCode` to the ProductDetails model. + +## 0.5.1+3 + +* Configured the iOS example App to make use of StoreKit Testing on iOS 14 and higher. + +## 0.5.1+2 + +* Update README to provide a better instruction of the plugin. + +## 0.5.1+1 + +* Fix error message when trying to consume purchase on iOS. + +## 0.5.1 + +* [iOS] Introduce `SKPaymentQueueWrapper.presentCodeRedemptionSheet` + +## 0.5.0 + +* Migrate to Google Billing Library 3.0 + * Add `obfuscatedProfileId`, `purchaseToken` in [BillingClientWrapper.launchBillingFlow]. + * **Breaking Change** + * Removed `developerPayload` in [BillingClientWrapper.acknowledgePurchase], [BillingClientWrapper.consumeAsync], [InAppPurchaseConnection.completePurchase], [InAppPurchaseConnection.consumePurchase]. + * Removed `isRewarded` from [SkuDetailsWrapper]. + * [SkuDetailsWrapper.introductoryPriceCycles] now returns `int` instead of `String`. + * Above breaking changes are inline with the breaking changes introduced in [Google Play Billing 3.0 release](https://developer.android.com/google/play/billing/release-notes#3-0). + * Additional information on some the changes: + * [Dropping reward SKU support](https://support.google.com/googleplay/android-developer/answer/9155268?hl=en) + * [Developer payload](https://developer.android.com/google/play/billing/developer-payload) + +## 0.4.1 + +* Support InApp subscription upgrade/downgrade. + +## 0.4.0 + +* Migrate to nullsafety. +* Deprecate `sandboxTesting`, introduce `simulatesAskToBuyInSandbox`. +* **Breaking Change:** + * Removed `callbackChannel` in `channels.dart`, see https://github.com/flutter/flutter/issues/69225. + +## 0.3.5+2 + +* Migrate deprecated references. + +## 0.3.5+1 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 0.3.5 + +* [Android] Fixed: added support for the SERVICE_TIMEOUT (-3) response code. + +## 0.3.4+18 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 0.3.4+17 + +* Update Flutter SDK constraint. + +## 0.3.4+16 + +* Add Dartdocs to all public APIs. + +## 0.3.4+15 + +* Update android compileSdkVersion to 29. + +## 0.3.4+14 + +* Add test target to iOS example app Podfile + +## 0.3.4+13 + +* Android Code Inspection and Clean up. + +## 0.3.4+12 + +* [iOS] Fixed: finishing purchases upon payment dialog cancellation. + +## 0.3.4+11 + +* [iOS] Fixed: crash when sending null for simulatesAskToBuyInSandbox parameter. + +## 0.3.4+10 + +* Fixed typo 'verity' for 'verify'. + +## 0.3.4+9 + +* [iOS] Fixed: purchase dialog not showing always. +* [iOS] Fixed: completing purchases could fail. +* [iOS] Fixed: restorePurchases caused hang (call never returned). + +## 0.3.4+8 + +* [iOS] Fixed: purchase dialog not showing always. +* [iOS] Fixed: completing purchases could fail. +* [iOS] Fixed: restorePurchases caused hang (call never returned). + +## 0.3.4+7 + +* iOS: Fix typo of the `simulatesAskToBuyInSandbox` key. + +## 0.3.4+6 + +* iOS: Fix the bug that prevent restored subscription transactions from being completed + +## 0.3.4+5 + +* Added necessary README docs for getting started with Android. + +## 0.3.4+4 + +* Update package:e2e -> package:integration_test + +## 0.3.4+3 + +* Fixed typo 'manuelly' for 'manually'. + +## 0.3.4+2 + +* Update package:e2e reference to use the local version in the flutter/plugins + repository. + +## 0.3.4+1 + +* iOS: Fix the bug that `SKPaymentQueueWrapper.transactions` doesn't return all transactions. +* iOS: Fix the app crashes if `InAppPurchaseConnection.instance` is called in the `main()`. + +## 0.3.4 + +* Expose SKError code to client apps. + +## 0.3.3+2 + +* Post-v2 Android embedding cleanups. + +## 0.3.3+1 + +* Update documentations for `InAppPurchase.completePurchase` and update README. + +## 0.3.3 + +* Introduce `SKPaymentQueueWrapper.transactions`. + +## 0.3.2+2 + +* Fix CocoaPods podspec lint warnings. + +## 0.3.2+1 + +* iOS: Fix only transactions with SKPaymentTransactionStatePurchased and SKPaymentTransactionStateFailed can be finished. +* iOS: Only one pending transaction of a given product is allowed. + +## 0.3.2 + +* Remove Android dependencies fallback. +* Require Flutter SDK 1.12.13+hotfix.5 or greater. + +## 0.3.1+2 + +* Fix potential casting crash on Android v1 embedding when registering life cycle callbacks. +* Remove hard-coded legacy xcode build setting. + +## 0.3.1+1 + +* Add `pedantic` to dev_dependency. + +## 0.3.1 + +* Android: Fix a bug where the `BillingClient` is disconnected when app goes to the background. +* Android: Make sure the `BillingClient` object is disconnected before the activity is destroyed. +* Android: Fix minor compiler warning. +* Fix typo in CHANGELOG. + +## 0.3.0+3 + +* Fix pendingCompletePurchase flag status to allow to complete purchases. + +## 0.3.0+2 + +* Update te example app to avoid using deprecated api. + +## 0.3.0+1 + +* Fixing usage example. No functional changes. + +## 0.3.0 + +* Migrate the `Google Play Library` to 2.0.3. + * Introduce a new class `BillingResultWrapper` which contains a detailed result of a BillingClient operation. + * **[Breaking Change]:** All the BillingClient methods that previously return a `BillingResponse` now return a `BillingResultWrapper`, including: `launchBillingFlow`, `startConnection` and `consumeAsync`. + * **[Breaking Change]:** The `SkuDetailsResponseWrapper` now contains a `billingResult` field in place of `billingResponse` field. + * A `billingResult` field is added to the `PurchasesResultWrapper`. + * Other Updates to the "billing_client_wrappers": + * Updates to the `PurchaseWrapper`: Add `developerPayload`, `purchaseState` and `isAcknowledged` fields. + * Updates to the `SkuDetailsWrapper`: Add `originalPrice` and `originalPriceAmountMicros` fields. + * **[Breaking Change]:** The `BillingClient.queryPurchaseHistory` is updated to return a `PurchasesHistoryResult`, which contains a list of `PurchaseHistoryRecordWrapper` instead of `PurchaseWrapper`. A `PurchaseHistoryRecordWrapper` object has the same fields and values as A `PurchaseWrapper` object, except that a `PurchaseHistoryRecordWrapper` object does not contain `isAutoRenewing`, `orderId` and `packageName`. + * Add a new `BillingClient.acknowledgePurchase` API. Starting from this version, the developer has to acknowledge any purchase on Android using this API within 3 days of purchase, or the user will be refunded. Note that if a product is "consumed" via `BillingClient.consumeAsync`, it is implicitly acknowledged. + * **[Breaking Change]:** Added `enablePendingPurchases` in `BillingClientWrapper`. The application has to call this method before calling `BillingClientWrapper.startConnection`. See [enablePendingPurchases](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html#enablependingpurchases) for more information. + * Updates to the "InAppPurchaseConnection": + * **[Breaking Change]:** `InAppPurchaseConnection.completePurchase` now returns a `Future` instead of `Future`. A new optional parameter `{String developerPayload}` has also been added to the API. On Android, this API does not throw an exception anymore, it instead acknowledge the purchase. If a purchase is not completed within 3 days on Android, the user will be refunded. + * **[Breaking Change]:** `InAppPurchaseConnection.consumePurchase` now returns a `Future` instead of `Future`. A new optional parameter `{String developerPayload}` has also been added to the API. + * A new boolean field `pendingCompletePurchase` has been added to the `PurchaseDetails` class. Which can be used as an indicator of whether to call `InAppPurchaseConnection.completePurchase` on the purchase. + * **[Breaking Change]:** Added `enablePendingPurchases` in `InAppPurchaseConnection`. The application has to call this method when initializing the `InAppPurchaseConnection` on Android. See [enablePendingPurchases](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html#enablependingpurchases) for more information. + * Misc: Some documentation updates reflecting the `BillingClient` migration and some documentation fixes. + * Refer to [Google Play Billing Library Release Note](https://developer.android.com/google/play/billing/billing_library_releases_notes#release-2_0) for a detailed information on the update. + +## 0.2.2+6 + +* Correct a comment. + +## 0.2.2+5 + +* Update version of json_annotation to ^3.0.0 and json_serializable to ^3.2.0. Resolve conflicts with other packages e.g. flutter_tools from sdk. + +## 0.2.2+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.2.2+3 + +* Fix failing pedantic lints. None of these fixes should have any change in + functionality. + +## 0.2.2+2 + +* Include lifecycle dependency as a compileOnly one on Android to resolve + potential version conflicts with other transitive libraries. + +## 0.2.2+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.2.2 + +* Support the v2 Android embedder. +* Update to AndroidX. +* Migrate to using the new e2e test binding. +* Add a e2e test. + +## 0.2.1+5 + +* Define clang module for iOS. +* Fix iOS build warning. + +## 0.2.1+4 + +* Update and migrate iOS example project. + +## 0.2.1+3 + +* Android : Improved testability. + +## 0.2.1+2 + +* Android: Require a non-null Activity to use the `launchBillingFlow` method. + +## 0.2.1+1 + +* Remove skipped driver test. + +## 0.2.1 + +* iOS: Add currencyCode to priceLocale on productDetails. + +## 0.2.0+8 + +* Add dependency on `androidx.annotation:annotation:1.0.0`. + +## 0.2.0+7 + +* Make Gradle version compatible with the Android Gradle plugin version. + +## 0.2.0+6 + +* Add missing `hashCode` implementations. + +## 0.2.0+5 + +* iOS: Support unsupported UserInfo value types on NSError. + +## 0.2.0+4 + +* Fixed code error in `README.md` and adjusted links to work on Pub. + +## 0.2.0+3 + +* Update the `README.md` so that the code samples compile with the latest Flutter/Dart version. + +## 0.2.0+2 + +* Fix a google_play_connection purchase update listener regression introduced in 0.2.0+1. + +## 0.2.0+1 + +* Fix an issue the type is not casted before passing to `PurchasesResultWrapper.fromJson`. + +## 0.2.0 + +* [Breaking Change] Rename 'PurchaseError' to 'IAPError'. +* [Breaking Change] Rename 'PurchaseSource' to 'IAPSource'. + +## 0.1.1+3 + +* Expanded description in `pubspec.yaml` and fixed typo in `README.md`. + +## 0.1.1+2 + +* Add missing template type parameter to `invokeMethod` calls. +* Bump minimum Flutter version to 1.5.0. +* Replace invokeMethod with invokeMapMethod wherever necessary. + +## 0.1.1+1 + +* Make `AdditionalSteps`(Used in the unit test) a void function. + +## 0.1.1 + +* Some error messages from iOS are slightly changed. +* `ProductDetailsResponse` returned by `queryProductDetails()` now contains an `PurchaseError` object that represents any error that might occurred during the request. +* If the device is not connected to the internet, `queryPastPurchases()` on iOS now have the error stored in the response instead of throwing. +* Clean up minor iOS warning. +* Example app shows how to handle error when calling `queryProductDetails()` and `queryProductDetails()`. + +## 0.1.0+4 + +* Change the `buy` methods to return `Future` instead of `void` in order + to propagate `launchBillingFlow` failures up through `google_play_connection`. + +## 0.1.0+3 + +* Guard against multiple onSetupFinished() calls. + +## 0.1.0+2 + +* Fix bug where error only purchases updates weren't propagated correctly in + `google_play_connection.dart`. + +## 0.1.0+1 + +* Add more consumable handling to the example app. + +## 0.1.0 + +Beta release. + +* Ability to list products, load previous purchases, and make purchases. +* Simplified Dart API that's been unified for ease of use. +* Platform-specific APIs more directly exposing `StoreKit` and `BillingClient`. + +Includes: + +* 5ba657dc [in_app_purchase] Remove extraneous download logic (#1560) +* 01bb8796 [in_app_purchase] Minor doc updates (#1555) +* 1a4d493f [in_app_purchase] Only fetch owned purchases (#1540) +* d63c51cf [in_app_purchase] Add auto-consume errors to PurchaseDetails (#1537) +* 959da97f [in_app_purchase] Minor doc updates (#1536) +* b82ae1a6 [in_app_purchase] Rename the unified API (#1517) +* d1ad723a [in_app_purchase]remove SKDownloadWrapper and related code. (#1474) +* 7c1e8b8a [in_app_purchase]make payment unified APIs (#1421) +* 80233db6 [in_app_purchase] Add references to the original object for PurchaseDetails and ProductDetails (#1448) +* 8c180f0d [in_app_purchase]load purchase (#1380) +* e9f141bc [in_app_purchase] Iap refactor (#1381) +* d3b3d60c add driver test command to cirrus (#1342) +* aee12523 [in_app_purchase] refactoring and tests (#1322) +* 6d7b4592 [in_app_purchase] Adds Dart BillingClient APIs for loading purchases (#1286) +* 5567a9c8 [in_app_purchase]retrieve receipt (#1303) +* 3475f1b7 [in_app_purchase]restore purchases (#1299) +* a533148d [in_app_purchase] payment queue dart ios (#1249) +* 10030840 [in_app_purchase] Minor bugfixes and code cleanup (#1284) +* 347f508d [in_app_purchase] Fix CI formatting errors. (#1281) +* fad02d87 [in_app_purchase] Java API for querying purchases (#1259) +* bc501915 [In_app_purchase]SKProduct related fixes (#1252) +* f92ba3a1 IAP make payment objc (#1231) +* 62b82522 [IAP] Add the Dart API for launchBillingFlow (#1232) +* b40a4acf [IAP] Add Java call for launchBillingFlow (#1230) +* 4ff06cd1 [In_app_purchase]remove categories (#1222) +* 0e72ca56 [In_app_purchase]fix requesthandler crash (#1199) +* 81dff2be Iap getproductlist basic draft (#1169) +* db139b28 Iap iOS add payment dart wrappers (#1178) +* 2e5fbb9b Fix the param map passed down to the platform channel when calling querySkuDetails (#1194) +* 4a84bac1 Mark some packages as unpublishable (#1193) +* 51696552 Add a gradle warning to the AndroidX plugins (#1138) +* 832ab832 Iap add payment objc translators (#1172) +* d0e615cf Revert "IAP add payment translators in objc (#1126)" (#1171) +* 09a5a36e IAP add payment translators in objc (#1126) +* a100fbf9 Expose nslocale and expose currencySymbol instead of currencyCode to match android (#1162) +* 1c982efd Using json serializer for skproduct wrapper and related classes (#1147) +* 3039a261 Iap productlist ios (#1068) +* 2a1593da [IAP] Update dev deps to match flutter_driver (#1118) +* 9f87cbe5 [IAP] Update README (#1112) +* 59e84d85 Migrate independent plugins to AndroidX (#1103) +* a027ccd6 [IAP] Generate boilerplate serializers (#1090) +* 909cf1c2 [IAP] Fetch SkuDetails from Google Play (#1084) +* 6bbaa7e5 [IAP] Add missing license headers (#1083) +* 5347e877 [IAP] Clean up Dart unit tests (#1082) +* fe03e407 [IAP] Check if the payment processor is available (#1057) +* 43ee28cf Fix `Manifest versionCode not found` (#1076) +* 4d702ad7 Supress `strong_mode_implicit_dynamic_method` for `invokeMethod` calls. (#1065) +* 809ccde7 Doc and build script updates to the IAP plugin (#1024) +* 052b71a9 Update the IAP README (#933) +* 54f9c4e2 Upgrade Android Gradle Plugin to 3.2.1 (#916) +* ced3e99d Set all gradle-wrapper versions to 4.10.2 (#915) +* eaa1388b Reconfigure Cirrus to use clang 7 (#905) +* 9b153920 Update gradle dependencies. (#881) +* 1aef7d92 Enable lint unnecessary_new (#701) + +## 0.0.2 + +* Added missing flutter_test package dependency. +* Added missing flutter version requirements. + +## 0.0.1 + +* Initial release. diff --git a/packages/in_app_purchase/in_app_purchase/LICENSE b/packages/in_app_purchase/in_app_purchase/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/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/in_app_purchase/in_app_purchase/README.md b/packages/in_app_purchase/in_app_purchase/README.md new file mode 100644 index 000000000000..ad28cfacb695 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/README.md @@ -0,0 +1,337 @@ +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). + +

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

+ +## Features + +Use this plugin in your Flutter app to: + +* Show in-app products that are available for sale from the underlying store. + Products can include consumables, permanent upgrades, and subscriptions. +* Load in-app products that the user owns. +* Send the user to the underlying store to purchase products. +* Present a UI for redeeming subscription offer codes. (iOS 14 only) + +## Getting started + +This plugin relies on the App Store and Google Play for making in-app purchases. +It exposes a unified surface, but you still need to understand and configure +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) + +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). + +Once you've configured your in-app purchases in their respective stores, you +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) + 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 + purchase-handling logic is significantly different for the different + storefronts. + +See also the codelab for [in-app purchases in Flutter](https://codelabs.developers.google.com/codelabs/flutter-in-app-purchases) for a detailed guide on adding in-app purchase support to a Flutter App. + +## Usage + +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) +* [Restoring previous purchases](#restoring-previous-purchases) +* [Making a purchase](#making-a-purchase) +* [Completing a purchase](#completing-a-purchase) +* [Upgrading or downgrading an existing in-app subscription](#upgrading-or-downgrading-an-existing-in-app-subscription) +* [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'; + +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 + +In your app's `initState` method, subscribe to any incoming purchases. These +can propagate from either underlying store. +You should always start listening to purchase update as early as possible to be able +to catch all purchase updates, including the ones from the previous app session. +To listen to the update: + +```dart +class _MyAppState extends State { + StreamSubscription> _subscription; + + @override + void initState() { + final Stream purchaseUpdated = + InAppPurchase.instance.purchaseStream; + _subscription = purchaseUpdated.listen((purchaseDetailsList) { + _listenToPurchaseUpdated(purchaseDetailsList); + }, onDone: () { + _subscription.cancel(); + }, onError: (error) { + // handle error here. + }); + super.initState(); + } + + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } +``` + +Here is an example of how to handle purchase updates: + +```dart +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 || + purchaseDetails.status == PurchaseStatus.restored) { + bool valid = await _verifyPurchase(purchaseDetails); + if (valid) { + _deliverProduct(purchaseDetails); + } else { + _handleInvalidPurchase(purchaseDetails); + } + } + if (purchaseDetails.pendingCompletePurchase) { + await InAppPurchase.instance + .completePurchase(purchaseDetails); + } + } + }); +} +``` + +### Connecting to the underlying store + +```dart +final bool available = await InAppPurchase.instance.isAvailable(); +if (!available) { + // The store cannot be reached or accessed. Update the UI accordingly. +} +``` + +### Loading products for sale + +```dart +// Set literals require Dart 2.2. Alternatively, use +// `Set _kIds = ['product1', 'product2'].toSet()`. +const Set _kIds = {'product1', 'product2'}; +final ProductDetailsResponse response = + await InAppPurchase.instance.queryProductDetails(_kIds); +if (response.notFoundIDs.isNotEmpty) { + // Handle the error. +} +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 +underlying store: + +* [Verifying App Store purchases](https://developer.apple.com/documentation/storekit/in-app_purchase/validating_receipts_with_the_app_store) +* [Verifying Google Play purchases](https://developer.android.com/google/play/billing/security#verify) + + +```dart +await InAppPurchase.instance.restorePurchases(); +``` + +Note that the App Store does not have any APIs for querying consumable +products, and Google Play considers consumable products to no longer be owned +once they're marked as consumed and fails to return them here. For restoring +these across devices you'll need to persist them on your own server and query +that as well. + +### Making a purchase + +Both underlying stores handle consumable and non-consumable products differently. If +you're using `InAppPurchase`, you need to make a distinction here and +call the right purchase method for each type. + +```dart +final ProductDetails productDetails = ... // Saved earlier from queryProductDetails(). +final PurchaseParam purchaseParam = PurchaseParam(productDetails: productDetails); +if (_isConsumable(productDetails)) { + InAppPurchase.instance.buyConsumable(purchaseParam: purchaseParam); +} else { + InAppPurchase.instance.buyNonConsumable(purchaseParam: purchaseParam); +} +// From here the purchase flow will be handled by the underlying store. +// Updates will be delivered to the `InAppPurchase.instance.purchaseStream`. +``` + +### 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. + +> **Warning:** Failure to call `InAppPurchase.completePurchase` and +> get a successful response within 3 days of the purchase will result a refund. + +### Upgrading or downgrading an existing in-app subscription + +To upgrade/downgrade an existing in-app subscription in Google Play, +you need to provide an instance of `ChangeSubscriptionParam` with the old +`PurchaseDetails` that the user needs to migrate from, and an optional +`ProrationMode` with the `GooglePlayPurchaseParam` object while calling +`InAppPurchase.buyNonConsumable`. + +The App Store does not require this because it provides a subscription +grouping mechanism. Each subscription you offer must be assigned to a +subscription group. Grouping related subscriptions together can help prevent +users from accidentally purchasing multiple subscriptions. Refer to the +[Creating a Subscription Group](https://developer.apple.com/app-store/subscriptions/#groups) section of +[Apple's subscription guide](https://developer.apple.com/app-store/subscriptions/). + +```dart +final PurchaseDetails oldPurchaseDetails = ...; +PurchaseParam purchaseParam = GooglePlayPurchaseParam( + productDetails: productDetails, + changeSubscriptionParam: ChangeSubscriptionParam( + oldPurchaseDetails: oldPurchaseDetails, + prorationMode: ProrationMode.immediateWithTimeProration)); +InAppPurchase.instance + .buyNonConsumable(purchaseParam: purchaseParam); +``` + +### 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 +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. + +This is an example on how to get the `introductoryPricePeriod` on Android: +```dart +//import for GooglePlayProductDetails +import 'package:in_app_purchase_android/in_app_purchase_android.dart'; +//import for SkuDetailsWrapper +import 'package:in_app_purchase_android/billing_client_wrappers.dart'; + +if (productDetails is GooglePlayProductDetails) { + SkuDetailsWrapper skuDetails = (productDetails as GooglePlayProductDetails).skuDetails; + print(skuDetails.introductoryPricePeriod); +} +``` + +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 for SKProductWrapper +import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; + +if (productDetails is AppStoreProductDetails) { + SKProductWrapper skProduct = (productDetails as AppStoreProductDetails).skProduct; + print(skProduct.subscriptionGroupIdentifier); +} +``` + +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: +```dart +//import for GooglePlayPurchaseDetails +import 'package:in_app_purchase_android/in_app_purchase_android.dart'; +//import for PurchaseWrapper +import 'package:in_app_purchase_android/billing_client_wrappers.dart'; + +if (purchaseDetails is GooglePlayPurchaseDetails) { + PurchaseWrapper billingClientPurchase = (purchaseDetails as GooglePlayPurchaseDetails).billingClientPurchase; + print(billingClientPurchase.originalJson); +} +``` + +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 for SKProductWrapper +import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; + +if (purchaseDetails is AppStorePurchaseDetails) { + SKPaymentTransactionWrapper skProduct = (purchaseDetails as AppStorePurchaseDetails).skPaymentTransaction; + print(skProduct.transactionState); +} +``` + +Please note that it is required to import `in_app_purchase_android` and/or `in_app_purchase_ios`. + +### Presenting a code redemption sheet (iOS 14) + +The following code brings up a sheet that enables the user to redeem offer +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(); +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`: +> ```dart +> import 'package:in_app_purchase_ios/in_app_purchase_ios.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). diff --git a/packages/in_app_purchase/in_app_purchase/analysis_options.yaml b/packages/in_app_purchase/in_app_purchase/analysis_options.yaml new file mode 100644 index 000000000000..5aeb4e7c5e21 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/analysis_options.yaml @@ -0,0 +1 @@ +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 new file mode 100644 index 000000000000..e15cf14b85fd --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/build.yaml @@ -0,0 +1,7 @@ +targets: + $default: + builders: + json_serializable: + options: + any_map: true + create_to_json: true diff --git a/packages/in_app_purchase/in_app_purchase/doc/iap_android.gif b/packages/in_app_purchase/in_app_purchase/doc/iap_android.gif new file mode 100644 index 000000000000..86348e4f6294 Binary files /dev/null and b/packages/in_app_purchase/in_app_purchase/doc/iap_android.gif differ diff --git a/packages/in_app_purchase/in_app_purchase/doc/iap_ios.gif b/packages/in_app_purchase/in_app_purchase/doc/iap_ios.gif new file mode 100644 index 000000000000..a2cba74412d7 Binary files /dev/null and b/packages/in_app_purchase/in_app_purchase/doc/iap_ios.gif differ diff --git a/packages/in_app_purchase/example/.metadata b/packages/in_app_purchase/in_app_purchase/example/.metadata similarity index 100% rename from packages/in_app_purchase/example/.metadata rename to packages/in_app_purchase/in_app_purchase/example/.metadata diff --git a/packages/in_app_purchase/in_app_purchase/example/README.md b/packages/in_app_purchase/in_app_purchase/example/README.md new file mode 100644 index 000000000000..65b5dad6214a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/README.md @@ -0,0 +1,118 @@ +# In App Purchase Example + +Demonstrates how to use the In App Purchase (IAP) Plugin. + +## Getting Started + +### Preparation + +There's a significant amount of setup required for testing in app purchases +successfully, including registering new app IDs and store entries to use for +testing in both the Play Developer Console and App Store Connect. Both Google +Play and the App Store require developers to configure an app with in-app items +for purchase to call their in-app-purchase APIs. Both stores have extensive +documentation on how to do this, and we've also included a high level guide +below. + +* [In-App Purchase (App Store)](https://developer.apple.com/in-app-purchase/) +* [Google Play Billing Overview](https://developer.android.com/google/play/billing/billing_overview) + +### Android + +1. Create a new app in the [Play Developer + Console](https://play.google.com/apps/publish/) (PDC). + +2. Sign up for a merchant's account in the PDC. + +3. Create IAPs in the PDC available for purchase in the app. The example assumes + the following SKU IDs exist: + + - `consumable`: A managed product. + - `upgrade`: A managed product. + - `subscription_silver`: A lower level subscription. + - `subscription_gold`: A higher level subscription. + + Make sure that all the products are set to `ACTIVE`. + +4. Update `APP_ID` in `example/android/app/build.gradle` to match your package + ID in the PDC. + +5. Create an `example/android/keystore.properties` file with all your signing + information. `keystore.example.properties` exists as an example to follow. + It's impossible to use any of the `BillingClient` APIs from an unsigned APK. + See + [here](https://developer.android.com/studio/publish/app-signing#secure-shared-keystore) + and [here](https://developer.android.com/studio/publish/app-signing#sign-apk) + for more information. + +6. Build a signed apk. `flutter build apk` will work for this, the gradle files + in this project have been configured to sign even debug builds. + +7. Upload the signed APK from step 6 to the PDC, and publish that to the alpha + test channel. Add your test account as an approved tester. The + `BillingClient` APIs won't work unless the app has been fully published to + the alpha channel and is being used by an authorized test account. See + [here](https://support.google.com/googleplay/android-developer/answer/3131213) + for more info. + +8. Sign in to the test device with the test account from step #7. Then use + `flutter run` to install the app to the device and test like normal. + +### iOS + +When using Xcode 12 and iOS 14 or higher you can run the example in the simulator or on a device without +having to configure an App in App Store Connect. The example app is set up to use StoreKit Testing configured +in the `example/ios/Runner/Configuration.storekit` file (as documented in the article [Setting Up StoreKit Testing in Xcode](https://developer.apple.com/documentation/xcode/setting_up_storekit_testing_in_xcode?language=objc)). +To run the application take the following steps (note that it will only work when running from Xcode): + +1. Open the example app with Xcode, `File > Open File` `example/ios/Runner.xcworkspace`; + +2. Within Xcode edit the current scheme, `Product > Scheme > Edit Scheme...` (or press `Command + Shift + ,`); + +3. Enable StoreKit testing: + a. Select the `Run` action; + b. Click `Options` in the action settings; + c. Select the `Configuration.storekit` for the StoreKit Configuration option. + +4. Click the `Close` button to close the scheme editor; + +5. Select the device you want to run the example App on; + +6. Run the application using `Product > Run` (or hit the run button). + +When testing on pre-iOS 14 you can't run the example app on a simulator and you will need to configure an app in App Store Connect. You can do so by following the steps below: + +1. Follow ["Workflow for configuring in-app + purchases"](https://help.apple.com/app-store-connect/#/devb57be10e7), a + detailed guide on all the steps needed to enable IAPs for an app. Complete + steps 1 ("Sign a Paid Applications Agreement") and 2 ("Configure in-app + purchases"). + + For step #2, "Configure in-app purchases in App Store Connect," you'll want + to create the following products: + + - A consumable with product ID `consumable` + - An upgrade with product ID `upgrade` + - An auto-renewing subscription with product ID `subscription_silver` + - An non-renewing subscription with product ID `subscription_gold` + +2. In XCode, `File > Open File` `example/ios/Runner.xcworkspace`. Update the + Bundle ID to match the Bundle ID of the app created in step #1. + +3. [Create a Sandbox tester + account](https://help.apple.com/app-store-connect/#/dev8b997bee1) to test the + in-app purchases with. + +4. Use `flutter run` to install the app and test it. Note that you need to test + it on a real device instead of a simulator. Next click on one of the products + in the example App, this enables the "SANDBOX ACCOUNT" section in the iOS + settings. You will now be asked to sign in with your sandbox test account to + complete the purchase (no worries you won't be charged). If for some reason + you aren't asked to sign-in or the wrong user is listed, go into the iOS + settings ("Settings" -> "App Store" -> "SANDBOX ACCOUNT") and update your + sandbox account from there. This procedure is explained in great detail in + the [Testing In-App Purchases with Sandbox](https://developer.apple.com/documentation/storekit/in-app_purchase/testing_in-app_purchases_with_sandbox?language=objc) article. + + +**Important:** signing into any production service (including iTunes!) with the +sandbox test account will permanently invalidate it. 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 new file mode 100644 index 000000000000..c95804685219 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/android/app/build.gradle @@ -0,0 +1,115 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +// Load the build signing secrets from a local `keystore.properties` file. +// TODO(YOU): Create release keys and a `keystore.properties` file. See +// `example/README.md` for more info and `keystore.example.properties` for an +// example. +def keystorePropertiesFile = rootProject.file("keystore.properties") +def keystoreProperties = new Properties() +def configured = true +try { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} catch (IOException e) { + configured = false + logger.error('Release signing information not found.') +} + +project.ext { + // TODO(YOU): Create release keys and a `keystore.properties` file. See + // `example/README.md` for more info and `keystore.example.properties` for an + // example. + APP_ID = configured ? keystoreProperties['appId'] : "io.flutter.plugins.inapppurchaseexample.DEFAULT_DO_NOT_USE" + KEYSTORE_STORE_FILE = configured ? rootProject.file(keystoreProperties['storeFile']) : null + KEYSTORE_STORE_PASSWORD = keystoreProperties['storePassword'] + KEYSTORE_KEY_ALIAS = keystoreProperties['keyAlias'] + KEYSTORE_KEY_PASSWORD = keystoreProperties['keyPassword'] + VERSION_CODE = configured ? keystoreProperties['versionCode'].toInteger() : 1 + VERSION_NAME = configured ? keystoreProperties['versionName'] : "0.0.1" +} + +if (project.APP_ID == "io.flutter.plugins.inapppurchaseexample.DEFAULT_DO_NOT_USE") { + configured = false + logger.error('Unique package name not set, defaulting to "io.flutter.plugins.inapppurchaseexample.DEFAULT_DO_NOT_USE".') +} + +// Log a final error message if we're unable to create a release key signed +// build for an app configured in the Play Developer Console. Apks built in this +// condition won't be able to call any of the BillingClient APIs. +if (!configured) { + logger.error('The app could not be configured for release signing. In app purchases will not be testable. See `example/README.md` for more info and instructions.') +} + +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.") +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + signingConfigs { + release { + storeFile project.KEYSTORE_STORE_FILE + storePassword project.KEYSTORE_STORE_PASSWORD + keyAlias project.KEYSTORE_KEY_ALIAS + keyPassword project.KEYSTORE_KEY_PASSWORD + } + } + + compileSdkVersion 29 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + applicationId project.APP_ID + minSdkVersion 16 + targetSdkVersion 28 + versionCode project.VERSION_CODE + versionName project.VERSION_NAME + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + // Google Play Billing APIs only work with apps signed for production. + debug { + if (configured) { + signingConfig signingConfigs.release + } else { + signingConfig signingConfigs.debug + } + } + release { + if (configured) { + signingConfig signingConfigs.release + } else { + signingConfig signingConfigs.debug + } + } + } + + testOptions { + unitTests.returnDefaultValues = true + } +} + +flutter { + source '../..' +} + +dependencies { + implementation 'com.android.billingclient:billing:3.0.2' + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-core:3.6.0' + testImplementation 'org.json:json:20180813' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' +} diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/in_app_purchase/in_app_purchase/example/android/app/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties rename to packages/in_app_purchase/in_app_purchase/example/android/app/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/in_app_purchase/example/android/app/src/main/AndroidManifest.xml b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/AndroidManifest.xml similarity index 100% rename from packages/in_app_purchase/example/android/app/src/main/AndroidManifest.xml rename to packages/in_app_purchase/in_app_purchase/example/android/app/src/main/AndroidManifest.xml diff --git a/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/EmbeddingV1Activity.java b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/EmbeddingV1Activity.java new file mode 100644 index 000000000000..c74ad9447e81 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/EmbeddingV1Activity.java @@ -0,0 +1,24 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.inapppurchaseexample; + +import android.os.Bundle; +import dev.flutter.plugins.integration_test.IntegrationTestPlugin; +import io.flutter.plugins.inapppurchase.InAppPurchasePlugin; +import io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin; + +@SuppressWarnings("deprecation") +public class EmbeddingV1Activity extends io.flutter.app.FlutterActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + IntegrationTestPlugin.registerWith( + registrarFor("dev.flutter.plugins.integration_test.IntegrationTestPlugin")); + SharedPreferencesPlugin.registerWith( + registrarFor("io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin")); + InAppPurchasePlugin.registerWith( + registrarFor("io.flutter.plugins.inapppurchase.InAppPurchasePlugin")); + } +} diff --git a/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/EmbeddingV1ActivityTest.java b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/EmbeddingV1ActivityTest.java new file mode 100644 index 000000000000..55d97a658ec0 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/EmbeddingV1ActivityTest.java @@ -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. + +package io.flutter.plugins.inapppurchaseexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@RunWith(FlutterTestRunner.class) +@SuppressWarnings("deprecation") +public class EmbeddingV1ActivityTest { + @Rule + public ActivityTestRule rule = + new ActivityTestRule<>(EmbeddingV1Activity.class); +} diff --git a/packages/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/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java new file mode 100644 index 000000000000..a60599573d57 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.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.inapppurchaseexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@RunWith(FlutterTestRunner.class) +public class FlutterActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/in_app_purchase/example/android/app/src/main/res/drawable/launch_background.xml b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from packages/in_app_purchase/example/android/app/src/main/res/drawable/launch_background.xml rename to packages/in_app_purchase/in_app_purchase/example/android/app/src/main/res/drawable/launch_background.xml diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png old mode 100755 new mode 100644 similarity index 100% rename from packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/in_app_purchase/in_app_purchase/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png old mode 100755 new mode 100644 similarity index 100% rename from packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/in_app_purchase/in_app_purchase/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png old mode 100755 new mode 100644 similarity index 100% rename from packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/in_app_purchase/in_app_purchase/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png old mode 100755 new mode 100644 similarity index 100% rename from packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/in_app_purchase/in_app_purchase/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png old mode 100755 new mode 100644 similarity index 100% rename from packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/in_app_purchase/in_app_purchase/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/packages/in_app_purchase/example/android/app/src/main/res/values/styles.xml b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/res/values/styles.xml similarity index 100% rename from packages/in_app_purchase/example/android/app/src/main/res/values/styles.xml rename to packages/in_app_purchase/in_app_purchase/example/android/app/src/main/res/values/styles.xml diff --git a/packages/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 similarity index 100% rename from packages/in_app_purchase/example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker rename to packages/in_app_purchase/in_app_purchase/example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/packages/in_app_purchase/in_app_purchase/example/android/build.gradle b/packages/in_app_purchase/in_app_purchase/example/android/build.gradle new file mode 100644 index 000000000000..e101ac08df55 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/android/build.gradle @@ -0,0 +1,29 @@ +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/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/gradle.properties b/packages/in_app_purchase/in_app_purchase/example/android/gradle.properties old mode 100755 new mode 100644 similarity index 100% rename from packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/gradle.properties rename to packages/in_app_purchase/in_app_purchase/example/android/gradle.properties diff --git a/packages/in_app_purchase/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/in_app_purchase/in_app_purchase/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/in_app_purchase/example/android/gradle/wrapper/gradle-wrapper.properties rename to packages/in_app_purchase/in_app_purchase/example/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/in_app_purchase/example/android/keystore.example.properties b/packages/in_app_purchase/in_app_purchase/example/android/keystore.example.properties similarity index 100% rename from packages/in_app_purchase/example/android/keystore.example.properties rename to packages/in_app_purchase/in_app_purchase/example/android/keystore.example.properties diff --git a/packages/in_app_purchase/example/android/settings.gradle b/packages/in_app_purchase/in_app_purchase/example/android/settings.gradle similarity index 100% rename from packages/in_app_purchase/example/android/settings.gradle rename to packages/in_app_purchase/in_app_purchase/example/android/settings.gradle diff --git a/packages/in_app_purchase/in_app_purchase/example/integration_test/in_app_purchase_test.dart b/packages/in_app_purchase/in_app_purchase/example/integration_test/in_app_purchase_test.dart new file mode 100644 index 000000000000..437ee99e9f36 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/integration_test/in_app_purchase_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:flutter_test/flutter_test.dart'; +import 'package:in_app_purchase/in_app_purchase.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('Can create InAppPurchase instance', (WidgetTester tester) async { + final InAppPurchase iapInstance = InAppPurchase.instance; + expect(iapInstance, isNotNull); + }); +} diff --git a/packages/in_app_purchase/example/ios/Flutter/AppFrameworkInfo.plist b/packages/in_app_purchase/in_app_purchase/example/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from packages/in_app_purchase/example/ios/Flutter/AppFrameworkInfo.plist rename to packages/in_app_purchase/in_app_purchase/example/ios/Flutter/AppFrameworkInfo.plist diff --git a/packages/android_alarm_manager/example/ios/Flutter/Debug.xcconfig b/packages/in_app_purchase/in_app_purchase/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/android_alarm_manager/example/ios/Flutter/Debug.xcconfig rename to packages/in_app_purchase/in_app_purchase/example/ios/Flutter/Debug.xcconfig diff --git a/packages/android_alarm_manager/example/ios/Flutter/Release.xcconfig b/packages/in_app_purchase/in_app_purchase/example/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/android_alarm_manager/example/ios/Flutter/Release.xcconfig rename to packages/in_app_purchase/in_app_purchase/example/ios/Flutter/Release.xcconfig diff --git a/packages/in_app_purchase/in_app_purchase/example/ios/Podfile b/packages/in_app_purchase/in_app_purchase/example/ios/Podfile new file mode 100644 index 000000000000..310b9b498ba6 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/ios/Podfile @@ -0,0 +1,39 @@ +# 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/in_app_purchase/in_app_purchase/example/ios/Runner.xcodeproj/project.pbxproj b/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..df13d20ae61d --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,484 @@ +// !$*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 */; }; + 861D0D93B0757D95C8A69620 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = B2AB6BE1D4E2232AB5D4A002 /* 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 */; }; + A5279298219369C600FF69E6 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5279297219369C600FF69E6 /* StoreKit.framework */; }; +/* 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 = ""; }; + 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 = ""; }; + A5279297219369C600FF69E6 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; + ACAF3B1D3B61187149C0FF81 /* Pods-in_app_purchase_pluginTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-in_app_purchase_pluginTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-in_app_purchase_pluginTests/Pods-in_app_purchase_pluginTests.release.xcconfig"; sourceTree = ""; }; + B2AB6BE1D4E2232AB5D4A002 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + BE95F46E12942F78BF67E55B /* 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 = ""; }; + CC2B3FFB29B2574DEDD718A6 /* Pods-in_app_purchase_pluginTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-in_app_purchase_pluginTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-in_app_purchase_pluginTests/Pods-in_app_purchase_pluginTests.debug.xcconfig"; sourceTree = ""; }; + DE7EEEE26E27ACC04BA9951D /* 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 = ""; }; + E20838C66ABCD8667B0BB95D /* libPods-in_app_purchase_pluginTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-in_app_purchase_pluginTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + F6E5D5F926131C4800C68BED /* Configuration.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Configuration.storekit; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 861D0D93B0757D95C8A69620 /* libPods-Runner.a in Frameworks */, + A5279298219369C600FF69E6 /* StoreKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2D4BBB2E0E7B18550E80D50C /* Pods */ = { + isa = PBXGroup; + children = ( + DE7EEEE26E27ACC04BA9951D /* Pods-Runner.debug.xcconfig */, + BE95F46E12942F78BF67E55B /* Pods-Runner.release.xcconfig */, + CC2B3FFB29B2574DEDD718A6 /* Pods-in_app_purchase_pluginTests.debug.xcconfig */, + ACAF3B1D3B61187149C0FF81 /* Pods-in_app_purchase_pluginTests.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 */, + 2D4BBB2E0E7B18550E80D50C /* Pods */, + E4DB99639FAD8ADED6B572FC /* 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 */, + F6E5D5F926131C4800C68BED /* Configuration.storekit */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + E4DB99639FAD8ADED6B572FC /* Frameworks */ = { + isa = PBXGroup; + children = ( + A5279297219369C600FF69E6 /* StoreKit.framework */, + B2AB6BE1D4E2232AB5D4A002 /* libPods-Runner.a */, + E20838C66ABCD8667B0BB95D /* libPods-in_app_purchase_pluginTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 5DF63B80D489A62B306EA07A /* [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 = 1100; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + SystemCapabilities = { + com.apple.InAppPurchase = { + 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 */, + ); + }; +/* 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"; + }; + 5DF63B80D489A62B306EA07A /* [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; + 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; + }; +/* 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/in_app_purchase/in_app_purchase/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/android_alarm_manager/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/packages/android_intent/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/android_intent/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/in_app_purchase/in_app_purchase/example/ios/Runner/AppDelegate.h b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..0681d288bb70 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/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/in_app_purchase/in_app_purchase/example/ios/Runner/AppDelegate.m b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..30b87969f44a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/AppDelegate.m @@ -0,0 +1,17 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "AppDelegate.h" +#include "GeneratedPluginRegistrant.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GeneratedPluginRegistrant registerWithRegistry:self]; + // Override point for customization after application launch. + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png similarity index 100% rename from packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/packages/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from packages/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/packages/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from packages/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/packages/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from packages/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/packages/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from packages/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/packages/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from packages/camera/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/packages/camera/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from packages/camera/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/android_intent/example/ios/Runner/Base.lproj/Main.storyboard b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/android_intent/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Configuration.storekit b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Configuration.storekit new file mode 100644 index 000000000000..4958a846e67d --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Configuration.storekit @@ -0,0 +1,96 @@ +{ + "products" : [ + { + "displayPrice" : "0.99", + "familyShareable" : false, + "internalID" : "AE10D05D", + "localizations" : [ + { + "description" : "A consumable product.", + "displayName" : "Consumable", + "locale" : "en_US" + } + ], + "productID" : "consumable", + "referenceName" : "consumable", + "type" : "Consumable" + }, + { + "displayPrice" : "10.99", + "familyShareable" : false, + "internalID" : "FABCF067", + "localizations" : [ + { + "description" : "An non-consumable product.", + "displayName" : "Upgrade", + "locale" : "en_US" + } + ], + "productID" : "upgrade", + "referenceName" : "upgrade", + "type" : "NonConsumable" + } + ], + "settings" : { + + }, + "subscriptionGroups" : [ + { + "id" : "D0FEE8D8", + "localizations" : [ + + ], + "name" : "Example Subscriptions", + "subscriptions" : [ + { + "adHocOffers" : [ + + ], + "displayPrice" : "3.99", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "922EB597", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "A lower level subscription.", + "displayName" : "Subscription Silver", + "locale" : "en_US" + } + ], + "productID" : "subscription_silver", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "subscription_silver", + "subscriptionGroupID" : "D0FEE8D8", + "type" : "RecurringSubscription" + }, + { + "adHocOffers" : [ + + ], + "displayPrice" : "5.99", + "familyShareable" : false, + "groupNumber" : 2, + "internalID" : "0BC7FF5E", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "A higher level subscription.", + "displayName" : "Subscription Gold", + "locale" : "en_US" + } + ], + "productID" : "subscription_gold", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "subscription_gold", + "subscriptionGroupID" : "D0FEE8D8", + "type" : "RecurringSubscription" + } + ] + } + ], + "version" : { + "major" : 1, + "minor" : 0 + } +} diff --git a/packages/in_app_purchase/example/ios/Runner/Info.plist b/packages/in_app_purchase/in_app_purchase/example/ios/Runner/Info.plist similarity index 100% rename from packages/in_app_purchase/example/ios/Runner/Info.plist rename to packages/in_app_purchase/in_app_purchase/example/ios/Runner/Info.plist 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 new file mode 100644 index 000000000000..f97b9ef5c8a1 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/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/example/lib/consumable_store.dart b/packages/in_app_purchase/in_app_purchase/example/lib/consumable_store.dart new file mode 100644 index 000000000000..4d10a50e1ee8 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/lib/consumable_store.dart @@ -0,0 +1,51 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// 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(); + + /// Adds a consumable with ID `id` to the store. + /// + /// The consumable is only added after the returned Future is complete. + static Future save(String id) { + _writes = _writes.then((void _) => _doSave(id)); + return _writes; + } + + /// Consumes a consumable with ID `id` from the store. + /// + /// The consumable was only consumed after the returned Future is complete. + static Future consume(String id) { + _writes = _writes.then((void _) => _doConsume(id)); + return _writes; + } + + /// 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(); + cached.add(id); + await prefs.setStringList(_kPrefKey, cached); + } + + static Future _doConsume(String id) async { + List cached = await load(); + 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 new file mode 100644 index 000000000000..5429a00125ac --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/lib/main.dart @@ -0,0 +1,462 @@ +// Copyright 2013 The Flutter Authors. All 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/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 '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()); +} + +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 InAppPurchase _inAppPurchase = InAppPurchase.instance; + 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 = + _inAppPurchase.purchaseStream; + _subscription = purchaseUpdated.listen((purchaseDetailsList) { + _listenToPurchaseUpdated(purchaseDetailsList); + }, onDone: () { + _subscription.cancel(); + }, onError: (error) { + // handle error here. + }); + initStoreInfo(); + super.initState(); + } + + Future initStoreInfo() async { + final bool isAvailable = await _inAppPurchase.isAvailable(); + if (!isAvailable) { + setState(() { + _isAvailable = isAvailable; + _products = []; + _purchases = []; + _notFoundIds = []; + _consumables = []; + _purchasePending = false; + _loading = false; + }); + return; + } + + ProductDetailsResponse productDetailResponse = + await _inAppPurchase.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) { + _inAppPurchase.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 + ? Icon(Icons.check) + : 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( + productDetails: productDetails, + applicationUserName: null, + ); + } + + if (productDetails.id == _kConsumableId) { + _inAppPurchase.buyConsumable( + purchaseParam: purchaseParam, + autoConsume: _kAutoConsume || Platform.isIOS); + } else { + _inAppPurchase.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: () => _inAppPurchase.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 (Platform.isAndroid) { + if (!_kAutoConsume && purchaseDetails.productID == _kConsumableId) { + final InAppPurchaseAndroidPlatformAddition androidAddition = + _inAppPurchase.getPlatformAddition< + InAppPurchaseAndroidPlatformAddition>(); + await androidAddition.consumePurchase(purchaseDetails); + } + } + if (purchaseDetails.pendingCompletePurchase) { + await _inAppPurchase.completePurchase(purchaseDetails); + } + } + }); + } + + GooglePlayPurchaseDetails? _getOldSubscription( + ProductDetails productDetails, Map purchases) { + // This is just to demonstrate a subscription upgrade or downgrade. + // This method assumes that you have only 2 subscriptions under a group, 'subscription_silver' & 'subscription_gold'. + // The 'subscription_silver' subscription can be upgraded to 'subscription_gold' and + // the 'subscription_gold' subscription can be downgraded to 'subscription_silver'. + // Please remember to replace the logic of finding the old subscription Id as per your app. + // The old subscription is only required on Android since Apple handles this internally + // by using the subscription group feature in iTunesConnect. + GooglePlayPurchaseDetails? oldSubscription; + if (productDetails.id == _kSilverSubscriptionId && + purchases[_kGoldSubscriptionId] != null) { + oldSubscription = + purchases[_kGoldSubscriptionId] as GooglePlayPurchaseDetails; + } else if (productDetails.id == _kGoldSubscriptionId && + purchases[_kSilverSubscriptionId] != null) { + oldSubscription = + 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 new file mode 100644 index 000000000000..a75aaa689eea --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/pubspec.yaml @@ -0,0 +1,31 @@ +name: in_app_purchase_example +description: Demonstrates how to use the in_app_purchase plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.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 + # 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/in_app_purchase/in_app_purchase/example/test_driver/integration_test.dart b/packages/in_app_purchase/in_app_purchase/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/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/lib/in_app_purchase.dart b/packages/in_app_purchase/in_app_purchase/lib/in_app_purchase.dart new file mode 100644 index 000000000000..4553619af770 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/lib/in_app_purchase.dart @@ -0,0 +1,212 @@ +// Copyright 2013 The Flutter Authors. All 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: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'; + +export 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart' + show + IAPError, + InAppPurchaseException, + ProductDetails, + ProductDetailsResponse, + PurchaseDetails, + PurchaseParam, + PurchaseVerificationData, + PurchaseStatus; + +/// Basic API for making in app purchases across multiple platforms. +class InAppPurchase implements InAppPurchasePlatformAdditionProvider { + InAppPurchase._(); + + static InAppPurchase? _instance; + + /// The instance of the [InAppPurchase] to use. + static InAppPurchase get instance => _getOrCreateInstance(); + + static InAppPurchase _getOrCreateInstance() { + if (_instance != null) { + return _instance!; + } + + if (defaultTargetPlatform == TargetPlatform.android) { + InAppPurchaseAndroidPlatform.registerPlatform(); + } else if (defaultTargetPlatform == TargetPlatform.iOS) { + InAppPurchaseIosPlatform.registerPlatform(); + } + + _instance = InAppPurchase._(); + return _instance!; + } + + @override + T getPlatformAddition() { + return InAppPurchasePlatformAddition.instance as T; + } + + /// Listen to this broadcast stream to get real time update for purchases. + /// + /// This stream will never close as long as the app is active. + /// + /// Purchase updates can happen in several situations: + /// * When a purchase is triggered by user in the app. + /// * When a purchase is triggered by user from the platform-specific store front. + /// * When a purchase is restored on the device by the user in the app. + /// * If a purchase is not completed ([completePurchase] is not called on the + /// purchase object) from the last app session. Purchase updates will happen + /// when a new app session starts instead. + /// + /// IMPORTANT! You must subscribe to this stream as soon as your app launches, + /// preferably before returning your main App Widget in main(). Otherwise you + /// will miss purchase updated made before this stream is subscribed to. + /// + /// We also recommend listening to the stream with one subscription at a given + /// time. If you choose to have multiple subscription at the same time, you + /// should be careful at the fact that each subscription will receive all the + /// events after they start to listen. + Stream> get purchaseStream => + InAppPurchasePlatform.instance.purchaseStream; + + /// Returns `true` if the payment platform is ready and available. + Future isAvailable() => InAppPurchasePlatform.instance.isAvailable(); + + /// Query product details for the given set of IDs. + /// + /// Identifiers in the underlying payment platform, for example, [App Store + /// Connect](https://appstoreconnect.apple.com/) for iOS and [Google Play + /// Console](https://play.google.com/) for Android. + Future queryProductDetails(Set identifiers) => + InAppPurchasePlatform.instance.queryProductDetails(identifiers); + + /// Buy a non consumable product or subscription. + /// + /// Non consumable items can only be bought once. For example, a purchase that + /// unlocks a special content in your app. Subscriptions are also non + /// consumable products. + /// + /// You always need to restore all the non consumable products for user when + /// they switch their phones. + /// + /// This method does not return the result of the purchase. Instead, after + /// triggering this method, purchase updates will be sent to + /// [purchaseStream]. You should [Stream.listen] to [purchaseStream] to get + /// [PurchaseDetails] objects in different [PurchaseDetails.status] and update + /// your UI accordingly. When the [PurchaseDetails.status] is + /// [PurchaseStatus.purchased], [PurchaseStatus.restored] or + /// [PurchaseStatus.error] you should deliver the content or handle the error, + /// then call [completePurchase] to finish the purchasing process. + /// + /// This method does return whether or not the purchase request was initially + /// sent successfully. + /// + /// Consumable items are defined differently by the different underlying + /// payment platforms, and there's no way to query for whether or not the + /// [ProductDetail] is a consumable at runtime. + /// + /// See also: + /// + /// * [buyConsumable], for buying a consumable product. + /// * [restorePurchases], for restoring non consumable products. + /// + /// Calling this method for consumable items will cause unwanted behaviors! + Future buyNonConsumable({required PurchaseParam purchaseParam}) => + InAppPurchasePlatform.instance.buyNonConsumable( + purchaseParam: purchaseParam, + ); + + /// Buy a consumable product. + /// + /// Consumable items can be "consumed" to mark that they've been used and then + /// bought additional times. For example, a health potion. + /// + /// To restore consumable purchases across devices, you should keep track of + /// those purchase on your own server and restore the purchase for your users. + /// Consumed products are no longer considered to be "owned" by payment + /// platforms and will not be delivered by calling [restorePurchases]. + /// + /// Consumable items are defined differently by the different underlying + /// payment platforms, and there's no way to query for whether or not the + /// [ProductDetail] is a consumable at runtime. + /// + /// `autoConsume` is provided as a utility and will instruct the plugin to + /// automatically consume the product after a succesful purchase. + /// `autoConsume` is `true` by default. + /// + /// This method does not return the result of the purchase. Instead, after + /// triggering this method, purchase updates will be sent to + /// [purchaseStream]. You should [Stream.listen] to + /// [purchaseStream] to get [PurchaseDetails] objects in different + /// [PurchaseDetails.status] and update your UI accordingly. When the + /// [PurchaseDetails.status] is [PurchaseStatus.purchased] or + /// [PurchaseStatus.error], you should deliver the content or handle the + /// error, then call [completePurchase] to finish the purchasing process. + /// + /// This method does return whether or not the purchase request was initially + /// sent succesfully. + /// + /// See also: + /// + /// * [buyNonConsumable], for buying a non consumable product or + /// subscription. + /// * [restorePurchases], for restoring non consumable products. + /// + /// Calling this method for non consumable items will cause unwanted + /// behaviors! + Future buyConsumable({ + required PurchaseParam purchaseParam, + bool autoConsume = true, + }) => + InAppPurchasePlatform.instance.buyConsumable( + purchaseParam: purchaseParam, + autoConsume: autoConsume, + ); + + /// Mark that purchased content has been delivered to the user. + /// + /// You are responsible for completing every [PurchaseDetails] whose + /// [PurchaseDetails.status] is [PurchaseStatus.purchased] or + /// [PurchaseStatus.restored]. + /// Completing a [PurchaseStatus.pending] purchase will cause an exception. + /// For convenience, [PurchaseDetails.pendingCompletePurchase] indicates if a + /// purchase is pending for completion. + /// + /// The method will throw a [PurchaseException] when the purchase could not be + /// finished. Depending on the [PurchaseException.errorCode] the developer + /// should try to complete the purchase via this method again, or retry the + /// [completePurchase] method at a later time. If the + /// [PurchaseException.errorCode] indicates you should not retry there might + /// be some issue with the app's code or the configuration of the app in the + /// respective store. The developer is responsible to fix this issue. The + /// [PurchaseException.message] field might provide more information on what + /// went wrong. + Future completePurchase(PurchaseDetails purchase) => + InAppPurchasePlatform.instance.completePurchase(purchase); + + /// Restore all previous purchases. + /// + /// The `applicationUserName` should match whatever was sent in the initial + /// `PurchaseParam`, if anything. If no `applicationUserName` was specified in the initial + /// `PurchaseParam`, use `null`. + /// + /// Restored purchases are delivered through the [purchaseStream] with a + /// status of [PurchaseStatus.restored]. You should listen for these purchases, + /// validate their receipts, deliver the content and mark the purchase complete + /// by calling the [finishPurchase] method for each purchase. + /// + /// This does not return consumed products. If you want to restore unused + /// consumable products, you need to persist consumable product information + /// for your user on your own server. + /// + /// See also: + /// + /// * [refreshPurchaseVerificationData], for reloading failed + /// [PurchaseDetails.verificationData]. + Future restorePurchases({String? applicationUserName}) => + InAppPurchasePlatform.instance.restorePurchases( + applicationUserName: applicationUserName, + ); +} diff --git a/packages/in_app_purchase/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/in_app_purchase/pubspec.yaml new file mode 100644 index 000000000000..aa2e8fcdee6b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/pubspec.yaml @@ -0,0 +1,35 @@ +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 +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 +version: 1.0.5 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +flutter: + plugin: + platforms: + android: + default_package: in_app_purchase_android + ios: + default_package: in_app_purchase_ios + +dependencies: + flutter: + sdk: flutter + in_app_purchase_platform_interface: ^1.0.0 + in_app_purchase_android: ^0.1.0 + in_app_purchase_ios: ^0.1.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + 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 new file mode 100644 index 000000000000..b8c7bd89206b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/test/in_app_purchase_test.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. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:in_app_purchase/in_app_purchase.dart'; +import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +void main() { + group('InAppPurchase', () { + final ProductDetails productDetails = ProductDetails( + id: 'id', + title: 'title', + description: 'description', + price: 'price', + rawPrice: 0.0, + currencyCode: 'currencyCode', + ); + + final PurchaseDetails purchaseDetails = PurchaseDetails( + productID: 'productID', + verificationData: PurchaseVerificationData( + localVerificationData: 'localVerificationData', + serverVerificationData: 'serverVerificationData', + source: 'source', + ), + transactionDate: 'transactionDate', + status: PurchaseStatus.purchased, + ); + + late InAppPurchase inAppPurchase; + late MockInAppPurchasePlatform fakePlatform; + + setUp(() { + debugDefaultTargetPlatformOverride = TargetPlatform.fuchsia; + + fakePlatform = MockInAppPurchasePlatform(); + InAppPurchasePlatform.instance = fakePlatform; + inAppPurchase = InAppPurchase.instance; + }); + + tearDown(() { + // Restore the default target platform + debugDefaultTargetPlatformOverride = null; + }); + + test('isAvailable', () async { + final bool isAvailable = await inAppPurchase.isAvailable(); + expect(isAvailable, true); + expect(fakePlatform.log, [ + isMethodCall('isAvailable', arguments: null), + ]); + }); + + test('purchaseStream', () async { + final bool isEmptyStream = await inAppPurchase.purchaseStream.isEmpty; + expect(isEmptyStream, true); + expect(fakePlatform.log, [ + isMethodCall('purchaseStream', arguments: null), + ]); + }); + + test('queryProductDetails', () async { + final ProductDetailsResponse response = + await inAppPurchase.queryProductDetails(Set()); + expect(response.notFoundIDs.isEmpty, true); + expect(response.productDetails.isEmpty, true); + expect(fakePlatform.log, [ + isMethodCall('queryProductDetails', arguments: null), + ]); + }); + + test('buyNonConsumable', () async { + final bool result = await inAppPurchase.buyNonConsumable( + purchaseParam: PurchaseParam( + productDetails: productDetails, + ), + ); + + expect(result, true); + expect(fakePlatform.log, [ + isMethodCall('buyNonConsumable', arguments: null), + ]); + }); + + test('buyConsumable', () async { + final 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, + }), + ]); + }); + + test('buyConsumable with autoConsume=false', () async { + final purchaseParam = PurchaseParam(productDetails: productDetails); + final bool result = await inAppPurchase.buyConsumable( + purchaseParam: purchaseParam, + autoConsume: false, + ); + + expect(result, true); + expect(fakePlatform.log, [ + isMethodCall('buyConsumable', arguments: { + "purchaseParam": purchaseParam, + "autoConsume": false, + }), + ]); + }); + + test('completePurchase', () async { + await inAppPurchase.completePurchase(purchaseDetails); + + expect(fakePlatform.log, [ + isMethodCall('completePurchase', arguments: null), + ]); + }); + + test('restorePurchases', () async { + await inAppPurchase.restorePurchases(); + + expect(fakePlatform.log, [ + isMethodCall('restorePurchases', arguments: null), + ]); + }); + }); +} + +class MockInAppPurchasePlatform extends Fake + with MockPlatformInterfaceMixin + implements InAppPurchasePlatform { + final List log = []; + + @override + Future isAvailable() { + log.add(MethodCall('isAvailable')); + return Future.value(true); + } + + @override + Stream> get purchaseStream { + log.add(MethodCall('purchaseStream')); + return Stream.empty(); + } + + @override + Future queryProductDetails(Set identifiers) { + log.add(MethodCall('queryProductDetails')); + return Future.value( + ProductDetailsResponse(productDetails: [], notFoundIDs: [])); + } + + @override + Future buyNonConsumable({required PurchaseParam purchaseParam}) { + log.add(MethodCall('buyNonConsumable')); + return Future.value(true); + } + + @override + Future buyConsumable({ + required PurchaseParam purchaseParam, + bool autoConsume = true, + }) { + log.add(MethodCall('buyConsumable', { + "purchaseParam": purchaseParam, + "autoConsume": autoConsume, + })); + return Future.value(true); + } + + @override + Future completePurchase(PurchaseDetails purchase) { + log.add(MethodCall('completePurchase')); + return Future.value(null); + } + + @override + Future restorePurchases({String? applicationUserName}) { + log.add(MethodCall('restorePurchases')); + return Future.value(null); + } +} diff --git a/packages/in_app_purchase/in_app_purchase_android.iml b/packages/in_app_purchase/in_app_purchase_android.iml deleted file mode 100644 index ac5d744d7acc..000000000000 --- a/packages/in_app_purchase/in_app_purchase_android.iml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md new file mode 100644 index 000000000000..61754627a595 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md @@ -0,0 +1,15 @@ +## 0.1.2 + +* Added support for the obfuscatedAccountId and obfuscatedProfileId in the PurchaseWrapper. + +## 0.1.1 + +* Added support to request a list of active subscriptions and non-consumed one-time purchases on Android, through the `InAppPurchaseAndroidPlatformAddition.queryPastPurchases` method. + +## 0.1.0+1 + +* Migrate maven repository from jcenter to mavenCentral. + +## 0.1.0 + +* Initial open-source release. diff --git a/packages/in_app_purchase/in_app_purchase_android/LICENSE b/packages/in_app_purchase/in_app_purchase_android/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_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/in_app_purchase/in_app_purchase_android/README.md b/packages/in_app_purchase/in_app_purchase_android/README.md new file mode 100644 index 000000000000..684dd66d48a2 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/README.md @@ -0,0 +1,48 @@ +# in_app_purchase_android + +The Android implementation of [`in_app_purchase`][1]. + +## Usage + +### Import the package + +This package has been endorsed, meaning that you only need to add `in_app_purchase` +as a dependency in your `pubspec.yaml`. It will be automatically included in your app +when you depend on `package:in_app_purchase`. + +This is what the above means to your `pubspec.yaml`: + +```yaml +... +dependencies: + ... + in_app_purchase: ^0.6.0 + ... +``` + +If you wish to use the Android package only, you can add `in_app_purchase_android` as a +dependency: + +```yaml +... +dependencies: + ... + in_app_purchase_android: ^1.0.0 + ... +``` + +## 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/in_app_purchase \ No newline at end of file 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 new file mode 100644 index 000000000000..5aeb4e7c5e21 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/analysis_options.yaml @@ -0,0 +1 @@ +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 new file mode 100644 index 000000000000..62b7a18d7931 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/android/build.gradle @@ -0,0 +1,48 @@ +group 'io.flutter.plugins.inapppurchase' +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' + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +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' +} diff --git a/packages/in_app_purchase/android/gradle.properties b/packages/in_app_purchase/in_app_purchase_android/android/gradle.properties similarity index 100% rename from packages/in_app_purchase/android/gradle.properties rename to packages/in_app_purchase/in_app_purchase_android/android/gradle.properties diff --git a/packages/in_app_purchase/android/gradle/wrapper/gradle-wrapper.properties b/packages/in_app_purchase/in_app_purchase_android/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/in_app_purchase/android/gradle/wrapper/gradle-wrapper.properties rename to packages/in_app_purchase/in_app_purchase_android/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/in_app_purchase/android/settings.gradle b/packages/in_app_purchase/in_app_purchase_android/android/settings.gradle similarity index 100% rename from packages/in_app_purchase/android/settings.gradle rename to packages/in_app_purchase/in_app_purchase_android/android/settings.gradle diff --git a/packages/in_app_purchase/android/src/main/AndroidManifest.xml b/packages/in_app_purchase/in_app_purchase_android/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/in_app_purchase/android/src/main/AndroidManifest.xml rename to packages/in_app_purchase/in_app_purchase_android/android/src/main/AndroidManifest.xml diff --git a/packages/in_app_purchase/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 similarity index 93% rename from packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java rename to packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactory.java index b320c17aa992..7b21cbf2e6f5 100644 --- a/packages/in_app_purchase/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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/in_app_purchase/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 similarity index 92% rename from packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java rename to packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/BillingClientFactoryImpl.java index 9bfddaf57545..c256d2c59551 100644 --- a/packages/in_app_purchase/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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/in_app_purchase/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 similarity index 98% rename from packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java rename to packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/InAppPurchasePlugin.java index 65904b12da28..e4719f030d53 100644 --- a/packages/in_app_purchase/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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/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 new file mode 100644 index 000000000000..cfcb81ae05b5 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -0,0 +1,382 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +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.fromSkuDetailsList; + +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.os.Bundle; +import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.android.billingclient.api.AcknowledgePurchaseParams; +import com.android.billingclient.api.AcknowledgePurchaseResponseListener; +import com.android.billingclient.api.BillingClient; +import com.android.billingclient.api.BillingClientStateListener; +import com.android.billingclient.api.BillingFlowParams; +import com.android.billingclient.api.BillingFlowParams.ProrationMode; +import com.android.billingclient.api.BillingResult; +import com.android.billingclient.api.ConsumeParams; +import com.android.billingclient.api.ConsumeResponseListener; +import com.android.billingclient.api.PurchaseHistoryRecord; +import com.android.billingclient.api.PurchaseHistoryResponseListener; +import com.android.billingclient.api.SkuDetails; +import com.android.billingclient.api.SkuDetailsParams; +import com.android.billingclient.api.SkuDetailsResponseListener; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** Handles method channel for the plugin. */ +class MethodCallHandlerImpl + implements MethodChannel.MethodCallHandler, Application.ActivityLifecycleCallbacks { + + private static final String TAG = "InAppPurchasePlugin"; + private static final String LOAD_SKU_DOC_URL = + "https://github.com/flutter/plugins/blob/master/packages/in_app_purchase/README.md#loading-products-for-sale"; + + @Nullable private BillingClient billingClient; + private final BillingClientFactory billingClientFactory; + + @Nullable private Activity activity; + private final Context applicationContext; + private final MethodChannel methodChannel; + + private HashMap cachedSkus = new HashMap<>(); + + /** Constructs the MethodCallHandlerImpl */ + MethodCallHandlerImpl( + @Nullable Activity activity, + @NonNull Context applicationContext, + @NonNull MethodChannel methodChannel, + @NonNull BillingClientFactory billingClientFactory) { + this.billingClientFactory = billingClientFactory; + this.applicationContext = applicationContext; + this.activity = activity; + this.methodChannel = methodChannel; + } + + /** + * Sets the activity. Should be called as soon as the the activity is available. When the activity + * becomes unavailable, call this method again with {@code null}. + */ + void setActivity(@Nullable Activity activity) { + this.activity = activity; + } + + @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 (this.activity == activity && this.applicationContext != null) { + ((Application) this.applicationContext).unregisterActivityLifecycleCallbacks(this); + endBillingClientConnection(); + } + } + + @Override + public void onActivityStopped(Activity activity) {} + + void onDetachedFromActivity() { + endBillingClientConnection(); + } + + @Override + public void onMethodCall(MethodCall call, MethodChannel.Result result) { + switch (call.method) { + case InAppPurchasePlugin.MethodNames.IS_READY: + isReady(result); + break; + case InAppPurchasePlugin.MethodNames.START_CONNECTION: + startConnection( + (int) call.argument("handle"), + (boolean) call.argument("enablePendingPurchases"), + result); + break; + case InAppPurchasePlugin.MethodNames.END_CONNECTION: + endConnection(result); + break; + case InAppPurchasePlugin.MethodNames.QUERY_SKU_DETAILS: + List skusList = call.argument("skusList"); + querySkuDetailsAsync((String) call.argument("skuType"), skusList, result); + break; + case InAppPurchasePlugin.MethodNames.LAUNCH_BILLING_FLOW: + launchBillingFlow( + (String) call.argument("sku"), + (String) call.argument("accountId"), + (String) call.argument("obfuscatedProfileId"), + (String) call.argument("oldSku"), + (String) call.argument("purchaseToken"), + call.hasArgument("prorationMode") + ? (int) call.argument("prorationMode") + : ProrationMode.UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY, + result); + break; + case InAppPurchasePlugin.MethodNames.QUERY_PURCHASES: + queryPurchases((String) call.argument("skuType"), result); + break; + case InAppPurchasePlugin.MethodNames.QUERY_PURCHASE_HISTORY_ASYNC: + queryPurchaseHistoryAsync((String) call.argument("skuType"), result); + break; + case InAppPurchasePlugin.MethodNames.CONSUME_PURCHASE_ASYNC: + consumeAsync((String) call.argument("purchaseToken"), result); + break; + case InAppPurchasePlugin.MethodNames.ACKNOWLEDGE_PURCHASE: + acknowledgePurchase((String) call.argument("purchaseToken"), result); + break; + default: + result.notImplemented(); + } + } + + private void endConnection(final MethodChannel.Result result) { + endBillingClientConnection(); + result.success(null); + } + + private void endBillingClientConnection() { + if (billingClient != null) { + billingClient.endConnection(); + billingClient = null; + } + } + + private void isReady(MethodChannel.Result result) { + if (billingClientError(result)) { + return; + } + + result.success(billingClient.isReady()); + } + + private void querySkuDetailsAsync( + final String skuType, final List skusList, final MethodChannel.Result result) { + if (billingClientError(result)) { + return; + } + + SkuDetailsParams params = + SkuDetailsParams.newBuilder().setType(skuType).setSkusList(skusList).build(); + billingClient.querySkuDetailsAsync( + params, + new SkuDetailsResponseListener() { + @Override + public void onSkuDetailsResponse( + BillingResult billingResult, List skuDetailsList) { + updateCachedSkus(skuDetailsList); + final Map skuDetailsResponse = new HashMap<>(); + skuDetailsResponse.put("billingResult", Translator.fromBillingResult(billingResult)); + skuDetailsResponse.put("skuDetailsList", fromSkuDetailsList(skuDetailsList)); + result.success(skuDetailsResponse); + } + }); + } + + private void launchBillingFlow( + String sku, + @Nullable String accountId, + @Nullable String obfuscatedProfileId, + @Nullable String oldSku, + @Nullable String purchaseToken, + int prorationMode, + MethodChannel.Result result) { + if (billingClientError(result)) { + return; + } + + SkuDetails skuDetails = cachedSkus.get(sku); + if (skuDetails == null) { + result.error( + "NOT_FOUND", + String.format( + "Details for sku %s are not available. It might because skus were not fetched prior to the call. Please fetch the skus first. An example of how to fetch the skus could be found here: %s", + sku, LOAD_SKU_DOC_URL), + null); + return; + } + + if (oldSku == null + && prorationMode != ProrationMode.UNKNOWN_SUBSCRIPTION_UPGRADE_DOWNGRADE_POLICY) { + result.error( + "IN_APP_PURCHASE_REQUIRE_OLD_SKU", + "launchBillingFlow failed because oldSku is null. You must provide a valid oldSku in order to use a proration mode.", + null); + return; + } else if (oldSku != null && !cachedSkus.containsKey(oldSku)) { + result.error( + "IN_APP_PURCHASE_INVALID_OLD_SKU", + String.format( + "Details for sku %s are not available. It might because skus were not fetched prior to the call. Please fetch the skus first. An example of how to fetch the skus could be found here: %s", + oldSku, LOAD_SKU_DOC_URL), + null); + return; + } + + if (activity == null) { + result.error( + "ACTIVITY_UNAVAILABLE", + "Details for sku " + + sku + + " are not available. This method must be run with the app in foreground.", + null); + return; + } + + BillingFlowParams.Builder paramsBuilder = + BillingFlowParams.newBuilder().setSkuDetails(skuDetails); + if (accountId != null && !accountId.isEmpty()) { + paramsBuilder.setObfuscatedAccountId(accountId); + } + if (obfuscatedProfileId != null && !obfuscatedProfileId.isEmpty()) { + paramsBuilder.setObfuscatedProfileId(obfuscatedProfileId); + } + if (oldSku != null && !oldSku.isEmpty()) { + paramsBuilder.setOldSku(oldSku, 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 + paramsBuilder.setReplaceSkusProrationMode(prorationMode); + result.success( + Translator.fromBillingResult( + billingClient.launchBillingFlow(activity, paramsBuilder.build()))); + } + + private void consumeAsync(String purchaseToken, final MethodChannel.Result result) { + if (billingClientError(result)) { + return; + } + + ConsumeResponseListener listener = + new ConsumeResponseListener() { + @Override + public void onConsumeResponse(BillingResult billingResult, String outToken) { + result.success(Translator.fromBillingResult(billingResult)); + } + }; + ConsumeParams.Builder paramsBuilder = + ConsumeParams.newBuilder().setPurchaseToken(purchaseToken); + + ConsumeParams params = paramsBuilder.build(); + + billingClient.consumeAsync(params, listener); + } + + private void queryPurchases(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))); + } + + private void queryPurchaseHistoryAsync(String skuType, final MethodChannel.Result result) { + if (billingClientError(result)) { + return; + } + + billingClient.queryPurchaseHistoryAsync( + skuType, + new PurchaseHistoryResponseListener() { + @Override + public void onPurchaseHistoryResponse( + BillingResult billingResult, List purchasesList) { + final Map serialized = new HashMap<>(); + serialized.put("billingResult", Translator.fromBillingResult(billingResult)); + serialized.put( + "purchaseHistoryRecordList", fromPurchaseHistoryRecordList(purchasesList)); + result.success(serialized); + } + }); + } + + private void startConnection( + final int handle, final boolean enablePendingPurchases, final MethodChannel.Result result) { + if (billingClient == null) { + billingClient = + billingClientFactory.createBillingClient( + applicationContext, methodChannel, enablePendingPurchases); + } + + billingClient.startConnection( + new BillingClientStateListener() { + private boolean alreadyFinished = false; + + @Override + public void onBillingSetupFinished(BillingResult billingResult) { + if (alreadyFinished) { + Log.d(TAG, "Tried to call onBilllingSetupFinished multiple times."); + return; + } + alreadyFinished = true; + // Consider the fact that we've finished a success, leave it to the Dart side to + // validate the responseCode. + result.success(Translator.fromBillingResult(billingResult)); + } + + @Override + public void onBillingServiceDisconnected() { + final Map arguments = new HashMap<>(); + arguments.put("handle", handle); + methodChannel.invokeMethod(InAppPurchasePlugin.MethodNames.ON_DISCONNECT, arguments); + } + }); + } + + private void acknowledgePurchase(String purchaseToken, final MethodChannel.Result result) { + if (billingClientError(result)) { + return; + } + AcknowledgePurchaseParams params = + AcknowledgePurchaseParams.newBuilder().setPurchaseToken(purchaseToken).build(); + billingClient.acknowledgePurchase( + params, + new AcknowledgePurchaseResponseListener() { + @Override + public void onAcknowledgePurchaseResponse(BillingResult billingResult) { + result.success(Translator.fromBillingResult(billingResult)); + } + }); + } + + private void updateCachedSkus(@Nullable List skuDetailsList) { + if (skuDetailsList == null) { + return; + } + + for (SkuDetails skuDetails : skuDetailsList) { + cachedSkus.put(skuDetails.getSku(), skuDetails); + } + } + + private boolean billingClientError(MethodChannel.Result result) { + if (billingClient != null) { + return false; + } + + result.error("UNAVAILABLE", "BillingClient is unset. Try reconnecting.", null); + return true; + } +} diff --git a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/PluginPurchaseListener.java b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/PluginPurchaseListener.java similarity index 95% rename from packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/PluginPurchaseListener.java rename to packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/PluginPurchaseListener.java index 20ab8ad92e65..54c775d0ad0f 100644 --- a/packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/PluginPurchaseListener.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/PluginPurchaseListener.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/in_app_purchase/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 similarity index 90% rename from packages/in_app_purchase/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java rename to packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/Translator.java index 80b6f1362255..079c18ab8b5c 100644 --- a/packages/in_app_purchase/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 @@ -1,10 +1,11 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. package io.flutter.plugins.inapppurchase; import androidx.annotation.Nullable; +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; @@ -31,7 +32,6 @@ static HashMap fromSkuDetail(SkuDetails detail) { info.put("priceCurrencyCode", detail.getPriceCurrencyCode()); info.put("sku", detail.getSku()); info.put("type", detail.getType()); - info.put("isRewarded", detail.isRewarded()); info.put("subscriptionPeriod", detail.getSubscriptionPeriod()); info.put("originalPrice", detail.getOriginalPrice()); info.put("originalPriceAmountMicros", detail.getOriginalPriceAmountMicros()); @@ -64,6 +64,11 @@ static HashMap fromPurchase(Purchase purchase) { info.put("developerPayload", purchase.getDeveloperPayload()); info.put("isAcknowledged", purchase.isAcknowledged()); info.put("purchaseState", purchase.getPurchaseState()); + AccountIdentifiers accountIdentifiers = purchase.getAccountIdentifiers(); + if (accountIdentifiers != null) { + info.put("obfuscatedAccountId", accountIdentifiers.getObfuscatedAccountId()); + info.put("obfuscatedProfileId", accountIdentifiers.getObfuscatedProfileId()); + } return info; } diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/android/text/TextUtils.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/android/text/TextUtils.java new file mode 100644 index 000000000000..d997ae1dcaa0 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/android/text/TextUtils.java @@ -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. + +package android.text; + +public class TextUtils { + public static boolean isEmpty(CharSequence str) { + return str == null || str.length() == 0; + } +} diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/android/util/Log.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/android/util/Log.java new file mode 100644 index 000000000000..310b9ad89cdf --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/android/util/Log.java @@ -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. + +package android.util; + +public class Log { + public static int d(String tag, String msg) { + System.out.println("DEBUG: " + tag + ": " + msg); + return 0; + } + + public static int i(String tag, String msg) { + System.out.println("INFO: " + tag + ": " + msg); + return 0; + } + + public static int w(String tag, String msg) { + System.out.println("WARN: " + tag + ": " + msg); + return 0; + } + + public static int e(String tag, String msg) { + System.out.println("ERROR: " + tag + ": " + msg); + return 0; + } +} diff --git a/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/InAppPurchasePluginTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/InAppPurchasePluginTest.java similarity index 95% rename from packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/InAppPurchasePluginTest.java rename to packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/InAppPurchasePluginTest.java index 0befa87e1d05..bcee5428eac9 100644 --- a/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/InAppPurchasePluginTest.java +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/InAppPurchasePluginTest.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/in_app_purchase/example/android/app/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 similarity index 76% rename from packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java rename to packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java index c6a9b4114a75..4d7a02220cf5 100644 --- a/packages/in_app_purchase/example/android/app/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 @@ -1,3 +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. + package io.flutter.plugins.inapppurchase; import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.ACKNOWLEDGE_PURCHASE; @@ -18,9 +22,11 @@ import static io.flutter.plugins.inapppurchase.Translator.fromSkuDetailsList; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; +import static java.util.Collections.unmodifiableList; import static java.util.stream.Collectors.toList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.contains; import static org.mockito.ArgumentMatchers.eq; @@ -59,6 +65,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.json.JSONException; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -78,7 +85,7 @@ public class MethodCallHandlerTest { @Before public void setUp() { - MockitoAnnotations.initMocks(this); + MockitoAnnotations.openMocks(this); factory = (@NonNull Context context, @NonNull MethodChannel channel, @@ -260,14 +267,18 @@ public void querySkuDetailsAsync_clientDisconnected() { verify(result, never()).success(any()); } + // Test launchBillingFlow not crash if `accountId` is `null` + // Ideally, we should check if the `accountId` is null in the parameter; however, + // since PBL 3.0, the `accountId` variable is not public. @Test - public void launchBillingFlow_ok_nullAccountId() { + public void launchBillingFlow_null_AccountId_do_not_crash() { // Fetch the sku details first and then prepare the launch billing flow call String skuId = "foo"; queryForSkus(singletonList(skuId)); HashMap arguments = new HashMap<>(); arguments.put("sku", skuId); arguments.put("accountId", null); + arguments.put("obfuscatedProfileId", null); MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); // Launch the billing flow @@ -285,13 +296,45 @@ public void launchBillingFlow_ok_nullAccountId() { verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); BillingFlowParams params = billingFlowParamsCaptor.getValue(); assertEquals(params.getSku(), skuId); - assertNull(params.getAccountId()); // Verify we pass the response code to result verify(result, never()).error(any(), any(), any()); verify(result, times(1)).success(fromBillingResult(billingResult)); } + @Test + public void launchBillingFlow_ok_null_OldSku() { + // Fetch the sku details first and then prepare the launch billing flow call + String skuId = "foo"; + String accountId = "account"; + queryForSkus(singletonList(skuId)); + HashMap arguments = new HashMap<>(); + arguments.put("sku", skuId); + arguments.put("accountId", accountId); + arguments.put("oldSku", null); + MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); + + // Launch the billing flow + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); + methodChannelHandler.onMethodCall(launchCall, result); + + // Verify we pass the arguments to the billing flow + ArgumentCaptor billingFlowParamsCaptor = + ArgumentCaptor.forClass(BillingFlowParams.class); + verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); + BillingFlowParams params = billingFlowParamsCaptor.getValue(); + assertEquals(params.getSku(), skuId); + assertNull(params.getOldSku()); + // Verify we pass the response code to result + verify(result, never()).error(any(), any(), any()); + verify(result, times(1)).success(fromBillingResult(billingResult)); + } + @Test public void launchBillingFlow_ok_null_Activity() { methodChannelHandler.setActivity(null); @@ -311,6 +354,41 @@ public void launchBillingFlow_ok_null_Activity() { verify(result, never()).success(any()); } + @Test + public void launchBillingFlow_ok_oldSku() { + // Fetch the sku details first and query the method call + String skuId = "foo"; + String accountId = "account"; + String oldSkuId = "oldFoo"; + queryForSkus(unmodifiableList(asList(skuId, oldSkuId))); + HashMap arguments = new HashMap<>(); + arguments.put("sku", skuId); + arguments.put("accountId", accountId); + arguments.put("oldSku", oldSkuId); + MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); + + // Launch the billing flow + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); + methodChannelHandler.onMethodCall(launchCall, result); + + // Verify we pass the arguments to the billing flow + ArgumentCaptor billingFlowParamsCaptor = + ArgumentCaptor.forClass(BillingFlowParams.class); + verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); + BillingFlowParams params = billingFlowParamsCaptor.getValue(); + assertEquals(params.getSku(), skuId); + assertEquals(params.getOldSku(), oldSkuId); + + // Verify we pass the response code to result + verify(result, never()).error(any(), any(), any()); + verify(result, times(1)).success(fromBillingResult(billingResult)); + } + @Test public void launchBillingFlow_ok_AccountId() { // Fetch the sku details first and query the method call @@ -337,13 +415,87 @@ public void launchBillingFlow_ok_AccountId() { verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); BillingFlowParams params = billingFlowParamsCaptor.getValue(); assertEquals(params.getSku(), skuId); - assertEquals(params.getAccountId(), accountId); // Verify we pass the response code to result verify(result, never()).error(any(), any(), any()); verify(result, times(1)).success(fromBillingResult(billingResult)); } + @Test + public void launchBillingFlow_ok_Proration() { + // 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_PRORATED_PRICE; + queryForSkus(unmodifiableList(asList(skuId, oldSkuId))); + HashMap arguments = new HashMap<>(); + arguments.put("sku", skuId); + arguments.put("accountId", accountId); + arguments.put("oldSku", oldSkuId); + arguments.put("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(); + 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()); + verify(result, times(1)).success(fromBillingResult(billingResult)); + } + + @Test + public void launchBillingFlow_ok_Proration_with_null_OldSku() { + // Fetch the sku details first and query the method call + String skuId = "foo"; + String accountId = "account"; + String queryOldSkuId = "oldFoo"; + String oldSkuId = null; + int prorationMode = BillingFlowParams.ProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE; + queryForSkus(unmodifiableList(asList(skuId, queryOldSkuId))); + HashMap arguments = new HashMap<>(); + arguments.put("sku", skuId); + arguments.put("accountId", accountId); + arguments.put("oldSku", oldSkuId); + arguments.put("prorationMode", prorationMode); + MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); + + // Launch the billing flow + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); + methodChannelHandler.onMethodCall(launchCall, result); + + // Assert that we sent an error back. + verify(result) + .error( + contains("IN_APP_PURCHASE_REQUIRE_OLD_SKU"), + contains("launchBillingFlow failed because oldSku is null"), + any()); + verify(result, never()).success(any()); + } + @Test public void launchBillingFlow_clientDisconnected() { // Prepare the launch call after disconnecting the client @@ -381,6 +533,27 @@ public void launchBillingFlow_skuNotFound() { verify(result, never()).success(any()); } + @Test + public void launchBillingFlow_oldSkuNotFound() { + // Try to launch the billing flow for a random sku ID + establishConnectedBillingClient(null, null); + String skuId = "foo"; + String accountId = "account"; + String oldSkuId = "oldSku"; + queryForSkus(singletonList(skuId)); + HashMap arguments = new HashMap<>(); + arguments.put("sku", skuId); + arguments.put("accountId", accountId); + arguments.put("oldSku", oldSkuId); + MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); + + methodChannelHandler.onMethodCall(launchCall, result); + + // Assert that we sent an error back. + verify(result).error(contains("IN_APP_PURCHASE_INVALID_OLD_SKU"), contains(oldSkuId), any()); + verify(result, never()).success(any()); + } + @Test public void queryPurchases() { establishConnectedBillingClient(null, null); @@ -503,11 +676,7 @@ public void consumeAsync() { methodChannelHandler.onMethodCall(new MethodCall(CONSUME_PURCHASE_ASYNC, arguments), result); - ConsumeParams params = - ConsumeParams.newBuilder() - .setDeveloperPayload("mockPayload") - .setPurchaseToken("mockToken") - .build(); + ConsumeParams params = ConsumeParams.newBuilder().setPurchaseToken("mockToken").build(); // Verify we pass the data to result verify(mockBillingClient).consumeAsync(refEq(params), listenerCaptor.capture()); @@ -538,10 +707,7 @@ public void acknowledgePurchase() { methodChannelHandler.onMethodCall(new MethodCall(ACKNOWLEDGE_PURCHASE, arguments), result); AcknowledgePurchaseParams params = - AcknowledgePurchaseParams.newBuilder() - .setDeveloperPayload("mockPayload") - .setPurchaseToken("mockToken") - .build(); + AcknowledgePurchaseParams.newBuilder().setPurchaseToken("mockToken").build(); // Verify we pass the data to result verify(mockBillingClient).acknowledgePurchase(refEq(params), listenerCaptor.capture()); @@ -609,6 +775,7 @@ private void queryForSkus(List skusList) { verify(mockBillingClient).querySkuDetailsAsync(any(), listenerCaptor.capture()); List skuDetailsResponse = skusList.stream().map(this::buildSkuDetails).collect(toList()); + BillingResult billingResult = BillingResult.newBuilder() .setResponseCode(100) @@ -618,8 +785,16 @@ private void queryForSkus(List skusList) { } private SkuDetails buildSkuDetails(String id) { - SkuDetails details = mock(SkuDetails.class); - when(details.getSku()).thenReturn(id); + String json = + String.format( + "{\"packageName\": \"dummyPackageName\",\"productId\":\"%s\",\"type\":\"inapp\",\"price\":\"$0.99\",\"price_amount_micros\":990000,\"price_currency_code\":\"USD\",\"title\":\"Example title\",\"description\":\"Example description.\",\"original_price\":\"$0.99\",\"original_price_micros\":990000}", + id); + SkuDetails details = null; + try { + details = new SkuDetails(json); + } catch (JSONException e) { + fail("buildSkuDetails failed with JSONException " + e.toString()); + } return details; } diff --git a/packages/in_app_purchase/example/android/app/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 similarity index 86% rename from packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java rename to packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/TranslatorTest.java index 2ee1044fe0c5..e65afcf42467 100644 --- a/packages/in_app_purchase/example/android/app/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 @@ -1,13 +1,17 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. package io.flutter.plugins.inapppurchase; import static org.junit.Assert.assertEquals; +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 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; @@ -26,7 +30,7 @@ public class TranslatorTest { private static final String SKU_DETAIL_EXAMPLE_JSON = "{\"productId\":\"example\",\"type\":\"inapp\",\"price\":\"$0.99\",\"price_amount_micros\":990000,\"price_currency_code\":\"USD\",\"title\":\"Example title\",\"description\":\"Example description.\",\"original_price\":\"$0.99\",\"original_price_micros\":990000}"; private static final String PURCHASE_EXAMPLE_JSON = - "{\"orderId\":\"foo\",\"packageName\":\"bar\",\"productId\":\"consumable\",\"purchaseTime\":11111111,\"purchaseState\":0,\"purchaseToken\":\"baz\",\"developerPayload\":\"dummy payload\",\"isAcknowledged\":\"true\"}"; + "{\"orderId\":\"foo\",\"packageName\":\"bar\",\"productId\":\"consumable\",\"purchaseTime\":11111111,\"purchaseState\":0,\"purchaseToken\":\"baz\",\"developerPayload\":\"dummy payload\",\"isAcknowledged\":\"true\", \"obfuscatedAccountId\":\"Account101\", \"obfuscatedProfileId\": \"Profile105\"}"; @Test public void fromSkuDetail() throws JSONException { @@ -63,6 +67,16 @@ public void fromPurchase() throws JSONException { assertSerialized(expected, Translator.fromPurchase(expected)); } + @Test + public void fromPurchaseWithoutAccountIds() throws JSONException { + final Purchase expected = + new PurchaseWithoutAccountIdentifiers(PURCHASE_EXAMPLE_JSON, "signature"); + Map serialized = Translator.fromPurchase(expected); + assertNotNull(serialized.get("orderId")); + assertNull(serialized.get("obfuscatedProfileId")); + assertNull(serialized.get("obfuscatedAccountId")); + } + @Test public void fromPurchaseHistoryRecord() throws JSONException { final PurchaseHistoryRecord expected = @@ -200,6 +214,14 @@ private void assertSerialized(Purchase expected, Map serialized) assertEquals(expected.getDeveloperPayload(), serialized.get("developerPayload")); assertEquals(expected.isAcknowledged(), serialized.get("isAcknowledged")); assertEquals(expected.getPurchaseState(), serialized.get("purchaseState")); + assertNotNull(expected.getAccountIdentifiers().getObfuscatedAccountId()); + assertEquals( + expected.getAccountIdentifiers().getObfuscatedAccountId(), + serialized.get("obfuscatedAccountId")); + assertNotNull(expected.getAccountIdentifiers().getObfuscatedProfileId()); + assertEquals( + expected.getAccountIdentifiers().getObfuscatedProfileId(), + serialized.get("obfuscatedProfileId")); } private void assertSerialized(PurchaseHistoryRecord expected, Map serialized) { @@ -211,3 +233,15 @@ private void assertSerialized(PurchaseHistoryRecord expected, Map + localProperties.load(reader) + } +} + +// Load the build signing secrets from a local `keystore.properties` file. +// TODO(YOU): Create release keys and a `keystore.properties` file. See +// `example/README.md` for more info and `keystore.example.properties` for an +// example. +def keystorePropertiesFile = rootProject.file("keystore.properties") +def keystoreProperties = new Properties() +def configured = true +try { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} catch (IOException e) { + configured = false + logger.error('Release signing information not found.') +} + +project.ext { + // TODO(YOU): Create release keys and a `keystore.properties` file. See + // `example/README.md` for more info and `keystore.example.properties` for an + // example. + APP_ID = configured ? keystoreProperties['appId'] : "io.flutter.plugins.inapppurchaseexample.DEFAULT_DO_NOT_USE" + KEYSTORE_STORE_FILE = configured ? rootProject.file(keystoreProperties['storeFile']) : null + KEYSTORE_STORE_PASSWORD = keystoreProperties['storePassword'] + KEYSTORE_KEY_ALIAS = keystoreProperties['keyAlias'] + KEYSTORE_KEY_PASSWORD = keystoreProperties['keyPassword'] + VERSION_CODE = configured ? keystoreProperties['versionCode'].toInteger() : 1 + VERSION_NAME = configured ? keystoreProperties['versionName'] : "0.0.1" +} + +if (project.APP_ID == "io.flutter.plugins.inapppurchaseexample.DEFAULT_DO_NOT_USE") { + configured = false + logger.error('Unique package name not set, defaulting to "io.flutter.plugins.inapppurchaseexample.DEFAULT_DO_NOT_USE".') +} + +// Log a final error message if we're unable to create a release key signed +// build for an app configured in the Play Developer Console. Apks built in this +// condition won't be able to call any of the BillingClient APIs. +if (!configured) { + logger.error('The app could not be configured for release signing. In app purchases will not be testable. See `example/README.md` for more info and instructions.') +} + +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.") +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + signingConfigs { + release { + storeFile project.KEYSTORE_STORE_FILE + storePassword project.KEYSTORE_STORE_PASSWORD + keyAlias project.KEYSTORE_KEY_ALIAS + keyPassword project.KEYSTORE_KEY_PASSWORD + } + } + + compileSdkVersion 29 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + applicationId project.APP_ID + minSdkVersion 16 + targetSdkVersion 29 + versionCode project.VERSION_CODE + versionName project.VERSION_NAME + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + // Google Play Billing APIs only work with apps signed for production. + debug { + if (configured) { + signingConfig signingConfigs.release + } else { + signingConfig signingConfigs.debug + } + } + release { + if (configured) { + signingConfig signingConfigs.release + } else { + signingConfig signingConfigs.debug + } + } + } + + testOptions { + unitTests.returnDefaultValues = true + } +} + +flutter { + source '../..' +} + +dependencies { + implementation 'com.android.billingclient:billing:3.0.2' + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-core:3.6.0' + testImplementation 'org.json:json:20180813' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' +} diff --git a/packages/in_app_purchase/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/in_app_purchase/in_app_purchase_android/example/android/app/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/in_app_purchase/example/android/app/gradle/wrapper/gradle-wrapper.properties rename to packages/in_app_purchase/in_app_purchase_android/example/android/app/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/AndroidManifest.xml b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..a17382b97d83 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/EmbeddingV1Activity.java b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/EmbeddingV1Activity.java new file mode 100644 index 000000000000..c74ad9447e81 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/EmbeddingV1Activity.java @@ -0,0 +1,24 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.inapppurchaseexample; + +import android.os.Bundle; +import dev.flutter.plugins.integration_test.IntegrationTestPlugin; +import io.flutter.plugins.inapppurchase.InAppPurchasePlugin; +import io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin; + +@SuppressWarnings("deprecation") +public class EmbeddingV1Activity extends io.flutter.app.FlutterActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + IntegrationTestPlugin.registerWith( + registrarFor("dev.flutter.plugins.integration_test.IntegrationTestPlugin")); + SharedPreferencesPlugin.registerWith( + registrarFor("io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin")); + InAppPurchasePlugin.registerWith( + registrarFor("io.flutter.plugins.inapppurchase.InAppPurchasePlugin")); + } +} diff --git a/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/EmbeddingV1ActivityTest.java b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/EmbeddingV1ActivityTest.java new file mode 100644 index 000000000000..55d97a658ec0 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/EmbeddingV1ActivityTest.java @@ -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. + +package io.flutter.plugins.inapppurchaseexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@RunWith(FlutterTestRunner.class) +@SuppressWarnings("deprecation") +public class EmbeddingV1ActivityTest { + @Rule + public ActivityTestRule rule = + new ActivityTestRule<>(EmbeddingV1Activity.class); +} diff --git a/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.java new file mode 100644 index 000000000000..a60599573d57 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/FlutterActivityTest.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.inapppurchaseexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@RunWith(FlutterTestRunner.class) +public class FlutterActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/integration_test/example/android/app/src/main/res/drawable/launch_background.xml b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from packages/integration_test/example/android/app/src/main/res/drawable/launch_background.xml rename to packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/res/drawable/launch_background.xml diff --git a/packages/in_app_purchase/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from packages/in_app_purchase/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/in_app_purchase/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from packages/in_app_purchase/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/in_app_purchase/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from packages/in_app_purchase/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/in_app_purchase/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from packages/in_app_purchase/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/in_app_purchase/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from packages/in_app_purchase/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/packages/integration_test/example/android/app/src/main/res/values/styles.xml b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/res/values/styles.xml similarity index 100% rename from packages/integration_test/example/android/app/src/main/res/values/styles.xml rename to packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/res/values/styles.xml diff --git a/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000000..1f0955d450f0 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/packages/in_app_purchase/in_app_purchase_android/example/android/build.gradle b/packages/in_app_purchase/in_app_purchase_android/example/android/build.gradle new file mode 100644 index 000000000000..e101ac08df55 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/example/android/build.gradle @@ -0,0 +1,29 @@ +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/in_app_purchase/example/android/gradle.properties b/packages/in_app_purchase/in_app_purchase_android/example/android/gradle.properties similarity index 100% rename from packages/in_app_purchase/example/android/gradle.properties rename to packages/in_app_purchase/in_app_purchase_android/example/android/gradle.properties diff --git a/packages/in_app_purchase/in_app_purchase_android/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/in_app_purchase/in_app_purchase_android/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..2819f022f1fd --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_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-4.10.2-all.zip diff --git a/packages/in_app_purchase/in_app_purchase_android/example/android/keystore.example.properties b/packages/in_app_purchase/in_app_purchase_android/example/android/keystore.example.properties new file mode 100644 index 000000000000..ccbbb3653569 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/example/android/keystore.example.properties @@ -0,0 +1,7 @@ +storePassword=??? +keyPassword=??? +keyAlias=??? +storeFile=??? +appId=io.flutter.plugins.inapppurchaseexample.DEFAULT_DO_NOT_USE +versionCode=1 +versionName=0.0.1 \ No newline at end of file diff --git a/packages/integration_test/example/android/settings.gradle b/packages/in_app_purchase/in_app_purchase_android/example/android/settings.gradle similarity index 100% rename from packages/integration_test/example/android/settings.gradle rename to packages/in_app_purchase/in_app_purchase_android/example/android/settings.gradle diff --git a/packages/in_app_purchase/in_app_purchase_android/example/integration_test/in_app_purchase_test.dart b/packages/in_app_purchase/in_app_purchase_android/example/integration_test/in_app_purchase_test.dart new file mode 100644 index 000000000000..8b655306a2b5 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/example/integration_test/in_app_purchase_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:in_app_purchase_android/in_app_purchase_android.dart'; +import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('Can create InAppPurchaseAndroid instance', + (WidgetTester tester) async { + InAppPurchaseAndroidPlatformAddition.enablePendingPurchases(); + InAppPurchaseAndroidPlatform.registerPlatform(); + final InAppPurchasePlatform androidPlatform = + InAppPurchasePlatform.instance; + expect(androidPlatform, isNotNull); + }); +} diff --git a/packages/in_app_purchase/in_app_purchase_android/example/lib/consumable_store.dart b/packages/in_app_purchase/in_app_purchase_android/example/lib/consumable_store.dart new file mode 100644 index 000000000000..4d10a50e1ee8 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/example/lib/consumable_store.dart @@ -0,0 +1,51 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// 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(); + + /// Adds a consumable with ID `id` to the store. + /// + /// The consumable is only added after the returned Future is complete. + static Future save(String id) { + _writes = _writes.then((void _) => _doSave(id)); + return _writes; + } + + /// Consumes a consumable with ID `id` from the store. + /// + /// The consumable was only consumed after the returned Future is complete. + static Future consume(String id) { + _writes = _writes.then((void _) => _doConsume(id)); + return _writes; + } + + /// 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(); + cached.add(id); + await prefs.setStringList(_kPrefKey, cached); + } + + static Future _doConsume(String id) async { + List cached = await load(); + 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 new file mode 100644 index 000000000000..c5726c4ade76 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/example/lib/main.dart @@ -0,0 +1,436 @@ +// Copyright 2013 The Flutter Authors. All 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_android/billing_client_wrappers.dart'; +import 'package:in_app_purchase_android/in_app_purchase_android.dart'; +import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; + +import '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(); + + 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 InAppPurchasePlatform _inAppPurchasePlatform = + InAppPurchasePlatform.instance; + 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 = + _inAppPurchasePlatform.purchaseStream; + _subscription = purchaseUpdated.listen((purchaseDetailsList) { + _listenToPurchaseUpdated(purchaseDetailsList); + }, onDone: () { + _subscription.cancel(); + }, onError: (error) { + // handle error here. + }); + initStoreInfo(); + super.initState(); + } + + Future initStoreInfo() async { + final bool isAvailable = await _inAppPurchasePlatform.isAvailable(); + if (!isAvailable) { + setState(() { + _isAvailable = isAvailable; + _products = []; + _purchases = []; + _notFoundIds = []; + _consumables = []; + _purchasePending = false; + _loading = false; + }); + return; + } + + ProductDetailsResponse productDetailResponse = + await _inAppPurchasePlatform.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; + } + + await _inAppPurchasePlatform.restorePurchases(); + + 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(), + ], + ), + ); + } 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) { + _inAppPurchasePlatform.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 + ? Icon(Icons.check) + : TextButton( + child: Text(productDetails.price), + style: TextButton.styleFrom( + backgroundColor: Colors.green[800], + primary: Colors.white, + ), + onPressed: () { + // NOTE: If you are making a subscription purchase/upgrade/downgrade, we recommend you to + // verify the latest status of you your subscription by using server side receipt validation + // and update the UI accordingly. The subscription purchase status shown + // inside the app may not be accurate. + final oldSubscription = _getOldSubscription( + productDetails as GooglePlayProductDetails, + purchases); + GooglePlayPurchaseParam purchaseParam = + GooglePlayPurchaseParam( + productDetails: productDetails, + applicationUserName: null, + changeSubscriptionParam: oldSubscription != null + ? ChangeSubscriptionParam( + oldPurchaseDetails: oldSubscription, + prorationMode: ProrationMode + .immediateWithTimeProration) + : null); + if (productDetails.id == _kConsumableId) { + _inAppPurchasePlatform.buyConsumable( + purchaseParam: purchaseParam, + autoConsume: _kAutoConsume || Platform.isIOS); + } else { + _inAppPurchasePlatform.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), + ) + ])); + } + + 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 || + purchaseDetails.status == PurchaseStatus.restored) { + bool valid = await _verifyPurchase(purchaseDetails); + if (valid) { + deliverProduct(purchaseDetails); + } else { + _handleInvalidPurchase(purchaseDetails); + return; + } + } + + if (!_kAutoConsume && purchaseDetails.productID == _kConsumableId) { + final InAppPurchaseAndroidPlatformAddition addition = + InAppPurchasePlatformAddition.instance + as InAppPurchaseAndroidPlatformAddition; + + await addition.consumePurchase(purchaseDetails); + } + + if (purchaseDetails.pendingCompletePurchase) { + await _inAppPurchasePlatform.completePurchase(purchaseDetails); + } + } + }); + } + + GooglePlayPurchaseDetails? _getOldSubscription( + GooglePlayProductDetails productDetails, + Map purchases) { + // This is just to demonstrate a subscription upgrade or downgrade. + // This method assumes that you have only 2 subscriptions under a group, 'subscription_silver' & 'subscription_gold'. + // The 'subscription_silver' subscription can be upgraded to 'subscription_gold' and + // the 'subscription_gold' subscription can be downgraded to 'subscription_silver'. + // Please remember to replace the logic of finding the old subscription Id as per your app. + // The old subscription is only required on Android since Apple handles this internally + // by using the subscription group feature in iTunesConnect. + GooglePlayPurchaseDetails? oldSubscription; + if (productDetails.id == _kSilverSubscriptionId && + purchases[_kGoldSubscriptionId] != null) { + oldSubscription = + purchases[_kGoldSubscriptionId] as GooglePlayPurchaseDetails; + } else if (productDetails.id == _kGoldSubscriptionId && + purchases[_kSilverSubscriptionId] != null) { + oldSubscription = + purchases[_kSilverSubscriptionId] as GooglePlayPurchaseDetails; + } + return oldSubscription; + } +} 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 new file mode 100644 index 000000000000..f27261669438 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/example/pubspec.yaml @@ -0,0 +1,31 @@ +name: in_app_purchase_android_example +description: Demonstrates how to use the in_app_purchase_android plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.9.1+hotfix.2" + +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 + # 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_android/example/test_driver/test/integration_test.dart b/packages/in_app_purchase/in_app_purchase_android/example/test_driver/test/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/example/test_driver/test/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/lib/billing_client_wrappers.dart b/packages/in_app_purchase/in_app_purchase_android/lib/billing_client_wrappers.dart similarity index 82% rename from packages/in_app_purchase/lib/billing_client_wrappers.dart rename to packages/in_app_purchase/in_app_purchase_android/lib/billing_client_wrappers.dart index 127c980c15e6..1dac19f825b8 100644 --- a/packages/in_app_purchase/lib/billing_client_wrappers.dart +++ b/packages/in_app_purchase/in_app_purchase_android/lib/billing_client_wrappers.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/in_app_purchase/in_app_purchase_android/lib/in_app_purchase_android.dart b/packages/in_app_purchase/in_app_purchase_android/lib/in_app_purchase_android.dart new file mode 100644 index 000000000000..71e4e7a698fb --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/in_app_purchase_android.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_android_platform.dart'; +export 'src/in_app_purchase_android_platform_addition.dart'; +export 'src/types/types.dart'; diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/README.md b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/README.md similarity index 100% rename from packages/in_app_purchase/lib/src/billing_client_wrappers/README.md rename to packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/README.md 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 new file mode 100644 index 000000000000..1f43b3a8fbdd --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart @@ -0,0 +1,448 @@ +// Copyright 2013 The Flutter Authors. All 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/foundation.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'; + +/// Method identifier for the OnPurchaseUpdated method channel method. +@visibleForTesting +const String kOnPurchasesUpdated = + 'PurchasesUpdatedListener#onPurchasesUpdated(int, List)'; +const String _kOnBillingServiceDisconnected = + 'BillingClientStateListener#onBillingServiceDisconnected()'; + +/// Callback triggered by Play in response to purchase activity. +/// +/// This callback is triggered in response to all purchase activity while an +/// instance of `BillingClient` is active. This includes purchases initiated by +/// the app ([BillingClient.launchBillingFlow]) as well as purchases made in +/// Play itself while this app is open. +/// +/// This does not provide any hooks for purchases made in the past. See +/// [BillingClient.queryPurchases] and [BillingClient.queryPurchaseHistory]. +/// +/// All purchase information should also be verified manually, with your server +/// if at all possible. See ["Verify a +/// purchase"](https://developer.android.com/google/play/billing/billing_library_overview#Verify). +/// +/// Wraps a +/// [`PurchasesUpdatedListener`](https://developer.android.com/reference/com/android/billingclient/api/PurchasesUpdatedListener.html). +typedef void PurchasesUpdatedListener(PurchasesResultWrapper purchasesResult); + +/// This class can be used directly instead of [InAppPurchaseConnection] to call +/// Play-specific billing APIs. +/// +/// Wraps a +/// [`com.android.billingclient.api.BillingClient`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient) +/// instance. +/// +/// +/// In general this API conforms to the Java +/// `com.android.billingclient.api.BillingClient` API as much as possible, with +/// some minor changes to account for language differences. Callbacks have been +/// converted to futures where appropriate. +class BillingClient { + bool _enablePendingPurchases = false; + + /// Creates a billing client. + BillingClient(PurchasesUpdatedListener onPurchasesUpdated) { + channel.setMethodCallHandler(callHandler); + _callbacks[kOnPurchasesUpdated] = [onPurchasesUpdated]; + } + + // Occasionally methods in the native layer require a Dart callback to be + // triggered in response to a Java callback. For example, + // [startConnection] registers an [OnBillingServiceDisconnected] callback. + // This list of names to callbacks is used to trigger Dart callbacks in + // response to those Java callbacks. Dart sends the Java layer a handle to the + // 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 = >{}; + + /// Calls + /// [`BillingClient#isReady()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#isReady()) + /// to get the ready status of the BillingClient instance. + Future isReady() async { + final bool? ready = + await channel.invokeMethod('BillingClient#isReady()'); + return ready ?? false; + } + + /// 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. + void enablePendingPurchases() { + _enablePendingPurchases = true; + } + + /// Calls + /// [`BillingClient#startConnection(BillingClientStateListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#startconnection) + /// to create and connect a `BillingClient` instance. + /// + /// [onBillingServiceConnected] has been converted from a callback parameter + /// to the Future result returned by this function. This returns the + /// `BillingClient.BillingResultWrapper` describing the connection result. + /// + /// This triggers the creation of a new `BillingClient` instance in Java if + /// one doesn't already exist. + Future startConnection( + {required OnBillingServiceDisconnected + onBillingServiceDisconnected}) async { + assert(_enablePendingPurchases, + 'enablePendingPurchases() must be called before calling startConnection'); + List disconnectCallbacks = + _callbacks[_kOnBillingServiceDisconnected] ??= []; + disconnectCallbacks.add(onBillingServiceDisconnected); + return BillingResultWrapper.fromJson((await channel + .invokeMapMethod( + "BillingClient#startConnection(BillingClientStateListener)", + { + 'handle': disconnectCallbacks.length - 1, + 'enablePendingPurchases': _enablePendingPurchases + })) ?? + {}); + } + + /// Calls + /// [`BillingClient#endConnection(BillingClientStateListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#endconnect + /// to disconnect a `BillingClient` instance. + /// + /// Will trigger the [OnBillingServiceDisconnected] callback passed to [startConnection]. + /// + /// This triggers the destruction of the `BillingClient` instance in Java. + Future endConnection() async { + return channel.invokeMethod("BillingClient#endConnection()", null); + } + + /// Returns a list of [SkuDetailsWrapper]s that have [SkuDetailsWrapper.sku] + /// in `skusList`, and [SkuDetailsWrapper.type] matching `skuType`. + /// + /// Calls through to [`BillingClient#querySkuDetailsAsync(SkuDetailsParams, + /// SkuDetailsResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#querySkuDetailsAsync(com.android.billingclient.api.SkuDetailsParams,%20com.android.billingclient.api.SkuDetailsResponseListener)) + /// Instead of taking a callback parameter, it returns a Future + /// [SkuDetailsResponseWrapper]. It also takes the values of + /// `SkuDetailsParams` as direct arguments instead of requiring it constructed + /// and passed in as a class. + Future querySkuDetails( + {required SkuType skuType, required List skusList}) async { + final Map arguments = { + 'skuType': SkuTypeConverter().toJson(skuType), + 'skusList': skusList + }; + return SkuDetailsResponseWrapper.fromJson((await channel.invokeMapMethod< + String, dynamic>( + 'BillingClient#querySkuDetailsAsync(SkuDetailsParams, SkuDetailsResponseListener)', + arguments)) ?? + {}); + } + + /// Attempt to launch the Play Billing Flow for a given [skuDetails]. + /// + /// The [skuDetails] needs to have already been fetched in a [querySkuDetails] + /// call. The [accountId] is an optional hashed string associated with the user + /// that's unique to your app. It's used by Google to detect unusual behavior. + /// Do not pass in a cleartext [accountId], and do not use this field to store any Personally Identifiable Information (PII) + /// such as emails in cleartext. Attempting to store PII in this field will result in purchases being blocked. + /// Google Play recommends that you use either encryption or a one-way hash to generate an obfuscated identifier to send to Google Play. + /// + /// Specifies an optional [obfuscatedProfileId] that is uniquely associated with the user's profile in your app. + /// Some applications allow users to have multiple profiles within a single account. Use this method to send the user's profile identifier to Google. + /// Setting this field requests the user's obfuscated account id. + /// + /// Calling this attemps to show the Google Play purchase UI. The user is free + /// to complete the transaction there. + /// + /// This method returns a [BillingResultWrapper] representing the initial attempt + /// to show the Google Play billing flow. Actual purchase updates are + /// delivered via the [PurchasesUpdatedListener]. + /// + /// This method calls through to + /// [`BillingClient#launchBillingFlow`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#launchbillingflow). + /// It constructs a + /// [`BillingFlowParams`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams) + /// instance by [setting the given skuDetails](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder.html#setskudetails), + /// [the given accountId](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setObfuscatedAccountId(java.lang.String)) + /// and the [obfuscatedProfileId] (https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setobfuscatedprofileid). + /// + /// When this method is called to purchase a subscription, an optional `oldSku` + /// can be passed in. This will tell Google Play that rather than purchasing a new subscription, + /// the user needs to upgrade/downgrade the existing subscription. + /// The [oldSku](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setoldsku) and [purchaseToken] are the SKU id and purchase token that the user is upgrading or downgrading from. + /// [purchaseToken] must not be `null` if [oldSku] is not `null`. + /// The [prorationMode](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder#setreplaceskusprorationmode) is the mode of proration during subscription upgrade/downgrade. + /// This value will only be effective if the `oldSku` is also set. + Future launchBillingFlow( + {required String sku, + String? accountId, + String? obfuscatedProfileId, + String? oldSku, + String? purchaseToken, + ProrationMode? prorationMode}) async { + assert(sku != null); + assert((oldSku == null) == (purchaseToken == null), + 'oldSku and purchaseToken must both be set, or both be null.'); + final Map arguments = { + 'sku': sku, + 'accountId': accountId, + 'obfuscatedProfileId': obfuscatedProfileId, + 'oldSku': oldSku, + 'purchaseToken': purchaseToken, + 'prorationMode': ProrationModeConverter().toJson(prorationMode ?? + ProrationMode.unknownSubscriptionUpgradeDowngradePolicy) + }; + return BillingResultWrapper.fromJson( + (await channel.invokeMapMethod( + 'BillingClient#launchBillingFlow(Activity, BillingFlowParams)', + arguments)) ?? + {}); + } + + /// Fetches recent purchases for the given [SkuType]. + /// + /// Unlike [queryPurchaseHistory], This does not make a network request and + /// does not return items that are no longer owned. + /// + /// All purchase information should also be verified manually, with your + /// server if at all possible. See ["Verify a + /// purchase"](https://developer.android.com/google/play/billing/billing_library_overview#Verify). + /// + /// This wraps [`BillingClient#queryPurchases(String + /// skutype)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#querypurchases). + Future queryPurchases(SkuType skuType) async { + assert(skuType != null); + return PurchasesResultWrapper.fromJson((await channel + .invokeMapMethod( + 'BillingClient#queryPurchases(String)', { + 'skuType': SkuTypeConverter().toJson(skuType) + })) ?? + {}); + } + + /// Fetches purchase history for the given [SkuType]. + /// + /// Unlike [queryPurchases], this makes a network request via Play and returns + /// the most recent purchase for each [SkuDetailsWrapper] of the given + /// [SkuType] even if the item is no longer owned. + /// + /// All purchase information should also be verified manually, with your + /// server if at all possible. See ["Verify a + /// purchase"](https://developer.android.com/google/play/billing/billing_library_overview#Verify). + /// + /// This wraps [`BillingClient#queryPurchaseHistoryAsync(String skuType, + /// PurchaseHistoryResponseListener + /// listener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#querypurchasehistoryasync). + Future queryPurchaseHistory(SkuType skuType) async { + assert(skuType != null); + return PurchasesHistoryResult.fromJson((await channel.invokeMapMethod< + String, dynamic>( + 'BillingClient#queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)', + { + 'skuType': SkuTypeConverter().toJson(skuType) + })) ?? + {}); + } + + /// Consumes a given in-app product. + /// + /// Consuming can only be done on an item that's owned, and as a result of consumption, the user will no longer own it. + /// Consumption is done asynchronously. The method returns a Future containing a [BillingResultWrapper]. + /// + /// This wraps [`BillingClient#consumeAsync(String, ConsumeResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#consumeAsync(java.lang.String,%20com.android.billingclient.api.ConsumeResponseListener)) + Future consumeAsync(String purchaseToken) async { + assert(purchaseToken != null); + return BillingResultWrapper.fromJson((await channel + .invokeMapMethod( + 'BillingClient#consumeAsync(String, ConsumeResponseListener)', + { + 'purchaseToken': purchaseToken, + })) ?? + {}); + } + + /// Acknowledge an in-app purchase. + /// + /// The developer must acknowledge all in-app purchases after they have been granted to the user. + /// If this doesn't happen within three days of the purchase, the purchase will be refunded. + /// + /// Consumables are already implicitly acknowledged by calls to [consumeAsync] and + /// do not need to be explicitly acknowledged by using this method. + /// However this method can be called for them in order to explicitly acknowledge them if desired. + /// + /// Be sure to only acknowledge a purchase after it has been granted to the user. + /// [PurchaseWrapper.purchaseState] should be [PurchaseStateWrapper.purchased] and + /// the purchase should be validated. See [Verify a purchase](https://developer.android.com/google/play/billing/billing_library_overview#Verify) on verifying purchases. + /// + /// Please refer to [acknowledge](https://developer.android.com/google/play/billing/billing_library_overview#acknowledge) for more + /// details. + /// + /// This wraps [`BillingClient#acknowledgePurchase(String, AcknowledgePurchaseResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#acknowledgePurchase(com.android.billingclient.api.AcknowledgePurchaseParams,%20com.android.billingclient.api.AcknowledgePurchaseResponseListener)) + Future acknowledgePurchase(String purchaseToken) async { + assert(purchaseToken != null); + return BillingResultWrapper.fromJson((await channel.invokeMapMethod( + 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)', + { + 'purchaseToken': purchaseToken, + })) ?? + {}); + } + + /// The method call handler for [channel]. + @visibleForTesting + Future callHandler(MethodCall call) async { + switch (call.method) { + case kOnPurchasesUpdated: + // The purchases updated listener is a singleton. + assert(_callbacks[kOnPurchasesUpdated]!.length == 1); + final PurchasesUpdatedListener listener = + _callbacks[kOnPurchasesUpdated]!.first as PurchasesUpdatedListener; + listener(PurchasesResultWrapper.fromJson( + call.arguments.cast())); + break; + case _kOnBillingServiceDisconnected: + final int handle = call.arguments['handle']; + await _callbacks[_kOnBillingServiceDisconnected]![handle](); + break; + } + } +} + +/// Callback triggered when the [BillingClientWrapper] is disconnected. +/// +/// 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(); + +/// Possible `BillingClient` response statuses. +/// +/// Wraps +/// [`BillingClient.BillingResponse`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponse). +/// See the `BillingResponse` docs for more explanation of the different +/// constants. +enum BillingResponse { + // WARNING: Changes to this class need to be reflected in our generated code. + // Run `flutter packages pub run build_runner watch` to rebuild and watch for + // further changes. + + /// The request has reached the maximum timeout before Google Play responds. + @JsonValue(-3) + serviceTimeout, + + /// The requested feature is not supported by Play Store on the current device. + @JsonValue(-2) + featureNotSupported, + + /// The play Store service is not connected now - potentially transient state. + @JsonValue(-1) + serviceDisconnected, + + /// Success. + @JsonValue(0) + ok, + + /// The user pressed back or canceled a dialog. + @JsonValue(1) + userCanceled, + + /// The network connection is down. + @JsonValue(2) + serviceUnavailable, + + /// The billing API version is not supported for the type requested. + @JsonValue(3) + billingUnavailable, + + /// The requested product is not available for purchase. + @JsonValue(4) + itemUnavailable, + + /// Invalid arguments provided to the API. + @JsonValue(5) + developerError, + + /// Fatal error during the API action. + @JsonValue(6) + error, + + /// Failure to purchase since item is already owned. + @JsonValue(7) + itemAlreadyOwned, + + /// Failure to consume since item is not owned. + @JsonValue(8) + itemNotOwned, +} + +/// 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. +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 + // further changes. + + /// A one time product. Acquired in a single transaction. + @JsonValue('inapp') + inapp, + + /// A product requiring a recurring charge over time. + @JsonValue('subs') + subs, +} + +/// Enum representing the proration mode. +/// +/// When upgrading or downgrading a subscription, set this mode to provide details +/// about the proration that will be applied when the subscription changes. +/// +/// Wraps [`BillingFlowParams.ProrationMode`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.ProrationMode) +/// See the linked documentation for an explanation of the different constants. +enum ProrationMode { +// WARNING: Changes to this class need to be reflected in our generated code. +// Run `flutter packages pub run build_runner watch` to rebuild and watch for +// further changes. + + /// Unknown upgrade or downgrade policy. + @JsonValue(0) + unknownSubscriptionUpgradeDowngradePolicy, + + /// Replacement takes effect immediately, and the remaining time will be prorated and credited to the user. + /// + /// This is the current default behavior. + @JsonValue(1) + immediateWithTimeProration, + + /// Replacement takes effect immediately, and the billing cycle remains the same. + /// + /// The price for the remaining period will be charged. + /// This option is only available for subscription upgrade. + @JsonValue(2) + immediateAndChargeProratedPrice, + + /// Replacement takes effect immediately, and the new price will be charged on next recurrence time. + /// + /// The billing cycle stays the same. + @JsonValue(3) + immediateWithoutProration, + + /// Replacement takes effect when the old plan expires, and the new price will be charged at the same time. + @JsonValue(4) + deferred, +} diff --git a/packages/in_app_purchase/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 new file mode 100644 index 000000000000..46d6843af846 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.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: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]!; +} + +// 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; +} + +/// 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; + } + } +} 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 new file mode 100644 index 000000000000..4186a2a24252 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.g.dart @@ -0,0 +1,85 @@ +// 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']); +} + +Map _$_SerializedEnumsToJson(_SerializedEnums instance) => + { + 'response': _$BillingResponseEnumMap[instance.response], + 'type': _$SkuTypeEnumMap[instance.type], + 'purchaseState': _$PurchaseStateWrapperEnumMap[instance.purchaseState], + 'prorationMode': _$ProrationModeEnumMap[instance.prorationMode], + }; + +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, +}; 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 new file mode 100644 index 000000000000..374c26ab4a7a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart @@ -0,0 +1,350 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui' show hashValues; +import 'package:flutter/foundation.dart'; +import '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'; + +// WARNING: Changes to `@JsonSerializable` classes need to be reflected in the +// below generated file. Run `flutter packages pub run build_runner watch` to +// rebuild and watch for further changes. +part 'purchase_wrapper.g.dart'; + +/// Data structure representing a successful purchase. +/// +/// All purchase information should also be verified manually, with your +/// server if at all possible. See ["Verify a +/// purchase"](https://developer.android.com/google/play/billing/billing_library_overview#Verify). +/// +/// This wraps [`com.android.billlingclient.api.Purchase`](https://developer.android.com/reference/com/android/billingclient/api/Purchase) +@JsonSerializable() +@PurchaseStateConverter() +class PurchaseWrapper { + /// Creates a purchase wrapper with the given purchase details. + @visibleForTesting + PurchaseWrapper({ + required this.orderId, + required this.packageName, + required this.purchaseTime, + required this.purchaseToken, + required this.signature, + required this.sku, + required this.isAutoRenewing, + required this.originalJson, + this.developerPayload, + required this.isAcknowledged, + required this.purchaseState, + this.obfuscatedAccountId, + this.obfuscatedProfileId, + }); + + /// Factory for creating a [PurchaseWrapper] from a [Map] with the purchase details. + factory PurchaseWrapper.fromJson(Map map) => + _$PurchaseWrapperFromJson(map); + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + if (other.runtimeType != runtimeType) return false; + final PurchaseWrapper typedOther = other 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; + } + + @override + int get hashCode => hashValues( + orderId, + packageName, + purchaseTime, + purchaseToken, + signature, + sku, + isAutoRenewing, + originalJson, + isAcknowledged, + purchaseState); + + /// The unique ID for this purchase. Corresponds to the Google Payments order + /// ID. + @JsonKey(defaultValue: '') + final String orderId; + + /// The package name the purchase was made from. + @JsonKey(defaultValue: '') + final String packageName; + + /// When the purchase was made, as an epoch timestamp. + @JsonKey(defaultValue: 0) + final int purchaseTime; + + /// A unique ID for a given [SkuDetailsWrapper], user, and purchase. + @JsonKey(defaultValue: '') + final String purchaseToken; + + /// Signature of purchase data, signed with the developer's private key. Uses + /// RSASSA-PKCS1-v1_5. + @JsonKey(defaultValue: '') + final String signature; + + /// The product ID of this purchase. + @JsonKey(defaultValue: '') + final String sku; + + /// True for subscriptions that renew automatically. Does not apply to + /// [SkuType.inapp] products. + /// + /// For [SkuType.subs] this means that the subscription is canceled when it is + /// false. + /// + /// The value is `false` for [SkuType.inapp] products. + final bool isAutoRenewing; + + /// Details about this purchase, in JSON. + /// + /// This can be used verify a purchase. See ["Verify a purchase on a + /// device"](https://developer.android.com/google/play/billing/billing_library_overview#Verify-purchase-device). + /// Note though that verifying a purchase locally is inherently insecure (see + /// the article for more details). + @JsonKey(defaultValue: '') + final String originalJson; + + /// The payload specified by the developer when the purchase was acknowledged or consumed. + /// + /// The value is `null` if it wasn't specified when the purchase was acknowledged or consumed. + /// The `developerPayload` is removed from [BillingClientWrapper.acknowledgePurchase], [BillingClientWrapper.consumeAsync], [InAppPurchaseConnection.completePurchase], [InAppPurchaseConnection.consumePurchase] + /// after plugin version `0.5.0`. As a result, this will be `null` for new purchases that happen after updating to `0.5.0`. + final String? developerPayload; + + /// Whether the purchase has been acknowledged. + /// + /// A successful purchase has to be acknowledged within 3 days after the purchase via [BillingClient.acknowledgePurchase]. + /// * See also [BillingClient.acknowledgePurchase] for more details on acknowledging purchases. + @JsonKey(defaultValue: false) + final bool isAcknowledged; + + /// Determines the current state of the purchase. + /// + /// [BillingClient.acknowledgePurchase] should only be called when the `purchaseState` is [PurchaseStateWrapper.purchased]. + /// * See also [BillingClient.acknowledgePurchase] for more details on acknowledging purchases. + final PurchaseStateWrapper purchaseState; + + /// The obfuscatedAccountId specified when making a purchase. + /// + /// The [obfuscatedAccountId] can either be set in + /// [PurchaseParam.applicationUserName] when using the [InAppPurchasePlatform] + /// or by setting the [accountId] in [BillingClient.launchBillingFlow]. + final String? obfuscatedAccountId; + + /// The obfuscatedProfileId can be used when there are multiple profiles + /// withing one account. The obfuscatedProfileId should be specified when + /// making a purchase. This property can only be set on a purchase by + /// directly calling [BillingClient.launchBillingFlow] and is not available + /// on the generic [InAppPurchasePlatform]. + final String? obfuscatedProfileId; +} + +/// Data structure representing a purchase history record. +/// +/// This class includes a subset of fields in [PurchaseWrapper]. +/// +/// This wraps [`com.android.billlingclient.api.PurchaseHistoryRecord`](https://developer.android.com/reference/com/android/billingclient/api/PurchaseHistoryRecord) +/// +/// * See also: [BillingClient.queryPurchaseHistory] for obtaining a [PurchaseHistoryRecordWrapper]. +// 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() +class PurchaseHistoryRecordWrapper { + /// Creates a [PurchaseHistoryRecordWrapper] with the given record details. + @visibleForTesting + PurchaseHistoryRecordWrapper({ + required this.purchaseTime, + required this.purchaseToken, + required this.signature, + required this.sku, + required this.originalJson, + required this.developerPayload, + }); + + /// Factory for creating a [PurchaseHistoryRecordWrapper] from a [Map] with the record details. + factory PurchaseHistoryRecordWrapper.fromJson(Map map) => + _$PurchaseHistoryRecordWrapperFromJson(map); + + /// When the purchase was made, as an epoch timestamp. + @JsonKey(defaultValue: 0) + final int purchaseTime; + + /// A unique ID for a given [SkuDetailsWrapper], user, and purchase. + @JsonKey(defaultValue: '') + final String purchaseToken; + + /// Signature of purchase data, signed with the developer's private key. Uses + /// RSASSA-PKCS1-v1_5. + @JsonKey(defaultValue: '') + final String signature; + + /// The product ID of this purchase. + @JsonKey(defaultValue: '') + final String sku; + + /// Details about this purchase, in JSON. + /// + /// This can be used verify a purchase. See ["Verify a purchase on a + /// device"](https://developer.android.com/google/play/billing/billing_library_overview#Verify-purchase-device). + /// Note though that verifying a purchase locally is inherently insecure (see + /// the article for more details). + @JsonKey(defaultValue: '') + final String originalJson; + + /// The payload specified by the developer when the purchase was acknowledged or consumed. + /// + /// The value is `null` if it wasn't specified when the purchase was acknowledged or consumed. + final String? developerPayload; + + @override + bool operator ==(Object other) { + if (identical(other, this)) return true; + if (other.runtimeType != runtimeType) return false; + final PurchaseHistoryRecordWrapper typedOther = + other as PurchaseHistoryRecordWrapper; + return typedOther.purchaseTime == purchaseTime && + typedOther.purchaseToken == purchaseToken && + typedOther.signature == signature && + typedOther.sku == sku && + typedOther.originalJson == originalJson && + typedOther.developerPayload == developerPayload; + } + + @override + int get hashCode => hashValues(purchaseTime, purchaseToken, signature, sku, + originalJson, developerPayload); +} + +/// A data struct representing the result of a transaction. +/// +/// Contains a potentially empty list of [PurchaseWrapper]s, a [BillingResultWrapper] +/// that contains a detailed description of the status and a +/// [BillingResponse] to signify the overall state of the transaction. +/// +/// Wraps [`com.android.billingclient.api.Purchase.PurchasesResult`](https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchasesResult). +@JsonSerializable() +@BillingResponseConverter() +class PurchasesResultWrapper { + /// Creates a [PurchasesResultWrapper] with the given purchase result details. + PurchasesResultWrapper( + {required this.responseCode, + required this.billingResult, + required this.purchasesList}); + + /// Factory for creating a [PurchaseResultWrapper] from a [Map] with the result details. + factory PurchasesResultWrapper.fromJson(Map map) => + _$PurchasesResultWrapperFromJson(map); + + @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; + } + + @override + int get hashCode => hashValues(billingResult, responseCode, purchasesList); + + /// The detailed description of the status of the operation. + final BillingResultWrapper billingResult; + + /// The status of the operation. + /// + /// This can represent either the status of the "query purchase history" half + /// of the operation and the "user made purchases" transaction itself. + final BillingResponse responseCode; + + /// The list of successful purchases made in this transaction. + /// + /// May be empty, especially if [responseCode] is not [BillingResponse.ok]. + @JsonKey(defaultValue: []) + final List purchasesList; +} + +/// A data struct representing the result of a purchase history. +/// +/// Contains a potentially empty list of [PurchaseHistoryRecordWrapper]s and a [BillingResultWrapper] +/// that contains a detailed description of the status. +@JsonSerializable() +@BillingResponseConverter() +class PurchasesHistoryResult { + /// Creates a [PurchasesHistoryResult] with the provided history. + PurchasesHistoryResult( + {required this.billingResult, required this.purchaseHistoryRecordList}); + + /// Factory for creating a [PurchasesHistoryResult] from a [Map] with the history result details. + factory PurchasesHistoryResult.fromJson(Map map) => + _$PurchasesHistoryResultFromJson(map); + + @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; + } + + @override + int get hashCode => hashValues(billingResult, purchaseHistoryRecordList); + + /// The detailed description of the status of the [BillingClient.queryPurchaseHistory]. + final BillingResultWrapper billingResult; + + /// The list of queried purchase history records. + /// + /// May be empty, especially if [billingResult.responseCode] is not [BillingResponse.ok]. + @JsonKey(defaultValue: []) + final List purchaseHistoryRecordList; +} + +/// Possible state of a [PurchaseWrapper]. +/// +/// Wraps +/// [`BillingClient.api.Purchase.PurchaseState`](https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchaseState.html). +/// * See also: [PurchaseWrapper]. +enum PurchaseStateWrapper { + /// The state is unspecified. + /// + /// No actions on the [PurchaseWrapper] should be performed on this state. + /// This is a catch-all. It should never be returned by the Play Billing Library. + @JsonValue(0) + unspecified_state, + + /// The user has completed the purchase process. + /// + /// The production should be delivered and then the purchase should be acknowledged. + /// * See also [BillingClient.acknowledgePurchase] for more details on acknowledging purchases. + @JsonValue(1) + purchased, + + /// The user has started the purchase process. + /// + /// The user should follow the instructions that were given to them by the Play + /// Billing Library to complete the purchase. + /// + /// You can also choose to remind the user to complete the purchase if you detected a + /// [PurchaseWrapper] is still in the `pending` state in the future while calling [BillingClient.queryPurchases]. + @JsonValue(2) + pending, +} 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 new file mode 100644 index 000000000000..5607dbdd8cb2 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.g.dart @@ -0,0 +1,113 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +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?, + ); +} + +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) { + 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?, + ); +} + +Map _$PurchaseHistoryRecordWrapperToJson( + PurchaseHistoryRecordWrapper instance) => + { + 'purchaseTime': instance.purchaseTime, + 'purchaseToken': instance.purchaseToken, + 'signature': instance.signature, + 'sku': instance.sku, + 'originalJson': instance.originalJson, + 'developerPayload': instance.developerPayload, + }; + +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, + }; 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 new file mode 100644 index 000000000000..e3d13df2262a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.dart @@ -0,0 +1,244 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui' show hashValues; +import 'package:flutter/foundation.dart'; +import '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 +// rebuild and watch for further changes. +part 'sku_details_wrapper.g.dart'; + +/// The error message shown when the map represents billing result is invalid from method channel. +/// +/// This usually indicates a series underlining code issue in the plugin. +@visibleForTesting +const kInvalidBillingResultErrorMessage = + 'Invalid billing result map from method channel.'; + +/// Dart wrapper around [`com.android.billingclient.api.SkuDetails`](https://developer.android.com/reference/com/android/billingclient/api/SkuDetails). +/// +/// Contains the details of an available product in Google Play Billing. +@JsonSerializable() +@SkuTypeConverter() +class SkuDetailsWrapper { + /// Creates a [SkuDetailsWrapper] with the given purchase details. + @visibleForTesting + SkuDetailsWrapper({ + required this.description, + required this.freeTrialPeriod, + required this.introductoryPrice, + required this.introductoryPriceMicros, + required this.introductoryPriceCycles, + required this.introductoryPricePeriod, + required this.price, + required this.priceAmountMicros, + required this.priceCurrencyCode, + required this.sku, + required this.subscriptionPeriod, + required this.title, + required this.type, + required this.originalPrice, + required this.originalPriceAmountMicros, + }); + + /// 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. + @visibleForTesting + factory SkuDetailsWrapper.fromJson(Map map) => + _$SkuDetailsWrapperFromJson(map); + + /// Textual description of the product. + @JsonKey(defaultValue: '') + final String description; + + /// Trial period in ISO 8601 format. + @JsonKey(defaultValue: '') + final String freeTrialPeriod; + + /// Introductory price, only applies to [SkuType.subs]. Formatted ("$0.99"). + @JsonKey(defaultValue: '') + final String introductoryPrice; + + /// [introductoryPrice] in micro-units 990000 + @JsonKey(defaultValue: '') + final String introductoryPriceMicros; + + /// The number of 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. + @JsonKey(defaultValue: 0) + final int introductoryPriceCycles; + + /// The billing period of [introductoryPrice], in ISO 8601 format. + @JsonKey(defaultValue: '') + final String introductoryPricePeriod; + + /// Formatted with currency symbol ("$0.99"). + @JsonKey(defaultValue: '') + final String price; + + /// [price] in micro-units ("990000"). + @JsonKey(defaultValue: 0) + final int priceAmountMicros; + + /// [price] ISO 4217 currency code. + @JsonKey(defaultValue: '') + final String priceCurrencyCode; + + /// The product ID in Google Play Console. + @JsonKey(defaultValue: '') + final String sku; + + /// Applies to [SkuType.subs], formatted in ISO 8601. + @JsonKey(defaultValue: '') + final String subscriptionPeriod; + + /// The product's title. + @JsonKey(defaultValue: '') + final String title; + + /// The [SkuType] of the product. + final SkuType type; + + /// The original price that the user purchased this product for. + @JsonKey(defaultValue: '') + final String originalPrice; + + /// [originalPrice] in micro-units ("990000"). + @JsonKey(defaultValue: 0) + final int originalPriceAmountMicros; + + @override + bool operator ==(dynamic other) { + if (other.runtimeType != runtimeType) { + return false; + } + + final SkuDetailsWrapper typedOther = other; + return typedOther is SkuDetailsWrapper && + typedOther.description == description && + typedOther.freeTrialPeriod == freeTrialPeriod && + typedOther.introductoryPrice == introductoryPrice && + typedOther.introductoryPriceMicros == introductoryPriceMicros && + typedOther.introductoryPriceCycles == introductoryPriceCycles && + typedOther.introductoryPricePeriod == introductoryPricePeriod && + typedOther.price == price && + typedOther.priceAmountMicros == priceAmountMicros && + typedOther.sku == sku && + typedOther.subscriptionPeriod == subscriptionPeriod && + typedOther.title == title && + typedOther.type == type && + typedOther.originalPrice == originalPrice && + typedOther.originalPriceAmountMicros == originalPriceAmountMicros; + } + + @override + int get hashCode { + return hashValues( + description.hashCode, + freeTrialPeriod.hashCode, + introductoryPrice.hashCode, + introductoryPriceMicros.hashCode, + introductoryPriceCycles.hashCode, + introductoryPricePeriod.hashCode, + price.hashCode, + priceAmountMicros.hashCode, + sku.hashCode, + subscriptionPeriod.hashCode, + title.hashCode, + type.hashCode, + originalPrice, + originalPriceAmountMicros); + } +} + +/// Translation of [`com.android.billingclient.api.SkuDetailsResponseListener`](https://developer.android.com/reference/com/android/billingclient/api/SkuDetailsResponseListener.html). +/// +/// Returned by [BillingClient.querySkuDetails]. +@JsonSerializable() +class SkuDetailsResponseWrapper { + /// Creates a [SkuDetailsResponseWrapper] with the given purchase details. + @visibleForTesting + SkuDetailsResponseWrapper( + {required this.billingResult, required this.skuDetailsList}); + + /// 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 SkuDetailsResponseWrapper.fromJson(Map map) => + _$SkuDetailsResponseWrapperFromJson(map); + + /// The final result of the [BillingClient.querySkuDetails] call. + final BillingResultWrapper billingResult; + + /// A list of [SkuDetailsWrapper] matching the query to [BillingClient.querySkuDetails]. + @JsonKey(defaultValue: []) + final List skuDetailsList; + + @override + bool operator ==(dynamic other) { + if (other.runtimeType != runtimeType) { + return false; + } + + final SkuDetailsResponseWrapper typedOther = other; + return typedOther is SkuDetailsResponseWrapper && + typedOther.billingResult == billingResult && + typedOther.skuDetailsList == skuDetailsList; + } + + @override + int get hashCode => hashValues(billingResult, skuDetailsList); +} + +/// Params containing the response code and the debug message from the Play Billing API response. +@JsonSerializable() +@BillingResponseConverter() +class BillingResultWrapper { + /// Constructs the object with [responseCode] and [debugMessage]. + BillingResultWrapper({required this.responseCode, this.debugMessage}); + + /// Constructs an instance of this from a key value map of data. + /// + /// The map needs to have named string keys with values matching the names and + /// types of all of the members on this class. + factory BillingResultWrapper.fromJson(Map? map) { + if (map == null || map.isEmpty) { + return BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage); + } + return _$BillingResultWrapperFromJson(map); + } + + /// Response code returned in the Play Billing API calls. + final BillingResponse responseCode; + + /// Debug message returned in the Play Billing API calls. + /// + /// Defaults to `null`. + /// This message uses an en-US locale and should not be shown to users. + final String? debugMessage; + + @override + bool operator ==(dynamic other) { + if (other.runtimeType != runtimeType) { + return false; + } + + final BillingResultWrapper typedOther = other; + return typedOther is BillingResultWrapper && + typedOther.responseCode == responseCode && + typedOther.debugMessage == debugMessage; + } + + @override + int get hashCode => hashValues(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 new file mode 100644 index 000000000000..a14affdf9ed3 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart @@ -0,0 +1,83 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +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['introductoryPriceMicros'] 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? ?? '', + 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, + 'introductoryPriceMicros': instance.introductoryPriceMicros, + 'introductoryPriceCycles': instance.introductoryPriceCycles, + 'introductoryPricePeriod': instance.introductoryPricePeriod, + 'price': instance.price, + 'priceAmountMicros': instance.priceAmountMicros, + 'priceCurrencyCode': instance.priceCurrencyCode, + '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, + }; diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/channel.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/channel.dart new file mode 100644 index 000000000000..f8ab4d48be7e --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/channel.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. + +import 'package:flutter/services.dart'; + +/// Method channel for the plugin's platform<-->Dart calls. +const MethodChannel channel = + MethodChannel('plugins.flutter.io/in_app_purchase'); 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 new file mode 100644 index 000000000000..f71132a77ef3 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform.dart @@ -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. + +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'; + +/// [IAPError.code] code for failed purchases. +const String kPurchaseErrorCode = 'purchase_error'; + +/// [IAPError.code] code used when a consuming a purchased item fails. +const String kConsumptionFailedErrorCode = 'consume_purchase_failed'; + +/// [IAPError.code] code used when a query for previous transaction has failed. +const String kRestoredPurchaseErrorCode = 'restore_transactions_failed'; + +/// Indicates store front is Google Play +const String kIAPSource = 'google_play'; + +/// An [InAppPurchasePlatform] that wraps Android BillingClient. +/// +/// This translates various `BillingClient` calls and responses into the +/// generic plugin API. +class InAppPurchaseAndroidPlatform extends InAppPurchasePlatform { + InAppPurchaseAndroidPlatform._() { + billingClient = BillingClient((PurchasesResultWrapper resultWrapper) async { + _purchaseUpdatedController + .add(await _getPurchaseDetailsFromResult(resultWrapper)); + }); + + // Register [InAppPurchaseAndroidPlatformAddition]. + InAppPurchasePlatformAddition.instance = + InAppPurchaseAndroidPlatformAddition(billingClient); + + _readyFuture = _connect(); + _purchaseUpdatedController = StreamController.broadcast(); + } + + /// Registers this class as the default instance of [InAppPurchasePlatform]. + static void registerPlatform() { + // Register the platform instance with the plugin platform + // interface. + InAppPurchasePlatform.instance = InAppPurchaseAndroidPlatform._(); + } + + static late StreamController> + _purchaseUpdatedController; + + @override + Stream> get purchaseStream => + _purchaseUpdatedController.stream; + + /// The [BillingClient] that's abstracted by [GooglePlayConnection]. + /// + /// This field should not be used out of test code. + @visibleForTesting + late final BillingClient billingClient; + + late Future _readyFuture; + static Set _productIdsToConsume = Set(); + + @override + Future isAvailable() async { + await _readyFuture; + return billingClient.isReady(); + } + + @override + Future queryProductDetails( + Set identifiers) async { + List responses; + PlatformException? exception; + try { + responses = await Future.wait([ + billingClient.querySkuDetails( + skuType: SkuType.inapp, skusList: identifiers.toList()), + billingClient.querySkuDetails( + skuType: SkuType.subs, skusList: identifiers.toList()) + ]); + } on PlatformException catch (e) { + exception = e; + responses = [ + // ignore: invalid_use_of_visible_for_testing_member + SkuDetailsResponseWrapper( + billingResult: BillingResultWrapper( + responseCode: BillingResponse.error, debugMessage: e.code), + skuDetailsList: []), + // ignore: invalid_use_of_visible_for_testing_member + SkuDetailsResponseWrapper( + billingResult: BillingResultWrapper( + responseCode: BillingResponse.error, debugMessage: e.code), + skuDetailsList: []) + ]; + } + List productDetailsList = + responses.expand((SkuDetailsResponseWrapper response) { + return response.skuDetailsList; + }).map((SkuDetailsWrapper skuDetailWrapper) { + return GooglePlayProductDetails.fromSkuDetails(skuDetailWrapper); + }).toList(); + + Set successIDS = productDetailsList + .map((ProductDetails productDetails) => productDetails.id) + .toSet(); + List notFoundIDS = identifiers.difference(successIDS).toList(); + return ProductDetailsResponse( + productDetails: productDetailsList, + notFoundIDs: notFoundIDS, + error: exception == null + ? null + : IAPError( + source: kIAPSource, + code: exception.code, + message: exception.message ?? '', + details: exception.details)); + } + + @override + Future buyNonConsumable({required PurchaseParam purchaseParam}) async { + ChangeSubscriptionParam? changeSubscriptionParam; + + if (purchaseParam is GooglePlayPurchaseParam) { + changeSubscriptionParam = purchaseParam.changeSubscriptionParam; + } + + BillingResultWrapper billingResultWrapper = + await billingClient.launchBillingFlow( + sku: purchaseParam.productDetails.id, + accountId: purchaseParam.applicationUserName, + oldSku: changeSubscriptionParam?.oldPurchaseDetails.productID, + purchaseToken: changeSubscriptionParam + ?.oldPurchaseDetails.verificationData.serverVerificationData, + prorationMode: changeSubscriptionParam?.prorationMode); + return billingResultWrapper.responseCode == BillingResponse.ok; + } + + @override + Future buyConsumable( + {required PurchaseParam purchaseParam, bool autoConsume = true}) { + if (autoConsume) { + _productIdsToConsume.add(purchaseParam.productDetails.id); + } + return buyNonConsumable(purchaseParam: purchaseParam); + } + + @override + Future completePurchase( + PurchaseDetails purchase) async { + assert( + purchase is GooglePlayPurchaseDetails, + 'On Android, the `purchase` should always be of type `GooglePlayPurchaseDetails`.', + ); + + GooglePlayPurchaseDetails googlePurchase = + purchase as GooglePlayPurchaseDetails; + + if (googlePurchase.billingClientPurchase.isAcknowledged) { + return BillingResultWrapper(responseCode: BillingResponse.ok); + } + + if (googlePurchase.verificationData == null) { + throw ArgumentError( + 'completePurchase unsuccessful. The `purchase.verificationData` is not valid'); + } + + return await billingClient + .acknowledgePurchase(purchase.verificationData.serverVerificationData); + } + + @override + Future restorePurchases({ + String? applicationUserName, + }) async { + List responses; + + responses = await Future.wait([ + billingClient.queryPurchases(SkuType.inapp), + billingClient.queryPurchases(SkuType.subs) + ]); + + Set errorCodeSet = responses + .where((PurchasesResultWrapper response) => + response.responseCode != BillingResponse.ok) + .map((PurchasesResultWrapper response) => + response.responseCode.toString()) + .toSet(); + + String errorMessage = + errorCodeSet.isNotEmpty ? errorCodeSet.join(', ') : ''; + + List pastPurchases = + responses.expand((PurchasesResultWrapper response) { + return response.purchasesList; + }).map((PurchaseWrapper purchaseWrapper) { + final GooglePlayPurchaseDetails purchaseDetails = + GooglePlayPurchaseDetails.fromPurchase(purchaseWrapper); + + purchaseDetails.status = PurchaseStatus.restored; + + return purchaseDetails; + }).toList(); + + if (errorMessage.isNotEmpty) { + throw InAppPurchaseException( + source: kIAPSource, + code: kRestoredPurchaseErrorCode, + message: errorMessage, + ); + } + + _purchaseUpdatedController.add(pastPurchases); + } + + Future _connect() => + billingClient.startConnection(onBillingServiceDisconnected: () {}); + + Future _maybeAutoConsumePurchase( + PurchaseDetails purchaseDetails) async { + if (!(purchaseDetails.status == PurchaseStatus.purchased && + _productIdsToConsume.contains(purchaseDetails.productID))) { + return purchaseDetails; + } + + final BillingResultWrapper billingResult = + await (InAppPurchasePlatformAddition.instance + as InAppPurchaseAndroidPlatformAddition) + .consumePurchase(purchaseDetails); + final BillingResponse consumedResponse = billingResult.responseCode; + if (consumedResponse != BillingResponse.ok) { + purchaseDetails.status = PurchaseStatus.error; + purchaseDetails.error = IAPError( + source: kIAPSource, + code: kConsumptionFailedErrorCode, + message: consumedResponse.toString(), + details: billingResult.debugMessage, + ); + } + _productIdsToConsume.remove(purchaseDetails.productID); + + return purchaseDetails; + } + + Future> _getPurchaseDetailsFromResult( + PurchasesResultWrapper resultWrapper) async { + IAPError? error; + if (resultWrapper.responseCode != BillingResponse.ok) { + error = IAPError( + source: kIAPSource, + code: kPurchaseErrorCode, + message: resultWrapper.responseCode.toString(), + details: resultWrapper.billingResult.debugMessage, + ); + } + final List> purchases = + resultWrapper.purchasesList.map((PurchaseWrapper purchase) { + return _maybeAutoConsumePurchase( + GooglePlayPurchaseDetails.fromPurchase(purchase)..error = error); + }).toList(); + if (purchases.isNotEmpty) { + return Future.wait(purchases); + } else { + return [ + PurchaseDetails( + purchaseID: '', + productID: '', + status: PurchaseStatus.error, + transactionDate: null, + verificationData: PurchaseVerificationData( + localVerificationData: '', + serverVerificationData: '', + source: kIAPSource)) + ..error = error + ]; + } + } +} diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart new file mode 100644 index 000000000000..84f8b9ef1787 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.dart @@ -0,0 +1,138 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:in_app_purchase_android/in_app_purchase_android.dart'; +import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; + +import '../billing_client_wrappers.dart'; +import 'types/types.dart'; + +/// 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(); + } + + /// Whether pending purchase is enabled. + /// + /// See also [enablePendingPurchases] for more on pending purchases. + static bool get enablePendingPurchase => _enablePendingPurchase; + static bool _enablePendingPurchase = false; + + /// 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. + static void enablePendingPurchases() { + _enablePendingPurchase = true; + } + + final BillingClient _billingClient; + + /// Mark that the user has consumed a product. + /// + /// You are responsible for consuming all consumable purchases once they are + /// delivered. The user won't be able to buy the same product again until the + /// purchase of the product is consumed. + Future consumePurchase(PurchaseDetails purchase) { + if (purchase.verificationData == null) { + throw ArgumentError( + 'consumePurchase unsuccessful. The `purchase.verificationData` is not valid'); + } + return _billingClient + .consumeAsync(purchase.verificationData.serverVerificationData); + } + + /// Query all previous purchases. + /// + /// The `applicationUserName` should match whatever was sent in the initial + /// `PurchaseParam`, if anything. If no `applicationUserName` was specified in + /// the initial `PurchaseParam`, use `null`. + /// + /// This does not return consumed products. If you want to restore unused + /// consumable products, you need to persist consumable product information + /// for your user on your own server. + /// + /// See also: + /// + /// * [refreshPurchaseVerificationData], for reloading failed + /// [PurchaseDetails.verificationData]. + Future queryPastPurchases( + {String? applicationUserName}) async { + List responses; + PlatformException? exception; + try { + responses = await Future.wait([ + _billingClient.queryPurchases(SkuType.inapp), + _billingClient.queryPurchases(SkuType.subs) + ]); + } on PlatformException catch (e) { + exception = e; + responses = [ + PurchasesResultWrapper( + responseCode: BillingResponse.error, + purchasesList: [], + billingResult: BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: e.details.toString(), + ), + ), + PurchasesResultWrapper( + responseCode: BillingResponse.error, + purchasesList: [], + billingResult: BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: e.details.toString(), + ), + ) + ]; + } + + Set errorCodeSet = responses + .where((PurchasesResultWrapper response) => + response.responseCode != BillingResponse.ok) + .map((PurchasesResultWrapper response) => + response.responseCode.toString()) + .toSet(); + + String errorMessage = + errorCodeSet.isNotEmpty ? errorCodeSet.join(', ') : ''; + + List pastPurchases = + responses.expand((PurchasesResultWrapper response) { + return response.purchasesList; + }).map((PurchaseWrapper purchaseWrapper) { + return GooglePlayPurchaseDetails.fromPurchase(purchaseWrapper); + }).toList(); + + IAPError? error; + if (exception != null) { + error = IAPError( + source: kIAPSource, + code: exception.code, + message: exception.message ?? '', + details: exception.details); + } else if (errorMessage.isNotEmpty) { + error = IAPError( + source: kIAPSource, + code: kRestoredPurchaseErrorCode, + message: errorMessage); + } + + return QueryPurchaseDetailsResponse( + pastPurchases: pastPurchases, error: error); + } +} diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/types/change_subscription_param.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/change_subscription_param.dart new file mode 100644 index 000000000000..1099da3bf159 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/change_subscription_param.dart @@ -0,0 +1,25 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '../../billing_client_wrappers.dart'; +import 'types.dart'; + +/// This parameter object for upgrading or downgrading an existing subscription. +class ChangeSubscriptionParam { + /// Creates a new change subscription param object with given data + ChangeSubscriptionParam({ + required this.oldPurchaseDetails, + this.prorationMode, + }); + + /// The purchase object of the existing subscription that the user needs to + /// upgrade/downgrade from. + final GooglePlayPurchaseDetails oldPurchaseDetails; + + /// The proration mode. + /// + /// This is an optional parameter that indicates how to handle the existing + /// subscription when the new subscription comes into effect. + final ProrationMode? prorationMode; +} 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 new file mode 100644 index 000000000000..62589038804e --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_product_details.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. + +import 'package:in_app_purchase_android/billing_client_wrappers.dart'; +import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; + +/// The class represents the information of a product as registered in at +/// Google Play store front. +class GooglePlayProductDetails extends ProductDetails { + /// Creates a new Google Play specific product details object with the + /// provided details. + GooglePlayProductDetails({ + required String id, + required String title, + required String description, + required String price, + required double rawPrice, + required String currencyCode, + required this.skuDetails, + }) : super( + id: id, + title: title, + description: description, + price: price, + rawPrice: rawPrice, + currencyCode: currencyCode, + ); + + /// 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( + SkuDetailsWrapper skuDetails, + ) { + return GooglePlayProductDetails( + id: skuDetails.sku, + title: skuDetails.title, + description: skuDetails.description, + price: skuDetails.price, + rawPrice: ((skuDetails.priceAmountMicros) / 1000000.0).toDouble(), + currencyCode: skuDetails.priceCurrencyCode, + skuDetails: 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 new file mode 100644 index 000000000000..66e3a8f5a590 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_details.dart @@ -0,0 +1,71 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +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'; +import '../in_app_purchase_android_platform.dart'; + +/// The class represents the information of a purchase made using Google Play. +class GooglePlayPurchaseDetails extends PurchaseDetails { + /// Creates a new Google Play specific purchase details object with the + /// provided details. + GooglePlayPurchaseDetails({ + String? purchaseID, + required String productID, + required PurchaseVerificationData verificationData, + required String? transactionDate, + required this.billingClientPurchase, + required PurchaseStatus status, + }) : super( + productID: productID, + purchaseID: purchaseID, + transactionDate: transactionDate, + verificationData: verificationData, + status: status) { + this.status = status; + } + + /// Points back to the [PurchaseWrapper] which was used to generate this + /// [GooglePlayPurchaseDetails] object. + final PurchaseWrapper billingClientPurchase; + + late PurchaseStatus _status; + + /// The status that this [PurchaseDetails] is currently on. + PurchaseStatus get status => _status; + set status(PurchaseStatus status) { + _pendingCompletePurchase = status == PurchaseStatus.purchased; + _status = status; + } + + bool _pendingCompletePurchase = false; + bool get pendingCompletePurchase => _pendingCompletePurchase; + + /// Generate a [PurchaseDetails] object based on an Android [Purchase] object. + factory GooglePlayPurchaseDetails.fromPurchase(PurchaseWrapper purchase) { + final GooglePlayPurchaseDetails purchaseDetails = GooglePlayPurchaseDetails( + purchaseID: purchase.orderId, + productID: purchase.sku, + verificationData: PurchaseVerificationData( + localVerificationData: purchase.originalJson, + serverVerificationData: purchase.purchaseToken, + source: kIAPSource), + transactionDate: purchase.purchaseTime.toString(), + billingClientPurchase: purchase, + status: PurchaseStateConverter().toPurchaseStatus(purchase.purchaseState), + ); + + if (purchaseDetails.status == PurchaseStatus.error) { + purchaseDetails.error = IAPError( + source: kIAPSource, + code: kPurchaseErrorCode, + message: '', + ); + } + + return purchaseDetails; + } +} diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_param.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_param.dart new file mode 100644 index 000000000000..bcf0ad62a245 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_param.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:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; + +import '../../in_app_purchase_android.dart'; + +/// Google Play specific parameter object for generating a purchase. +class GooglePlayPurchaseParam extends PurchaseParam { + /// Creates a new [GooglePlayPurchaseParam] object with the given data. + GooglePlayPurchaseParam({ + required ProductDetails productDetails, + String? applicationUserName, + this.changeSubscriptionParam, + }) : super( + productDetails: productDetails, + applicationUserName: applicationUserName, + ); + + /// The 'changeSubscriptionParam' containing information for upgrading or + /// downgrading an existing subscription. + final ChangeSubscriptionParam? changeSubscriptionParam; +} diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/types/query_purchase_details_response.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/query_purchase_details_response.dart new file mode 100644 index 000000000000..c0795a9be573 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/query_purchase_details_response.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:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; + +import 'types.dart'; + +/// The response object for fetching the past purchases. +/// +/// An instance of this class is returned in [InAppPurchaseConnection.queryPastPurchases]. +class QueryPurchaseDetailsResponse { + /// Creates a new [QueryPurchaseDetailsResponse] object with the provider information. + QueryPurchaseDetailsResponse({required this.pastPurchases, this.error}); + + /// A list of successfully fetched past purchases. + /// + /// If there are no past purchases, or there is an [error] fetching past purchases, + /// this variable is an empty List. + /// You should verify the purchase data using [PurchaseDetails.verificationData] before using the [PurchaseDetails] object. + final List pastPurchases; + + /// The error when fetching past purchases. + /// + /// If the fetch is successful, the value is `null`. + final IAPError? error; +} diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/types/types.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/types.dart new file mode 100644 index 000000000000..0a43425f6e94 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/types.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 'change_subscription_param.dart'; +export 'google_play_product_details.dart'; +export 'google_play_purchase_details.dart'; +export 'google_play_purchase_param.dart'; +export 'query_purchase_details_response.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml new file mode 100644 index 000000000000..900fa4374bd0 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -0,0 +1,31 @@ +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 +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 +version: 0.1.2 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +flutter: + plugin: + platforms: + android: + package: io.flutter.plugins.inapppurchase + pluginClass: InAppPurchasePlugin + +dependencies: + collection: ^1.15.0 + flutter: + sdk: flutter + in_app_purchase_platform_interface: ^1.0.0 + json_annotation: ^4.0.1 + meta: ^1.3.0 + test: ^1.16.0 + +dev_dependencies: + build_runner: ^1.11.1 + flutter_test: + sdk: flutter + json_serializable: ^4.1.1 diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart new file mode 100644 index 000000000000..ec7289735ade --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -0,0 +1,547 @@ +// Copyright 2013 The Flutter Authors. All 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:flutter/services.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'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); + late BillingClient billingClient; + + setUpAll(() => + channel.setMockMethodCallHandler(stubPlatform.fakeMethodCallHandler)); + + setUp(() { + billingClient = BillingClient((PurchasesResultWrapper _) {}); + billingClient.enablePendingPurchases(); + stubPlatform.reset(); + }); + + group('isReady', () { + test('true', () async { + stubPlatform.addResponse(name: 'BillingClient#isReady()', value: true); + expect(await billingClient.isReady(), isTrue); + }); + + test('false', () async { + stubPlatform.addResponse(name: 'BillingClient#isReady()', value: false); + expect(await billingClient.isReady(), isFalse); + }); + }); + + // Make sure that the enum values are supported and that the converter call + // does not fail + test('response states', () async { + BillingResponseConverter converter = BillingResponseConverter(); + converter.fromJson(-3); + converter.fromJson(-2); + converter.fromJson(-1); + converter.fromJson(0); + converter.fromJson(1); + converter.fromJson(2); + converter.fromJson(3); + converter.fromJson(4); + converter.fromJson(5); + converter.fromJson(6); + converter.fromJson(7); + converter.fromJson(8); + }); + + group('startConnection', () { + final String methodName = + 'BillingClient#startConnection(BillingClientStateListener)'; + test('returns BillingResultWrapper', () async { + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.developerError; + stubPlatform.addResponse( + name: methodName, + value: { + 'responseCode': BillingResponseConverter().toJson(responseCode), + 'debugMessage': debugMessage, + }, + ); + + BillingResultWrapper billingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + expect( + await billingClient.startConnection( + onBillingServiceDisconnected: () {}), + equals(billingResult)); + }); + + test('passes handle to onBillingServiceDisconnected', () async { + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.developerError; + stubPlatform.addResponse( + name: methodName, + value: { + 'responseCode': BillingResponseConverter().toJson(responseCode), + 'debugMessage': debugMessage, + }, + ); + await billingClient.startConnection(onBillingServiceDisconnected: () {}); + final MethodCall call = stubPlatform.previousCallMatching(methodName); + expect( + call.arguments, + equals( + {'handle': 0, 'enablePendingPurchases': true})); + }); + + test('handles method channel returning null', () async { + stubPlatform.addResponse( + name: methodName, + value: null, + ); + + expect( + await billingClient.startConnection( + onBillingServiceDisconnected: () {}), + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + }); + }); + + test('endConnection', () async { + final String endConnectionName = 'BillingClient#endConnection()'; + expect(stubPlatform.countPreviousCalls(endConnectionName), equals(0)); + stubPlatform.addResponse(name: endConnectionName, value: null); + await billingClient.endConnection(); + expect(stubPlatform.countPreviousCalls(endConnectionName), equals(1)); + }); + + group('querySkuDetails', () { + final String queryMethodName = + 'BillingClient#querySkuDetailsAsync(SkuDetailsParams, SkuDetailsResponseListener)'; + + test('handles empty skuDetails', () async { + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.developerError; + stubPlatform.addResponse(name: queryMethodName, value: { + 'billingResult': { + 'responseCode': BillingResponseConverter().toJson(responseCode), + 'debugMessage': debugMessage, + }, + 'skuDetailsList': >[] + }); + + final SkuDetailsResponseWrapper response = await billingClient + .querySkuDetails( + skuType: SkuType.inapp, skusList: ['invalid']); + + BillingResultWrapper billingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + expect(response.billingResult, equals(billingResult)); + expect(response.skuDetailsList, isEmpty); + }); + + test('returns SkuDetailsResponseWrapper', () async { + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.ok; + stubPlatform.addResponse(name: queryMethodName, value: { + 'billingResult': { + 'responseCode': BillingResponseConverter().toJson(responseCode), + 'debugMessage': debugMessage, + }, + 'skuDetailsList': >[buildSkuMap(dummySkuDetails)] + }); + + final SkuDetailsResponseWrapper response = await billingClient + .querySkuDetails( + skuType: SkuType.inapp, skusList: ['invalid']); + + BillingResultWrapper billingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + expect(response.billingResult, equals(billingResult)); + expect(response.skuDetailsList, contains(dummySkuDetails)); + }); + + test('handles null method channel response', () async { + stubPlatform.addResponse(name: queryMethodName, value: null); + + final SkuDetailsResponseWrapper response = await billingClient + .querySkuDetails( + skuType: SkuType.inapp, skusList: ['invalid']); + + BillingResultWrapper billingResult = BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage); + expect(response.billingResult, equals(billingResult)); + expect(response.skuDetailsList, isEmpty); + }); + }); + + group('launchBillingFlow', () { + final 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( + responseCode: responseCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: launchMethodName, + value: buildBillingResultMap(expectedBillingResult), + ); + final SkuDetailsWrapper skuDetails = dummySkuDetails; + final String accountId = "hashedAccountId"; + final String profileId = "hashedProfileId"; + + expect( + await billingClient.launchBillingFlow( + sku: skuDetails.sku, + accountId: accountId, + obfuscatedProfileId: profileId), + equals(expectedBillingResult)); + Map arguments = + stubPlatform.previousCallMatching(launchMethodName).arguments; + expect(arguments['sku'], equals(skuDetails.sku)); + expect(arguments['accountId'], equals(accountId)); + expect(arguments['obfuscatedProfileId'], equals(profileId)); + }); + + test( + '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( + responseCode: responseCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: launchMethodName, + value: buildBillingResultMap(expectedBillingResult), + ); + final SkuDetailsWrapper skuDetails = dummySkuDetails; + final String accountId = 'hashedAccountId'; + final String profileId = 'hashedProfileId'; + + expect( + billingClient.launchBillingFlow( + sku: skuDetails.sku, + accountId: accountId, + obfuscatedProfileId: profileId, + oldSku: dummyOldPurchase.sku, + purchaseToken: null), + throwsAssertionError); + + expect( + billingClient.launchBillingFlow( + sku: skuDetails.sku, + accountId: accountId, + obfuscatedProfileId: profileId, + oldSku: null, + purchaseToken: dummyOldPurchase.purchaseToken), + throwsAssertionError); + }); + + test( + 'serializes and deserializes data on change subscription without proration', + () async { + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: launchMethodName, + value: buildBillingResultMap(expectedBillingResult), + ); + final SkuDetailsWrapper skuDetails = dummySkuDetails; + final String accountId = 'hashedAccountId'; + final String profileId = 'hashedProfileId'; + + expect( + await billingClient.launchBillingFlow( + sku: skuDetails.sku, + accountId: accountId, + obfuscatedProfileId: profileId, + oldSku: dummyOldPurchase.sku, + purchaseToken: dummyOldPurchase.purchaseToken), + equals(expectedBillingResult)); + Map arguments = + stubPlatform.previousCallMatching(launchMethodName).arguments; + expect(arguments['sku'], equals(skuDetails.sku)); + expect(arguments['accountId'], equals(accountId)); + expect(arguments['oldSku'], equals(dummyOldPurchase.sku)); + expect( + arguments['purchaseToken'], equals(dummyOldPurchase.purchaseToken)); + expect(arguments['obfuscatedProfileId'], equals(profileId)); + }); + + test( + 'serializes and deserializes data on change subscription with proration', + () async { + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: launchMethodName, + value: buildBillingResultMap(expectedBillingResult), + ); + final SkuDetailsWrapper skuDetails = dummySkuDetails; + final String accountId = 'hashedAccountId'; + final String profileId = 'hashedProfileId'; + final prorationMode = ProrationMode.immediateAndChargeProratedPrice; + + expect( + await billingClient.launchBillingFlow( + sku: skuDetails.sku, + accountId: accountId, + obfuscatedProfileId: profileId, + oldSku: dummyOldPurchase.sku, + prorationMode: prorationMode, + purchaseToken: dummyOldPurchase.purchaseToken), + equals(expectedBillingResult)); + Map arguments = + stubPlatform.previousCallMatching(launchMethodName).arguments; + expect(arguments['sku'], equals(skuDetails.sku)); + expect(arguments['accountId'], equals(accountId)); + expect(arguments['oldSku'], equals(dummyOldPurchase.sku)); + expect(arguments['obfuscatedProfileId'], equals(profileId)); + expect( + arguments['purchaseToken'], equals(dummyOldPurchase.purchaseToken)); + expect(arguments['prorationMode'], + ProrationModeConverter().toJson(prorationMode)); + }); + + test('handles null accountId', () async { + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: launchMethodName, + value: buildBillingResultMap(expectedBillingResult), + ); + final SkuDetailsWrapper skuDetails = dummySkuDetails; + + expect(await billingClient.launchBillingFlow(sku: skuDetails.sku), + equals(expectedBillingResult)); + Map arguments = + stubPlatform.previousCallMatching(launchMethodName).arguments; + expect(arguments['sku'], equals(skuDetails.sku)); + expect(arguments['accountId'], isNull); + }); + + test('handles method channel returning null', () async { + stubPlatform.addResponse( + name: launchMethodName, + value: null, + ); + final SkuDetailsWrapper skuDetails = dummySkuDetails; + expect( + await billingClient.launchBillingFlow(sku: skuDetails.sku), + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + }); + }); + + group('queryPurchases', () { + const String queryPurchasesMethodName = + 'BillingClient#queryPurchases(String)'; + + test('serializes and deserializes data', () async { + final BillingResponse expectedCode = BillingResponse.ok; + final List expectedList = [ + dummyPurchase + ]; + const String debugMessage = 'dummy message'; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: expectedCode, debugMessage: debugMessage); + stubPlatform + .addResponse(name: queryPurchasesMethodName, value: { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'responseCode': BillingResponseConverter().toJson(expectedCode), + 'purchasesList': expectedList + .map((PurchaseWrapper purchase) => buildPurchaseMap(purchase)) + .toList(), + }); + + final PurchasesResultWrapper response = + await billingClient.queryPurchases(SkuType.inapp); + + expect(response.billingResult, equals(expectedBillingResult)); + expect(response.responseCode, equals(expectedCode)); + expect(response.purchasesList, equals(expectedList)); + }); + + test('handles empty purchases', () async { + final BillingResponse expectedCode = BillingResponse.userCanceled; + const String debugMessage = 'dummy message'; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: expectedCode, debugMessage: debugMessage); + stubPlatform + .addResponse(name: queryPurchasesMethodName, value: { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'responseCode': BillingResponseConverter().toJson(expectedCode), + 'purchasesList': [], + }); + + final PurchasesResultWrapper response = + await billingClient.queryPurchases(SkuType.inapp); + + expect(response.billingResult, equals(expectedBillingResult)); + expect(response.responseCode, equals(expectedCode)); + expect(response.purchasesList, isEmpty); + }); + + test('handles method channel returning null', () async { + stubPlatform.addResponse( + name: queryPurchasesMethodName, + value: null, + ); + final PurchasesResultWrapper response = + await billingClient.queryPurchases(SkuType.inapp); + + expect( + response.billingResult, + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + expect(response.responseCode, BillingResponse.error); + expect(response.purchasesList, isEmpty); + }); + }); + + group('queryPurchaseHistory', () { + const String queryPurchaseHistoryMethodName = + 'BillingClient#queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)'; + + test('serializes and deserializes data', () async { + final BillingResponse expectedCode = BillingResponse.ok; + final List expectedList = + [ + dummyPurchaseHistoryRecord, + ]; + const String debugMessage = 'dummy message'; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: expectedCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: queryPurchaseHistoryMethodName, + value: { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'purchaseHistoryRecordList': expectedList + .map((PurchaseHistoryRecordWrapper purchaseHistoryRecord) => + buildPurchaseHistoryRecordMap(purchaseHistoryRecord)) + .toList(), + }); + + final PurchasesHistoryResult response = + await billingClient.queryPurchaseHistory(SkuType.inapp); + expect(response.billingResult, equals(expectedBillingResult)); + expect(response.purchaseHistoryRecordList, equals(expectedList)); + }); + + test('handles empty purchases', () async { + final BillingResponse expectedCode = BillingResponse.userCanceled; + const String debugMessage = 'dummy message'; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: expectedCode, debugMessage: debugMessage); + stubPlatform.addResponse(name: queryPurchaseHistoryMethodName, value: { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'purchaseHistoryRecordList': [], + }); + + final PurchasesHistoryResult response = + await billingClient.queryPurchaseHistory(SkuType.inapp); + + expect(response.billingResult, equals(expectedBillingResult)); + expect(response.purchaseHistoryRecordList, isEmpty); + }); + + test('handles method channel returning null', () async { + stubPlatform.addResponse( + name: queryPurchaseHistoryMethodName, + value: null, + ); + final PurchasesHistoryResult response = + await billingClient.queryPurchaseHistory(SkuType.inapp); + + expect( + response.billingResult, + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + expect(response.purchaseHistoryRecordList, isEmpty); + }); + }); + + group('consume purchases', () { + const String consumeMethodName = + 'BillingClient#consumeAsync(String, ConsumeResponseListener)'; + test('consume purchase async success', () async { + final BillingResponse expectedCode = BillingResponse.ok; + const String debugMessage = 'dummy message'; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: expectedCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: consumeMethodName, + value: buildBillingResultMap(expectedBillingResult)); + + final BillingResultWrapper billingResult = + await billingClient.consumeAsync('dummy token'); + + expect(billingResult, equals(expectedBillingResult)); + }); + + test('handles method channel returning null', () async { + stubPlatform.addResponse( + name: consumeMethodName, + value: null, + ); + final BillingResultWrapper billingResult = + await billingClient.consumeAsync('dummy token'); + + expect( + billingResult, + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + }); + }); + + group('acknowledge purchases', () { + const String acknowledgeMethodName = + 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)'; + test('acknowledge purchase success', () async { + final BillingResponse expectedCode = BillingResponse.ok; + const String debugMessage = 'dummy message'; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: expectedCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: acknowledgeMethodName, + value: buildBillingResultMap(expectedBillingResult)); + + final BillingResultWrapper billingResult = + await billingClient.acknowledgePurchase('dummy token'); + + expect(billingResult, equals(expectedBillingResult)); + }); + test('handles method channel returning null', () async { + stubPlatform.addResponse( + name: acknowledgeMethodName, + value: null, + ); + final BillingResultWrapper billingResult = + await billingClient.acknowledgePurchase('dummy token'); + + expect( + billingResult, + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + }); + }); +} 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 new file mode 100644 index 000000000000..bb7ff8535c7a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart @@ -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 '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( + orderId: 'orderId', + packageName: 'packageName', + purchaseTime: 0, + signature: 'signature', + sku: 'sku', + purchaseToken: 'purchaseToken', + isAutoRenewing: false, + originalJson: '', + developerPayload: 'dummy payload', + isAcknowledged: true, + purchaseState: PurchaseStateWrapper.purchased, + obfuscatedAccountId: 'Account101', + obfuscatedProfileId: 'Profile103', +); + +final PurchaseWrapper dummyUnacknowledgedPurchase = PurchaseWrapper( + orderId: 'orderId', + packageName: 'packageName', + purchaseTime: 0, + signature: 'signature', + sku: 'sku', + purchaseToken: 'purchaseToken', + isAutoRenewing: false, + originalJson: '', + developerPayload: 'dummy payload', + isAcknowledged: false, + purchaseState: PurchaseStateWrapper.purchased, +); + +final PurchaseHistoryRecordWrapper dummyPurchaseHistoryRecord = + PurchaseHistoryRecordWrapper( + purchaseTime: 0, + signature: 'signature', + sku: 'sku', + purchaseToken: 'purchaseToken', + originalJson: '', + developerPayload: 'dummy payload', +); + +final PurchaseWrapper dummyOldPurchase = PurchaseWrapper( + orderId: 'oldOrderId', + packageName: 'oldPackageName', + purchaseTime: 0, + signature: 'oldSignature', + sku: 'oldSku', + purchaseToken: 'oldPurchaseToken', + isAutoRenewing: false, + originalJson: '', + developerPayload: 'old dummy payload', + isAcknowledged: true, + purchaseState: PurchaseStateWrapper.purchased, +); + +void main() { + group('PurchaseWrapper', () { + test('converts from map', () { + final PurchaseWrapper expected = dummyPurchase; + final PurchaseWrapper parsed = + PurchaseWrapper.fromJson(buildPurchaseMap(expected)); + + expect(parsed, equals(expected)); + }); + + test('toPurchaseDetails() should return correct PurchaseDetail object', () { + final GooglePlayPurchaseDetails details = + GooglePlayPurchaseDetails.fromPurchase(dummyPurchase); + expect(details.purchaseID, dummyPurchase.orderId); + expect(details.productID, dummyPurchase.sku); + expect(details.transactionDate, dummyPurchase.purchaseTime.toString()); + expect(details.verificationData, isNotNull); + expect(details.verificationData.source, kIAPSource); + expect(details.verificationData.localVerificationData, + dummyPurchase.originalJson); + expect(details.verificationData.serverVerificationData, + dummyPurchase.purchaseToken); + expect(details.billingClientPurchase, dummyPurchase); + expect(details.pendingCompletePurchase, true); + }); + }); + + group('PurchaseHistoryRecordWrapper', () { + test('converts from map', () { + final PurchaseHistoryRecordWrapper expected = dummyPurchaseHistoryRecord; + final PurchaseHistoryRecordWrapper parsed = + PurchaseHistoryRecordWrapper.fromJson( + buildPurchaseHistoryRecordMap(expected)); + + expect(parsed, equals(expected)); + }); + }); + + group('PurchasesResultWrapper', () { + test('parsed from map', () { + final BillingResponse responseCode = BillingResponse.ok; + final List purchases = [ + dummyPurchase, + dummyPurchase + ]; + const String debugMessage = 'dummy Message'; + final BillingResultWrapper billingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + final PurchasesResultWrapper expected = PurchasesResultWrapper( + billingResult: billingResult, + responseCode: responseCode, + purchasesList: purchases); + final PurchasesResultWrapper parsed = + PurchasesResultWrapper.fromJson({ + 'billingResult': buildBillingResultMap(billingResult), + 'responseCode': BillingResponseConverter().toJson(responseCode), + 'purchasesList': >[ + buildPurchaseMap(dummyPurchase), + buildPurchaseMap(dummyPurchase) + ] + }); + expect(parsed.billingResult, equals(expected.billingResult)); + expect(parsed.responseCode, equals(expected.responseCode)); + expect(parsed.purchasesList, containsAll(expected.purchasesList)); + }); + + test('parsed from empty map', () { + final PurchasesResultWrapper parsed = + PurchasesResultWrapper.fromJson({}); + expect( + parsed.billingResult, + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + expect(parsed.responseCode, BillingResponse.error); + expect(parsed.purchasesList, isEmpty); + }); + }); + + group('PurchasesHistoryResult', () { + test('parsed from map', () { + final BillingResponse responseCode = BillingResponse.ok; + final List purchaseHistoryRecordList = + [ + dummyPurchaseHistoryRecord, + dummyPurchaseHistoryRecord + ]; + const String debugMessage = 'dummy Message'; + final BillingResultWrapper billingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + final PurchasesHistoryResult expected = PurchasesHistoryResult( + billingResult: billingResult, + purchaseHistoryRecordList: purchaseHistoryRecordList); + final PurchasesHistoryResult parsed = + PurchasesHistoryResult.fromJson({ + 'billingResult': buildBillingResultMap(billingResult), + 'purchaseHistoryRecordList': >[ + buildPurchaseHistoryRecordMap(dummyPurchaseHistoryRecord), + buildPurchaseHistoryRecordMap(dummyPurchaseHistoryRecord) + ] + }); + expect(parsed.billingResult, equals(billingResult)); + expect(parsed.purchaseHistoryRecordList, + containsAll(expected.purchaseHistoryRecordList)); + }); + + test('parsed from empty map', () { + final PurchasesHistoryResult parsed = + PurchasesHistoryResult.fromJson({}); + expect( + parsed.billingResult, + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + expect(parsed.purchaseHistoryRecordList, isEmpty); + }); + }); +} + +Map buildPurchaseMap(PurchaseWrapper original) { + return { + 'orderId': original.orderId, + 'packageName': original.packageName, + 'purchaseTime': original.purchaseTime, + 'signature': original.signature, + 'sku': original.sku, + 'purchaseToken': original.purchaseToken, + 'isAutoRenewing': original.isAutoRenewing, + 'originalJson': original.originalJson, + 'developerPayload': original.developerPayload, + 'purchaseState': PurchaseStateConverter().toJson(original.purchaseState), + 'isAcknowledged': original.isAcknowledged, + 'obfuscatedAccountId': original.obfuscatedAccountId, + 'obfuscatedProfileId': original.obfuscatedProfileId, + }; +} + +Map buildPurchaseHistoryRecordMap( + PurchaseHistoryRecordWrapper original) { + return { + 'purchaseTime': original.purchaseTime, + 'signature': original.signature, + 'sku': original.sku, + 'purchaseToken': original.purchaseToken, + 'originalJson': original.originalJson, + 'developerPayload': original.developerPayload, + }; +} + +Map buildBillingResultMap(BillingResultWrapper original) { + return { + 'responseCode': 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_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_test.dart new file mode 100644 index 000000000000..ead6d26576f3 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_test.dart @@ -0,0 +1,149 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +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( + description: 'description', + freeTrialPeriod: 'freeTrialPeriod', + introductoryPrice: 'introductoryPrice', + introductoryPriceMicros: 'introductoryPriceMicros', + introductoryPriceCycles: 1, + introductoryPricePeriod: 'introductoryPricePeriod', + price: 'price', + priceAmountMicros: 1000, + priceCurrencyCode: 'priceCurrencyCode', + sku: 'sku', + subscriptionPeriod: 'subscriptionPeriod', + title: 'title', + type: SkuType.inapp, + originalPrice: 'originalPrice', + originalPriceAmountMicros: 1000, +); + +void main() { + group('SkuDetailsWrapper', () { + test('converts from map', () { + final SkuDetailsWrapper expected = dummySkuDetails; + final SkuDetailsWrapper parsed = + SkuDetailsWrapper.fromJson(buildSkuMap(expected)); + + expect(parsed, equals(expected)); + }); + }); + + group('SkuDetailsResponseWrapper', () { + test('parsed from map', () { + final BillingResponse responseCode = BillingResponse.ok; + const String debugMessage = 'dummy message'; + final List skusDetails = [ + dummySkuDetails, + dummySkuDetails + ]; + BillingResultWrapper result = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + final SkuDetailsResponseWrapper expected = SkuDetailsResponseWrapper( + billingResult: result, skuDetailsList: skusDetails); + + final SkuDetailsResponseWrapper parsed = + SkuDetailsResponseWrapper.fromJson({ + 'billingResult': { + 'responseCode': BillingResponseConverter().toJson(responseCode), + 'debugMessage': debugMessage, + }, + 'skuDetailsList': >[ + buildSkuMap(dummySkuDetails), + buildSkuMap(dummySkuDetails) + ] + }); + + expect(parsed.billingResult, equals(expected.billingResult)); + expect(parsed.skuDetailsList, containsAll(expected.skuDetailsList)); + }); + + test('toProductDetails() should return correct Product object', () { + final SkuDetailsWrapper wrapper = + SkuDetailsWrapper.fromJson(buildSkuMap(dummySkuDetails)); + final GooglePlayProductDetails product = + GooglePlayProductDetails.fromSkuDetails(wrapper); + expect(product.title, wrapper.title); + expect(product.description, wrapper.description); + expect(product.id, wrapper.sku); + expect(product.price, wrapper.price); + expect(product.skuDetails, wrapper); + }); + + test('handles empty list of skuDetails', () { + final BillingResponse responseCode = BillingResponse.error; + const String debugMessage = 'dummy message'; + final List skusDetails = []; + BillingResultWrapper billingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + final SkuDetailsResponseWrapper expected = SkuDetailsResponseWrapper( + billingResult: billingResult, skuDetailsList: skusDetails); + + final SkuDetailsResponseWrapper parsed = + SkuDetailsResponseWrapper.fromJson({ + 'billingResult': { + 'responseCode': BillingResponseConverter().toJson(responseCode), + 'debugMessage': debugMessage, + }, + 'skuDetailsList': >[] + }); + + expect(parsed.billingResult, equals(expected.billingResult)); + expect(parsed.skuDetailsList, containsAll(expected.skuDetailsList)); + }); + + test('fromJson creates an object with default values', () { + final SkuDetailsResponseWrapper skuDetails = + SkuDetailsResponseWrapper.fromJson({}); + expect( + skuDetails.billingResult, + equals(BillingResultWrapper( + responseCode: BillingResponse.error, + debugMessage: kInvalidBillingResultErrorMessage))); + expect(skuDetails.skuDetailsList, isEmpty); + }); + }); + + group('BillingResultWrapper', () { + test('fromJson on empty map creates an object with default values', () { + final BillingResultWrapper billingResult = + BillingResultWrapper.fromJson({}); + expect(billingResult.debugMessage, kInvalidBillingResultErrorMessage); + expect(billingResult.responseCode, BillingResponse.error); + }); + + test('fromJson on null creates an object with default values', () { + final BillingResultWrapper billingResult = + BillingResultWrapper.fromJson(null); + expect(billingResult.debugMessage, kInvalidBillingResultErrorMessage); + expect(billingResult.responseCode, BillingResponse.error); + }); + }); +} + +Map buildSkuMap(SkuDetailsWrapper original) { + return { + 'description': original.description, + 'freeTrialPeriod': original.freeTrialPeriod, + 'introductoryPrice': original.introductoryPrice, + 'introductoryPriceMicros': original.introductoryPriceMicros, + 'introductoryPriceCycles': original.introductoryPriceCycles, + 'introductoryPricePeriod': original.introductoryPricePeriod, + 'price': original.price, + 'priceAmountMicros': original.priceAmountMicros, + 'priceCurrencyCode': original.priceCurrencyCode, + 'sku': original.sku, + 'subscriptionPeriod': original.subscriptionPeriod, + 'title': original.title, + 'type': original.type.toString().substring(8), + 'originalPrice': original.originalPrice, + 'originalPriceAmountMicros': original.originalPriceAmountMicros, + }; +} 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 new file mode 100644 index 000000000000..36958d277f18 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_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/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'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); + late InAppPurchaseAndroidPlatformAddition iapAndroidPlatformAddition; + const String startConnectionCall = + 'BillingClient#startConnection(BillingClientStateListener)'; + const String endConnectionCall = 'BillingClient#endConnection()'; + + setUpAll(() { + channel.setMockMethodCallHandler(stubPlatform.fakeMethodCallHandler); + }); + + setUp(() { + widgets.WidgetsFlutterBinding.ensureInitialized(); + + InAppPurchaseAndroidPlatformAddition.enablePendingPurchases(); + + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: startConnectionCall, + value: buildBillingResultMap(expectedBillingResult)); + stubPlatform.addResponse(name: endConnectionCall, value: null); + iapAndroidPlatformAddition = + InAppPurchaseAndroidPlatformAddition(BillingClient((_) {})); + }); + + group('consume purchases', () { + const String consumeMethodName = + 'BillingClient#consumeAsync(String, ConsumeResponseListener)'; + test('consume purchase async success', () async { + final BillingResponse expectedCode = BillingResponse.ok; + const String debugMessage = 'dummy message'; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: expectedCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: consumeMethodName, + value: buildBillingResultMap(expectedBillingResult), + ); + final BillingResultWrapper billingResultWrapper = + await iapAndroidPlatformAddition.consumePurchase( + GooglePlayPurchaseDetails.fromPurchase(dummyPurchase)); + + expect(billingResultWrapper, equals(expectedBillingResult)); + }); + }); + + group('queryPastPurchase', () { + group('queryPurchaseDetails', () { + const String queryMethodName = 'BillingClient#queryPurchases(String)'; + test('handles error', () async { + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.developerError; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + + stubPlatform + .addResponse(name: queryMethodName, value: { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'responseCode': BillingResponseConverter().toJson(responseCode), + 'purchasesList': >[] + }); + final QueryPurchaseDetailsResponse response = + await iapAndroidPlatformAddition.queryPastPurchases(); + expect(response.pastPurchases, isEmpty); + expect(response.error, isNotNull); + expect( + response.error!.message, BillingResponse.developerError.toString()); + expect(response.error!.source, kIAPSource); + }); + + test('returns SkuDetailsResponseWrapper', () async { + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + + stubPlatform + .addResponse(name: queryMethodName, value: { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'responseCode': BillingResponseConverter().toJson(responseCode), + 'purchasesList': >[ + buildPurchaseMap(dummyPurchase), + ] + }); + + // Since queryPastPurchases makes 2 platform method calls (one for each SkuType), the result will contain 2 dummyWrapper instead + // of 1. + final QueryPurchaseDetailsResponse response = + await iapAndroidPlatformAddition.queryPastPurchases(); + expect(response.error, isNull); + expect(response.pastPurchases.first.purchaseID, dummyPurchase.orderId); + }); + + test('should store platform exception in the response', () async { + const String debugMessage = 'dummy message'; + + final BillingResponse responseCode = BillingResponse.developerError; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: queryMethodName, + value: { + 'responseCode': BillingResponseConverter().toJson(responseCode), + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'purchasesList': >[] + }, + additionalStepBeforeReturn: (_) { + throw PlatformException( + code: 'error_code', + message: 'error_message', + details: {'info': 'error_info'}, + ); + }); + final QueryPurchaseDetailsResponse response = + await iapAndroidPlatformAddition.queryPastPurchases(); + expect(response.pastPurchases, isEmpty); + expect(response.error, isNotNull); + expect(response.error!.code, 'error_code'); + expect(response.error!.message, 'error_message'); + expect(response.error!.details, {'info': 'error_info'}); + }); + }); + }); +} 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 new file mode 100644 index 000000000000..01c73d6ed43e --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart @@ -0,0 +1,646 @@ +// Copyright 2013 The Flutter Authors. All 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' 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'; +import 'billing_client_wrappers/sku_details_wrapper_test.dart'; +import 'stub_in_app_purchase_platform.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); + late InAppPurchaseAndroidPlatform iapAndroidPlatform; + const String startConnectionCall = + 'BillingClient#startConnection(BillingClientStateListener)'; + const String endConnectionCall = 'BillingClient#endConnection()'; + + setUpAll(() { + channel.setMockMethodCallHandler(stubPlatform.fakeMethodCallHandler); + }); + + setUp(() { + widgets.WidgetsFlutterBinding.ensureInitialized(); + + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.ok; + final 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; + }); + + tearDown(() { + stubPlatform.reset(); + }); + + group('connection management', () { + test('connects on initialization', () { + //await iapAndroidPlatform.isAvailable(); + expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(1)); + }); + }); + + group('isAvailable', () { + test('true', () async { + stubPlatform.addResponse(name: 'BillingClient#isReady()', value: true); + expect(await iapAndroidPlatform.isAvailable(), isTrue); + }); + + test('false', () async { + stubPlatform.addResponse(name: 'BillingClient#isReady()', value: false); + expect(await iapAndroidPlatform.isAvailable(), isFalse); + }); + }); + + group('querySkuDetails', () { + final 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( + responseCode: responseCode, debugMessage: debugMessage); + stubPlatform.addResponse(name: queryMethodName, value: { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'skuDetailsList': [], + }); + + final ProductDetailsResponse response = + await iapAndroidPlatform.queryProductDetails([''].toSet()); + 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( + responseCode: responseCode, debugMessage: debugMessage); + stubPlatform.addResponse(name: queryMethodName, value: { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'skuDetailsList': >[buildSkuMap(dummySkuDetails)] + }); + // 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()); + expect(response.productDetails.first.title, dummySkuDetails.title); + expect(response.productDetails.first.description, + dummySkuDetails.description); + expect(response.productDetails.first.price, dummySkuDetails.price); + }); + + test('should get the correct notFoundIDs', () async { + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + stubPlatform.addResponse(name: queryMethodName, value: { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'skuDetailsList': >[buildSkuMap(dummySkuDetails)] + }); + // 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.first, 'invalid'); + }); + + test( + 'should have error stored in the response when platform exception is thrown', + () async { + final BillingResponse responseCode = BillingResponse.ok; + stubPlatform.addResponse( + name: queryMethodName, + value: { + 'responseCode': BillingResponseConverter().toJson(responseCode), + 'skuDetailsList': >[ + buildSkuMap(dummySkuDetails) + ] + }, + additionalStepBeforeReturn: (_) { + throw PlatformException( + code: 'error_code', + message: 'error_message', + 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']); + 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'}); + }); + }); + + group('restorePurchases', () { + const String queryMethodName = 'BillingClient#queryPurchases(String)'; + test('handles error', () async { + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.developerError; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + + stubPlatform.addResponse(name: queryMethodName, value: { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'responseCode': BillingResponseConverter().toJson(responseCode), + 'purchasesList': >[] + }); + + expect( + iapAndroidPlatform.restorePurchases(), + throwsA( + isA() + .having((e) => e.source, 'source', kIAPSource) + .having((e) => e.code, 'code', kRestoredPurchaseErrorCode) + .having((e) => e.message, 'message', responseCode.toString()), + ), + ); + }); + + test('should store platform exception in the response', () async { + const String debugMessage = 'dummy message'; + + final BillingResponse responseCode = BillingResponse.developerError; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: queryMethodName, + value: { + 'responseCode': BillingResponseConverter().toJson(responseCode), + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'purchasesList': >[] + }, + additionalStepBeforeReturn: (_) { + throw PlatformException( + code: 'error_code', + message: 'error_message', + details: {'info': 'error_info'}, + ); + }); + + expect( + 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'}), + ), + ); + }); + + test('returns SkuDetailsResponseWrapper', () async { + Completer completer = Completer(); + Stream> stream = iapAndroidPlatform.purchaseStream; + + late StreamSubscription subscription; + subscription = stream.listen((purchaseDetailsList) { + if (purchaseDetailsList.first.status == PurchaseStatus.restored) { + completer.complete(purchaseDetailsList); + subscription.cancel(); + } + }); + + const String debugMessage = 'dummy message'; + final BillingResponse responseCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: responseCode, debugMessage: debugMessage); + + stubPlatform.addResponse(name: queryMethodName, value: { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'responseCode': BillingResponseConverter().toJson(responseCode), + 'purchasesList': >[ + buildPurchaseMap(dummyPurchase), + ] + }); + + // Since queryPastPurchases makes 2 platform method calls (one for each + // SkuType), the result will contain 2 dummyPurchase instances instead + // of 1. + await iapAndroidPlatform.restorePurchases(); + final List restoredPurchases = await completer.future; + + expect(restoredPurchases.length, 2); + restoredPurchases.forEach((element) { + GooglePlayPurchaseDetails purchase = + element as GooglePlayPurchaseDetails; + + expect(purchase.productID, dummyPurchase.sku); + expect(purchase.purchaseID, dummyPurchase.orderId); + expect(purchase.verificationData.localVerificationData, + dummyPurchase.originalJson); + expect(purchase.verificationData.serverVerificationData, + dummyPurchase.purchaseToken); + expect(purchase.verificationData.source, kIAPSource); + expect(purchase.transactionDate, dummyPurchase.purchaseTime.toString()); + expect(purchase.billingClientPurchase, dummyPurchase); + expect(purchase.status, PurchaseStatus.restored); + }); + }); + }); + + group('make payment', () { + final 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 String debugMessage = 'dummy message'; + final BillingResponse sentCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: sentCode, debugMessage: debugMessage); + + stubPlatform.addResponse( + name: launchMethodName, + value: buildBillingResultMap(expectedBillingResult), + additionalStepBeforeReturn: (_) { + // Mock java update purchase callback. + MethodCall call = MethodCall(kOnPurchasesUpdated, { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'responseCode': 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); + }); + Completer completer = Completer(); + PurchaseDetails purchaseDetails; + 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); + final bool launchResult = await iapAndroidPlatform.buyNonConsumable( + purchaseParam: purchaseParam); + + PurchaseDetails result = await completer.future; + expect(launchResult, isTrue); + expect(result.purchaseID, 'orderID1'); + expect(result.status, PurchaseStatus.purchased); + expect(result.productID, dummySkuDetails.sku); + }); + + test('handles an error with an empty purchases list', () async { + final SkuDetailsWrapper skuDetails = dummySkuDetails; + final String accountId = "hashedAccountId"; + const String debugMessage = 'dummy message'; + final BillingResponse sentCode = BillingResponse.error; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: sentCode, debugMessage: debugMessage); + + stubPlatform.addResponse( + name: launchMethodName, + value: buildBillingResultMap(expectedBillingResult), + additionalStepBeforeReturn: (_) { + // Mock java update purchase callback. + MethodCall call = MethodCall(kOnPurchasesUpdated, { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'responseCode': BillingResponseConverter().toJson(sentCode), + 'purchasesList': [] + }); + iapAndroidPlatform.billingClient.callHandler(call); + }); + Completer completer = Completer(); + PurchaseDetails purchaseDetails; + 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.buyNonConsumable(purchaseParam: purchaseParam); + PurchaseDetails result = await completer.future; + + expect(result.error, isNotNull); + expect(result.error!.source, kIAPSource); + expect(result.status, PurchaseStatus.error); + expect(result.purchaseID, isEmpty); + }); + + test('buy consumable with auto consume, serializes and deserializes data', + () async { + final SkuDetailsWrapper skuDetails = dummySkuDetails; + final String accountId = "hashedAccountId"; + const String debugMessage = 'dummy message'; + final BillingResponse sentCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: sentCode, debugMessage: debugMessage); + + stubPlatform.addResponse( + name: launchMethodName, + value: buildBillingResultMap(expectedBillingResult), + additionalStepBeforeReturn: (_) { + // Mock java update purchase callback. + MethodCall call = MethodCall(kOnPurchasesUpdated, { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'responseCode': 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); + }); + Completer consumeCompleter = Completer(); + // adding call back for consume purchase + final BillingResponse expectedCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResultForConsume = + BillingResultWrapper( + responseCode: expectedCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: consumeMethodName, + value: buildBillingResultMap(expectedBillingResultForConsume), + additionalStepBeforeReturn: (dynamic args) { + String purchaseToken = args['purchaseToken']; + consumeCompleter.complete((purchaseToken)); + }); + + Completer completer = Completer(); + PurchaseDetails purchaseDetails; + 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); + final bool launchResult = + await iapAndroidPlatform.buyConsumable(purchaseParam: purchaseParam); + + // Verify that the result has succeeded + GooglePlayPurchaseDetails result = await completer.future; + expect(launchResult, isTrue); + expect(result.billingClientPurchase, isNotNull); + expect(result.billingClientPurchase.purchaseToken, + await consumeCompleter.future); + expect(result.status, PurchaseStatus.purchased); + expect(result.error, isNull); + }); + + test('buyNonConsumable propagates failures to launch the billing flow', + () async { + const String debugMessage = 'dummy message'; + final BillingResponse sentCode = BillingResponse.error; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: sentCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: launchMethodName, + value: buildBillingResultMap(expectedBillingResult)); + + final bool result = await iapAndroidPlatform.buyNonConsumable( + purchaseParam: GooglePlayPurchaseParam( + productDetails: + GooglePlayProductDetails.fromSkuDetails(dummySkuDetails))); + + // Verify that the failure has been converted and returned + expect(result, isFalse); + }); + + test('buyConsumable propagates failures to launch the billing flow', + () async { + const String debugMessage = 'dummy message'; + final BillingResponse sentCode = BillingResponse.developerError; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: sentCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: launchMethodName, + value: buildBillingResultMap(expectedBillingResult), + ); + + final bool result = await iapAndroidPlatform.buyConsumable( + purchaseParam: GooglePlayPurchaseParam( + productDetails: + GooglePlayProductDetails.fromSkuDetails(dummySkuDetails))); + + // Verify that the failure has been converted and returned + expect(result, isFalse); + }); + + test('adds consumption failures to PurchaseDetails objects', () async { + final SkuDetailsWrapper skuDetails = dummySkuDetails; + final String accountId = "hashedAccountId"; + const String debugMessage = 'dummy message'; + final BillingResponse sentCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: sentCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: launchMethodName, + value: buildBillingResultMap(expectedBillingResult), + additionalStepBeforeReturn: (_) { + // Mock java update purchase callback. + MethodCall call = MethodCall(kOnPurchasesUpdated, { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'responseCode': 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); + }); + Completer consumeCompleter = Completer(); + // adding call back for consume purchase + final BillingResponse expectedCode = BillingResponse.error; + final BillingResultWrapper expectedBillingResultForConsume = + BillingResultWrapper( + responseCode: expectedCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: consumeMethodName, + value: buildBillingResultMap(expectedBillingResultForConsume), + additionalStepBeforeReturn: (dynamic args) { + String purchaseToken = args['purchaseToken']; + consumeCompleter.complete(purchaseToken); + }); + + Completer completer = Completer(); + PurchaseDetails purchaseDetails; + 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 + GooglePlayPurchaseDetails result = await completer.future; + expect(result.billingClientPurchase, isNotNull); + expect(result.billingClientPurchase.purchaseToken, + await consumeCompleter.future); + expect(result.status, PurchaseStatus.error); + expect(result.error, isNotNull); + expect(result.error!.code, kConsumptionFailedErrorCode); + }); + + test( + 'buy consumable without auto consume, consume api should not receive calls', + () async { + final SkuDetailsWrapper skuDetails = dummySkuDetails; + final String accountId = "hashedAccountId"; + const String debugMessage = 'dummy message'; + final BillingResponse sentCode = BillingResponse.developerError; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: sentCode, debugMessage: debugMessage); + + stubPlatform.addResponse( + name: launchMethodName, + value: buildBillingResultMap(expectedBillingResult), + additionalStepBeforeReturn: (_) { + // Mock java update purchase callback. + MethodCall call = MethodCall(kOnPurchasesUpdated, { + 'billingResult': buildBillingResultMap(expectedBillingResult), + 'responseCode': 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); + }); + Completer consumeCompleter = Completer(); + // adding call back for consume purchase + final BillingResponse expectedCode = BillingResponse.ok; + final BillingResultWrapper expectedBillingResultForConsume = + BillingResultWrapper( + responseCode: expectedCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: consumeMethodName, + value: buildBillingResultMap(expectedBillingResultForConsume), + additionalStepBeforeReturn: (dynamic args) { + String purchaseToken = args['purchaseToken']; + consumeCompleter.complete((purchaseToken)); + }); + + Stream purchaseStream = iapAndroidPlatform.purchaseStream; + late StreamSubscription subscription; + subscription = purchaseStream.listen((_) { + consumeCompleter.complete(null); + subscription.cancel(); + }, onDone: () {}); + final GooglePlayPurchaseParam purchaseParam = GooglePlayPurchaseParam( + productDetails: GooglePlayProductDetails.fromSkuDetails(skuDetails), + applicationUserName: accountId); + await iapAndroidPlatform.buyConsumable( + purchaseParam: purchaseParam, autoConsume: false); + expect(null, await consumeCompleter.future); + }); + }); + + group('complete purchase', () { + const String completeMethodName = + 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)'; + test('complete purchase success', () async { + final BillingResponse expectedCode = BillingResponse.ok; + const String debugMessage = 'dummy message'; + final BillingResultWrapper expectedBillingResult = BillingResultWrapper( + responseCode: expectedCode, debugMessage: debugMessage); + stubPlatform.addResponse( + name: completeMethodName, + value: buildBillingResultMap(expectedBillingResult), + ); + PurchaseDetails purchaseDetails = + GooglePlayPurchaseDetails.fromPurchase(dummyUnacknowledgedPurchase); + Completer completer = Completer(); + purchaseDetails.status = PurchaseStatus.purchased; + if (purchaseDetails.pendingCompletePurchase) { + final BillingResultWrapper billingResultWrapper = + await iapAndroidPlatform.completePurchase(purchaseDetails); + expect(billingResultWrapper, equals(expectedBillingResult)); + completer.complete(billingResultWrapper); + } + expect(await completer.future, equals(expectedBillingResult)); + }); + }); +} 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 new file mode 100644 index 000000000000..11a3426335d5 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/test/stub_in_app_purchase_platform.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 'dart:async'; +import 'package:flutter/services.dart'; + +typedef void AdditionalSteps(dynamic args); + +class StubInAppPurchasePlatform { + Map _expectedCalls = {}; + Map _additionalSteps = {}; + void addResponse( + {required String name, + dynamic value, + AdditionalSteps? additionalStepBeforeReturn}) { + _additionalSteps[name] = additionalStepBeforeReturn; + _expectedCalls[name] = value; + } + + List _previousCalls = []; + List get previousCalls => _previousCalls; + MethodCall previousCallMatching(String name) => + _previousCalls.firstWhere((MethodCall call) => call.method == name); + int countPreviousCalls(String name) => + _previousCalls.where((MethodCall call) => call.method == name).length; + + void reset() { + _expectedCalls.clear(); + _previousCalls.clear(); + _additionalSteps.clear(); + } + + Future fakeMethodCallHandler(MethodCall call) async { + _previousCalls.add(call); + if (_expectedCalls.containsKey(call.method)) { + if (_additionalSteps[call.method] != null) { + _additionalSteps[call.method]!(call.arguments); + } + return Future.sync(() => _expectedCalls[call.method]); + } else { + return Future.sync(() => null); + } + } +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md new file mode 100644 index 000000000000..480426cf5e54 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md @@ -0,0 +1,7 @@ +## 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. \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_ios/LICENSE b/packages/in_app_purchase/in_app_purchase_ios/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_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/in_app_purchase/in_app_purchase_ios/README.md b/packages/in_app_purchase/in_app_purchase_ios/README.md new file mode 100644 index 000000000000..46839b5ee3ec --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/README.md @@ -0,0 +1,48 @@ +# in_app_purchase_ios + +The iOS implementation of [`in_app_purchase`][1]. + +## Usage + +### Import the package + +This package has been endorsed, meaning that you only need to add `in_app_purchase` +as a dependency in your `pubspec.yaml`. It will be automatically included in your app +when you depend on `package:in_app_purchase`. + +This is what the above means to your `pubspec.yaml`: + +```yaml +... +dependencies: + ... + in_app_purchase: ^0.6.0 + ... +``` + +If you wish to use the iOS package only, you can add `in_app_purchase_ios` as a +dependency: + +```yaml +... +dependencies: + ... + in_app_purchase_ios: ^1.0.0 + ... +``` + +## 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/in_app_purchase \ No newline at end of file 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 new file mode 100644 index 000000000000..5aeb4e7c5e21 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/analysis_options.yaml @@ -0,0 +1 @@ +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 new file mode 100644 index 000000000000..e15cf14b85fd --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/build.yaml @@ -0,0 +1,7 @@ +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/README.md b/packages/in_app_purchase/in_app_purchase_ios/example/README.md new file mode 100644 index 000000000000..9cf98bf02e79 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/README.md @@ -0,0 +1,75 @@ +# In App Purchase iOS Example + +Demonstrates how to use the In App Purchase iOS (IAP) Plugin. + +## Getting Started + +### Preparation + +There's a significant amount of setup required for testing in app purchases +successfully, including registering new app IDs and store entries to use for +testing in App Store Connect. The App Store requires developers to configure +an app with in-app items for purchase to call their in-app-purchase APIs. +The App Store has extensive documentation on how to do this, and we've also +included a high level guide below. + +* [In-App Purchase (App Store)](https://developer.apple.com/in-app-purchase/) + +### iOS + +When using Xcode 12 and iOS 14 or higher you can run the example in the simulator or on a device without +having to configure an App in App Store Connect. The example app is set up to use StoreKit Testing configured +in the `example/ios/Runner/Configuration.storekit` file (as documented in the article [Setting Up StoreKit Testing in Xcode](https://developer.apple.com/documentation/xcode/setting_up_storekit_testing_in_xcode?language=objc)). +To run the application take the following steps (note that it will only work when running from Xcode): + +1. Open the example app with Xcode, `File > Open File` `example/ios/Runner.xcworkspace`; + +2. Within Xcode edit the current scheme, `Product > Scheme > Edit Scheme...` (or press `Command + Shift + ,`); + +3. Enable StoreKit testing: + a. Select the `Run` action; + b. Click `Options` in the action settings; + c. Select the `Configuration.storekit` for the StoreKit Configuration option. + +4. Click the `Close` button to close the scheme editor; + +5. Select the device you want to run the example App on; + +6. Run the application using `Product > Run` (or hit the run button). + +When testing on pre-iOS 14 you can't run the example app on a simulator and you will need to configure an app in App Store Connect. You can do so by following the steps below: + +1. Follow ["Workflow for configuring in-app + purchases"](https://help.apple.com/app-store-connect/#/devb57be10e7), a + detailed guide on all the steps needed to enable IAPs for an app. Complete + steps 1 ("Sign a Paid Applications Agreement") and 2 ("Configure in-app + purchases"). + + For step #2, "Configure in-app purchases in App Store Connect," you'll want + to create the following products: + + - A consumable with product ID `consumable` + - An upgrade with product ID `upgrade` + - An auto-renewing subscription with product ID `subscription_silver` + - An non-renewing subscription with product ID `subscription_gold` + +2. In XCode, `File > Open File` `example/ios/Runner.xcworkspace`. Update the + Bundle ID to match the Bundle ID of the app created in step #1. + +3. [Create a Sandbox tester + account](https://help.apple.com/app-store-connect/#/dev8b997bee1) to test the + in-app purchases with. + +4. Use `flutter run` to install the app and test it. Note that you need to test + it on a real device instead of a simulator. Next click on one of the products + in the example App, this enables the "SANDBOX ACCOUNT" section in the iOS + settings. You will now be asked to sign in with your sandbox test account to + complete the purchase (no worries you won't be charged). If for some reason + you aren't asked to sign-in or the wrong user is listed, go into the iOS + settings ("Settings" -> "App Store" -> "SANDBOX ACCOUNT") and update your + sandbox account from there. This procedure is explained in great detail in + the [Testing In-App Purchases with Sandbox](https://developer.apple.com/documentation/storekit/in-app_purchase/testing_in-app_purchases_with_sandbox?language=objc) article. + + +**Important:** signing into any production service (including iTunes!) with the +sandbox test account will permanently invalidate it. 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_ios/example/integration_test/in_app_purchase_test.dart new file mode 100644 index 000000000000..3d68d0f1f4f0 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/integration_test/in_app_purchase_test.dart @@ -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. + +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:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('Can create InAppPurchaseAndroid instance', + (WidgetTester tester) async { + InAppPurchaseIosPlatform.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_ios/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..9367d483e44e --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 8.0 + + diff --git a/packages/android_intent/example/ios/Flutter/Debug.xcconfig b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/android_intent/example/ios/Flutter/Debug.xcconfig rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/Debug.xcconfig diff --git a/packages/android_intent/example/ios/Flutter/Release.xcconfig b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/android_intent/example/ios/Flutter/Release.xcconfig rename to packages/in_app_purchase/in_app_purchase_ios/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_ios/example/ios/Podfile new file mode 100644 index 000000000000..ae8750242a6e --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_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 + inherit! :search_paths + + # Matches in_app_purchase test_spec dependency. + 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/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 new file mode 100644 index 000000000000..590b07f0d385 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,672 @@ +// !$*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 */; }; + 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 */; }; + AB7252348F077C046D6617D3 /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 630DD71BB3F145A22B1DE15D /* libPods-RunnerTests.a */; }; + 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 */ + 027D04BC80EACAAB3B5232B8 /* 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; }; + 194D4829A79EF6C7426B39F7 /* 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 = ""; }; + 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 = ""; }; + 630DD71BB3F145A22B1DE15D /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; + 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 = ""; }; + 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 = ( + AB7252348F077C046D6617D3 /* 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 */, + 194D4829A79EF6C7426B39F7 /* Pods-RunnerTests.debug.xcconfig */, + 027D04BC80EACAAB3B5232B8 /* 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 */, + ); + path = RunnerTests; + sourceTree = ""; + }; + E4DB99639FAD8ADED6B572FC /* Frameworks */ = { + isa = PBXGroup; + children = ( + A5279297219369C600FF69E6 /* StoreKit.framework */, + 1630769A874F9381BC761FE1 /* libPods-Runner.a */, + 630DD71BB3F145A22B1DE15D /* 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 = ( + 321E2F5767F55B0A360AA77E /* [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 */ + 321E2F5767F55B0A360AA77E /* [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"; + }; + 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 */, + 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 = 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; + 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 = 194D4829A79EF6C7426B39F7 /* 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 = 027D04BC80EACAAB3B5232B8 /* 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_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + 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 new file mode 100644 index 000000000000..3bd47ecb9ec0 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/camera/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/camera/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/AppDelegate.h b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..0681d288bb70 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_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/in_app_purchase/in_app_purchase_ios/example/ios/Runner/AppDelegate.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..30b87969f44a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/AppDelegate.m @@ -0,0 +1,17 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "AppDelegate.h" +#include "GeneratedPluginRegistrant.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GeneratedPluginRegistrant registerWithRegistry:self]; + // Override point for customization after application launch. + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/in_app_purchase/in_app_purchase_ios/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_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 000000000000..3d43d11e66f4 Binary files /dev/null and b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from packages/camera/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from packages/espresso/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/packages/espresso/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from packages/espresso/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/camera/example/ios/Runner/Base.lproj/Main.storyboard b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/camera/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/in_app_purchase/in_app_purchase_ios/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_ios/example/ios/Runner/Configuration.storekit new file mode 100644 index 000000000000..4958a846e67d --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Configuration.storekit @@ -0,0 +1,96 @@ +{ + "products" : [ + { + "displayPrice" : "0.99", + "familyShareable" : false, + "internalID" : "AE10D05D", + "localizations" : [ + { + "description" : "A consumable product.", + "displayName" : "Consumable", + "locale" : "en_US" + } + ], + "productID" : "consumable", + "referenceName" : "consumable", + "type" : "Consumable" + }, + { + "displayPrice" : "10.99", + "familyShareable" : false, + "internalID" : "FABCF067", + "localizations" : [ + { + "description" : "An non-consumable product.", + "displayName" : "Upgrade", + "locale" : "en_US" + } + ], + "productID" : "upgrade", + "referenceName" : "upgrade", + "type" : "NonConsumable" + } + ], + "settings" : { + + }, + "subscriptionGroups" : [ + { + "id" : "D0FEE8D8", + "localizations" : [ + + ], + "name" : "Example Subscriptions", + "subscriptions" : [ + { + "adHocOffers" : [ + + ], + "displayPrice" : "3.99", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "922EB597", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "A lower level subscription.", + "displayName" : "Subscription Silver", + "locale" : "en_US" + } + ], + "productID" : "subscription_silver", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "subscription_silver", + "subscriptionGroupID" : "D0FEE8D8", + "type" : "RecurringSubscription" + }, + { + "adHocOffers" : [ + + ], + "displayPrice" : "5.99", + "familyShareable" : false, + "groupNumber" : 2, + "internalID" : "0BC7FF5E", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "A higher level subscription.", + "displayName" : "Subscription Gold", + "locale" : "en_US" + } + ], + "productID" : "subscription_gold", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "subscription_gold", + "subscriptionGroupID" : "D0FEE8D8", + "type" : "RecurringSubscription" + } + ] + } + ], + "version" : { + "major" : 1, + "minor" : 0 + } +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Info.plist b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..a8f31ba92572 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + in_app_purchase_example + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + + 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 new file mode 100644 index 000000000000..f97b9ef5c8a1 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_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/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 new file mode 100644 index 000000000000..6e436e414aad --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m @@ -0,0 +1,304 @@ +// Copyright 2013 The Flutter Authors. 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) InAppPurchasePlugin* plugin; + +@end + +@implementation InAppPurchasePluginTest + +- (void)setUp { + self.plugin = + [[InAppPurchasePluginStub alloc] initWithReceiptManager:[FIAPReceiptManagerStub new]]; +} + +- (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 (@available(iOS 8.3, *)) { + if (!transaction.payment.simulatesAskToBuyInSandbox) { + [simulatesAskToBuyInSandboxExpectation fulfill]; + } + } else { + [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)testRetrieveReceiptData { + 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]; + NSLog(@"%@", result); + XCTAssertNotNil(result); +} + +- (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 ]); +} + +@end diff --git a/packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Info.plist b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Info.plist similarity index 100% rename from packages/in_app_purchase/example/ios/in_app_purchase_pluginTests/Info.plist rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Info.plist 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 new file mode 100644 index 000000000000..6cfbd278a429 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/PaymentQueueTests.m @@ -0,0 +1,212 @@ +// Copyright 2013 The Flutter Authors. 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/ProductRequestHandlerTests.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/ProductRequestHandlerTests.m new file mode 100644 index 000000000000..16b9462ce11d --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/ProductRequestHandlerTests.m @@ -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 +#import "Stubs.h" + +@import in_app_purchase_ios; + +#pragma tests start here + +@interface RequestHandlerTest : XCTestCase + +@end + +@implementation RequestHandlerTest + +- (void)testRequestHandlerWithProductRequestSuccess { + SKProductRequestStub *request = + [[SKProductRequestStub alloc] initWithProductIdentifiers:[NSSet setWithArray:@[ @"123" ]]]; + FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get response with 1 product"]; + __block SKProductsResponse *response; + [handler + startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable r, NSError *error) { + response = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertNotNil(response); + XCTAssertEqual(response.products.count, 1); + SKProduct *product = response.products.firstObject; + XCTAssertTrue([product.productIdentifier isEqualToString:@"123"]); +} + +- (void)testRequestHandlerWithProductRequestFailure { + SKProductRequestStub *request = [[SKProductRequestStub alloc] + initWithFailureError:[NSError errorWithDomain:@"test" code:123 userInfo:@{}]]; + FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; + XCTestExpectation *expectation = + [self expectationWithDescription:@"expect to get response with 1 product"]; + __block NSError *error; + __block SKProductsResponse *response; + [handler startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable r, NSError *e) { + error = e; + response = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertNotNil(error); + XCTAssertEqual(error.domain, @"test"); + XCTAssertNil(response); +} + +- (void)testRequestHandlerWithRefreshReceiptSuccess { + SKReceiptRefreshRequestStub *request = + [[SKReceiptRefreshRequestStub alloc] initWithReceiptProperties:nil]; + FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; + XCTestExpectation *expectation = [self expectationWithDescription:@"expect no error"]; + __block NSError *e; + [handler + startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable r, NSError *error) { + e = error; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertNil(e); +} + +- (void)testRequestHandlerWithRefreshReceiptFailure { + SKReceiptRefreshRequestStub *request = [[SKReceiptRefreshRequestStub alloc] + initWithFailureError:[NSError errorWithDomain:@"test" code:123 userInfo:@{}]]; + FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; + XCTestExpectation *expectation = [self expectationWithDescription:@"expect error"]; + __block NSError *error; + __block SKProductsResponse *response; + [handler startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable r, NSError *e) { + error = e; + response = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertNotNil(error); + XCTAssertEqual(error.domain, @"test"); + XCTAssertNil(response); +} + +@end diff --git a/packages/in_app_purchase/ios/Tests/Stubs.h b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h similarity index 95% rename from packages/in_app_purchase/ios/Tests/Stubs.h rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h index 630ae2f633dd..60c481980dff 100644 --- a/packages/in_app_purchase/ios/Tests/Stubs.h +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.h @@ -1,11 +1,11 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. 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 in_app_purchase; +@import in_app_purchase_ios; NS_ASSUME_NONNULL_BEGIN API_AVAILABLE(ios(11.2), macos(10.13.2)) diff --git a/packages/in_app_purchase/ios/Tests/Stubs.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m similarity index 99% rename from packages/in_app_purchase/ios/Tests/Stubs.m rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m index 58b77c14127d..66610a88a77d 100644 --- a/packages/in_app_purchase/ios/Tests/Stubs.m +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/Stubs.m @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/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 new file mode 100644 index 000000000000..385d29140e49 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/TranslatorTests.m @@ -0,0 +1,147 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#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; + +@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", + } + }; +} + +- (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.systemLocale; + NSDictionary *map = [FIAObjectTranslator getMapFromNSLocale:system]; + XCTAssertEqualObjects(map[@"currencySymbol"], system.currencySymbol); + } +} + +@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_ios/example/lib/consumable_store.dart new file mode 100644 index 000000000000..4d10a50e1ee8 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/lib/consumable_store.dart @@ -0,0 +1,51 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'package:shared_preferences/shared_preferences.dart'; + +/// 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(); + + /// Adds a consumable with ID `id` to the store. + /// + /// The consumable is only added after the returned Future is complete. + static Future save(String id) { + _writes = _writes.then((void _) => _doSave(id)); + return _writes; + } + + /// Consumes a consumable with ID `id` from the store. + /// + /// The consumable was only consumed after the returned Future is complete. + static Future consume(String id) { + _writes = _writes.then((void _) => _doConsume(id)); + return _writes; + } + + /// 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(); + cached.add(id); + await prefs.setStringList(_kPrefKey, cached); + } + + static Future _doConsume(String id) async { + List cached = await load(); + 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/main.dart b/packages/in_app_purchase/in_app_purchase_ios/example/lib/main.dart new file mode 100644 index 000000000000..5452f5a0ee83 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/lib/main.dart @@ -0,0 +1,406 @@ +// Copyright 2013 The Flutter Authors. All 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_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; + 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. + }); + 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 + ? Icon(Icons.check) + : 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 new file mode 100644 index 000000000000..b916990d3979 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/pubspec.yaml @@ -0,0 +1,31 @@ +name: in_app_purchase_ios_example +description: Demonstrates how to use the in_app_purchase_ios plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.9.1+hotfix.2" + +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/example/test_driver/test/integration_test.dart b/packages/in_app_purchase/in_app_purchase_ios/example/test_driver/test/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/test_driver/test/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/android_intent/ios/Assets/.gitkeep b/packages/in_app_purchase/in_app_purchase_ios/ios/Assets/.gitkeep similarity index 100% rename from packages/android_intent/ios/Assets/.gitkeep rename to packages/in_app_purchase/in_app_purchase_ios/ios/Assets/.gitkeep diff --git a/packages/in_app_purchase/ios/Classes/FIAObjectTranslator.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h similarity index 94% rename from packages/in_app_purchase/ios/Classes/FIAObjectTranslator.h rename to packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h index 5243a391ddaf..2d0187e88aed 100644 --- a/packages/in_app_purchase/ios/Classes/FIAObjectTranslator.h +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/in_app_purchase/ios/Classes/FIAObjectTranslator.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m similarity index 99% rename from packages/in_app_purchase/ios/Classes/FIAObjectTranslator.m rename to packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m index f1e5c538cb0e..5d6e0a244a96 100644 --- a/packages/in_app_purchase/ios/Classes/FIAObjectTranslator.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.m @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/in_app_purchase/ios/Classes/FIAPReceiptManager.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.h similarity index 85% rename from packages/in_app_purchase/ios/Classes/FIAPReceiptManager.h rename to packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.h index c5b67756bad0..94020ff2348b 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.h +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.h @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/in_app_purchase/ios/Classes/FIAPReceiptManager.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m similarity index 83% rename from packages/in_app_purchase/ios/Classes/FIAPReceiptManager.m rename to packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m index 92872d91234e..526364020ad3 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m @@ -1,14 +1,7 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// -// FIAPReceiptManager.m -// in_app_purchase -// -// Created by Chris Yang on 3/2/19. -// - #import "FIAPReceiptManager.h" #import diff --git a/packages/in_app_purchase/ios/Classes/FIAPRequestHandler.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPRequestHandler.h similarity index 90% rename from packages/in_app_purchase/ios/Classes/FIAPRequestHandler.h rename to packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPRequestHandler.h index 892f5f013cc9..cbf21d6e161f 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPRequestHandler.h +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPRequestHandler.h @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/in_app_purchase/ios/Classes/FIAPRequestHandler.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPRequestHandler.m similarity index 95% rename from packages/in_app_purchase/ios/Classes/FIAPRequestHandler.m rename to packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPRequestHandler.m index 5dc2cea2e9db..8767265d8544 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPRequestHandler.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPRequestHandler.m @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h similarity index 95% rename from packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h rename to packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h index 54898d170304..fddeb07e01a3 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -29,6 +29,7 @@ typedef void (^UpdatedDownloads)(NSArray *downloads); // 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. diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.m similarity index 94% rename from packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m rename to packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.m index ecbd237c90ce..eb3348e4b3c9 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.m @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -66,6 +66,14 @@ - (void)restoreTransactions:(nullable NSString *)applicationName { } } +- (void)presentCodeRedemptionSheet { + if (@available(iOS 14, *)) { + [self.queue presentCodeRedemptionSheet]; + } else { + NSLog(@"presentCodeRedemptionSheet is only available on iOS 14 or newer"); + } +} + #pragma mark - observing // Sent when the transaction array has changed (additions or state changes). Client should check diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.h similarity index 88% rename from packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.h rename to packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.h index a7c00f18bc37..8cb42f3fe8c2 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.h +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.h @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m similarity index 89% rename from packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m rename to packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m index 156ce0c33e8f..9034fe6cdd1e 100644 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -75,7 +75,7 @@ - (instancetype)initWithRegistrar:(NSObject *)registrar }]; [_paymentQueueHandler startObservingPaymentQueue]; _callbackChannel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase_callback" + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase" binaryMessenger:[registrar messenger]]; return self; } @@ -93,6 +93,9 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result [self finishTransaction:call result:result]; } else if ([@"-[InAppPurchasePlugin restoreTransactions:result:]" isEqualToString:call.method]) { [self restoreTransactions:call result:result]; + } else if ([@"-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]" + isEqualToString:call.method]) { + [self presentCodeRedemptionSheet:call result:result]; } else if ([@"-[InAppPurchasePlugin retrieveReceiptData:result:]" isEqualToString:call.method]) { [self retrieveReceiptData:call result:result]; } else if ([@"-[InAppPurchasePlugin refreshReceipt:result:]" isEqualToString:call.method]) { @@ -103,7 +106,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } - (void)canMakePayments:(FlutterResult)result { - result([NSNumber numberWithBool:[SKPaymentQueue canMakePayments]]); + result(@([SKPaymentQueue canMakePayments])); } - (void)getPendingTransactions:(FlutterResult)result { @@ -179,8 +182,10 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { NSNumber *quantity = [paymentMap objectForKey:@"quantity"]; payment.quantity = (quantity != nil) ? quantity.integerValue : 1; if (@available(iOS 8.3, *)) { - payment.simulatesAskToBuyInSandbox = - [[paymentMap objectForKey:@"simulatesAskToBuyInSandbox"] boolValue]; + NSNumber *simulatesAskToBuyInSandbox = [paymentMap objectForKey:@"simulatesAskToBuyInSandbox"]; + payment.simulatesAskToBuyInSandbox = (id)simulatesAskToBuyInSandbox == (id)[NSNull null] + ? NO + : [simulatesAskToBuyInSandbox boolValue]; } if (![self.paymentQueueHandler addPayment:payment]) { @@ -197,19 +202,27 @@ - (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { } - (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result { - if (![call.arguments isKindOfClass:[NSString class]]) { + if (![call.arguments isKindOfClass:[NSDictionary class]]) { result([FlutterError errorWithCode:@"storekit_invalid_argument" - message:@"Argument type of finishTransaction is not a string." + message:@"Argument type of finishTransaction is not a Dictionary" details:call.arguments]); return; } - NSString *transactionIdentifier = call.arguments; + NSDictionary *paymentMap = (NSDictionary *)call.arguments; + NSString *transactionIdentifier = [paymentMap objectForKey:@"transactionIdentifier"]; + NSString *productIdentifier = [paymentMap objectForKey:@"productIdentifier"]; NSArray *pendingTransactions = [self.paymentQueueHandler getUnfinishedTransactions]; for (SKPaymentTransaction *transaction in pendingTransactions) { - if ([transaction.transactionIdentifier isEqualToString:transactionIdentifier]) { + // If the user cancels the purchase dialog we won't have a transactionIdentifier. + // So if it is null AND a transaction in the pendingTransactions list has + // also a null transactionIdentifier we check for equal product identifiers. + if ([transaction.transactionIdentifier isEqualToString:transactionIdentifier] || + ([transactionIdentifier isEqual:[NSNull null]] && + transaction.transactionIdentifier == nil && + [transaction.payment.productIdentifier isEqualToString:productIdentifier])) { @try { [self.paymentQueueHandler finishTransaction:transaction]; } @catch (NSException *e) { @@ -236,6 +249,11 @@ - (void)restoreTransactions:(FlutterMethodCall *)call result:(FlutterResult)resu result(nil); } +- (void)presentCodeRedemptionSheet:(FlutterMethodCall *)call result:(FlutterResult)result { + [self.paymentQueueHandler presentCodeRedemptionSheet]; + result(nil); +} + - (void)retrieveReceiptData:(FlutterMethodCall *)call result:(FlutterResult)result { FlutterError *error = nil; NSString *receiptData = [self.receiptManager retrieveReceiptWithError:&error]; @@ -280,7 +298,7 @@ - (void)refreshReceipt:(FlutterMethodCall *)call result:(FlutterResult)result { }]; } -#pragma mark - delegates +#pragma mark - delegates: - (void)handleTransactionsUpdated:(NSArray *)transactions { NSMutableArray *maps = [NSMutableArray new]; diff --git a/packages/in_app_purchase/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 new file mode 100644 index 000000000000..785235336e43 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase_ios.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_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, '8.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS' => 'armv7 arm64 x86_64' } +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 new file mode 100644 index 000000000000..21e76815e6ac --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/in_app_purchase_ios.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_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/channel.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/channel.dart new file mode 100644 index 000000000000..f8ab4d48be7e --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/channel.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. + +import 'package:flutter/services.dart'; + +/// Method channel for the plugin's platform<-->Dart calls. +const MethodChannel channel = + MethodChannel('plugins.flutter.io/in_app_purchase'); 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 new file mode 100644 index 000000000000..a83c88796343 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart @@ -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. + +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(); + _observer = _TransactionObserver(StreamController.broadcast()); + _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 new file mode 100644 index 000000000000..0c7b2de860b6 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform_addition.dart @@ -0,0 +1,33 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package: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(); + final String? receipt = await SKReceiptManager.retrieveReceiptData(); + if (receipt == null) { + return null; + } + return PurchaseVerificationData( + localVerificationData: receipt, + serverVerificationData: receipt, + source: kIAPSource); + } +} diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/README.md b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/README.md similarity index 100% rename from packages/in_app_purchase/lib/src/store_kit_wrappers/README.md rename to packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/README.md 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 new file mode 100644 index 000000000000..08af2c6058c4 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.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: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 new file mode 100644 index 000000000000..b003f435a800 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.g.dart @@ -0,0 +1,73 @@ +// 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/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 similarity index 77% rename from packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart rename to packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart index 7f20736afbf8..b677772869f6 100644 --- a/packages/in_app_purchase/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 @@ -1,14 +1,16 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:ui' show hashValues; import 'dart:async'; +import 'dart:ui' show hashValues; + import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:in_app_purchase/src/channel.dart'; -import 'package:json_annotation/json_annotation.dart'; import 'package:flutter/services.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; + +import '../channel.dart'; import 'sk_payment_transaction_wrappers.dart'; import 'sk_product_wrapper.dart'; @@ -25,8 +27,6 @@ part 'sk_payment_queue_wrapper.g.dart'; /// available at the [In-App Purchase Programming /// Guide](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Introduction.html#//apple_ref/doc/uid/TP40008267). class SKPaymentQueueWrapper { - SKTransactionObserverWrapper _observer; - /// Returns the default payment queue. /// /// We do not support instantiating a custom payment queue, hence the @@ -35,19 +35,23 @@ class SKPaymentQueueWrapper { return _singleton; } + SKPaymentQueueWrapper._(); + static final SKPaymentQueueWrapper _singleton = SKPaymentQueueWrapper._(); - SKPaymentQueueWrapper._(); + 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]')); + return _getTransactionList((await channel + .invokeListMethod('-[SKPaymentQueue transactions]'))!); } /// Calls [`-[SKPaymentQueue canMakePayments:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506139-canmakepayments?language=objc). static Future canMakePayments() async => - await channel.invokeMethod('-[SKPaymentQueue canMakePayments:]'); + (await channel + .invokeMethod('-[SKPaymentQueue canMakePayments:]')) ?? + false; /// Sets an observer to listen to all incoming transaction events. /// @@ -57,7 +61,7 @@ class SKPaymentQueueWrapper { /// addTransactionObserver:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506042-addtransactionobserver?language=objc). void setTransactionObserver(SKTransactionObserverWrapper observer) { _observer = observer; - callbackChannel.setMethodCallHandler(_handleObserverCallbacks); + channel.setMethodCallHandler(_handleObserverCallbacks); } /// Posts a payment to the queue. @@ -83,7 +87,7 @@ class SKPaymentQueueWrapper { Future addPayment(SKPaymentWrapper payment) async { assert(_observer != null, '[in_app_purchase]: Trying to add a payment without an observer. One must be set using `SkPaymentQueueWrapper.setTransactionObserver` before the app launches.'); - Map requestMap = payment.toMap(); + final Map requestMap = payment.toMap(); await channel.invokeMethod( '-[InAppPurchasePlugin addPayment:result:]', requestMap, @@ -103,9 +107,11 @@ class SKPaymentQueueWrapper { /// 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:]', - transaction.transactionIdentifier); + '-[InAppPurchasePlugin finishTransaction:result:]', + requestMap, + ); } /// Restore previously purchased transactions. @@ -122,28 +128,41 @@ class SKPaymentQueueWrapper { /// /// The `applicationUserName` should match the original /// [SKPaymentWrapper.applicationUsername] used in [addPayment]. + /// If no `applicationUserName` was used, `applicationUserName` should be null. /// /// This method either triggers [`-[SKPayment /// restoreCompletedTransactions]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506123-restorecompletedtransactions?language=objc) /// or [`-[SKPayment restoreCompletedTransactionsWithApplicationUsername:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1505992-restorecompletedtransactionswith?language=objc) /// depending on whether the `applicationUserName` is set. - Future restoreTransactions({String applicationUserName}) async { + Future restoreTransactions({String? applicationUserName}) async { await channel.invokeMethod( '-[InAppPurchasePlugin restoreTransactions:result:]', applicationUserName); } + /// 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:]'); + } + // Triage a method channel call from the platform and triggers the correct observer method. - Future _handleObserverCallbacks(MethodCall call) { + Future _handleObserverCallbacks(MethodCall call) async { assert(_observer != null, '[in_app_purchase]: (Fatal)The observer has not been set but we received a purchase transaction notification. Please ensure the observer has been set using `setTransactionObserver`. Make sure the observer is added right at the App Launch.'); + final SKTransactionObserverWrapper observer = _observer!; switch (call.method) { case 'updatedTransactions': { final List transactions = _getTransactionList(call.arguments); return Future(() { - _observer.updatedTransactions(transactions: transactions); + observer.updatedTransactions(transactions: transactions); }); } case 'removedTransactions': @@ -151,20 +170,20 @@ class SKPaymentQueueWrapper { final List transactions = _getTransactionList(call.arguments); return Future(() { - _observer.removedTransactions(transactions: transactions); + observer.removedTransactions(transactions: transactions); }); } case 'restoreCompletedTransactionsFailed': { SKError error = SKError.fromJson(call.arguments); return Future(() { - _observer.restoreCompletedTransactionsFailed(error: error); + observer.restoreCompletedTransactionsFailed(error: error); }); } case 'paymentQueueRestoreCompletedTransactionsFinished': { return Future(() { - _observer.paymentQueueRestoreCompletedTransactionsFinished(); + observer.paymentQueueRestoreCompletedTransactionsFinished(); }); } case 'shouldAddStorePayment': @@ -174,7 +193,7 @@ class SKPaymentQueueWrapper { SKProductWrapper product = SKProductWrapper.fromJson(call.arguments['product']); return Future(() { - if (_observer.shouldAddStorePayment( + if (observer.shouldAddStorePayment( payment: payment, product: product) == true) { SKPaymentQueueWrapper().addPayment(payment); @@ -184,48 +203,54 @@ class SKPaymentQueueWrapper { default: break; } - return null; + throw PlatformException( + code: 'no_such_callback', + message: 'Did not recognize the observer callback ${call.method}.'); } // Get transaction wrapper object list from arguments. - List _getTransactionList(dynamic arguments) { - final List transactions = arguments - .map( - (dynamic map) => SKPaymentTransactionWrapper.fromJson(map)) - .toList(); - return transactions; + List _getTransactionList( + List transactionsData) { + return transactionsData.map((dynamic map) { + return SKPaymentTransactionWrapper.fromJson( + Map.castFrom(map)); + }).toList(); } } /// Dart wrapper around StoreKit's /// [NSError](https://developer.apple.com/documentation/foundation/nserror?language=objc). -@JsonSerializable(nullable: true) +@immutable +@JsonSerializable() class SKError { - SKError( - {@required this.code, @required this.domain, @required this.userInfo}); + /// 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) { - assert(map != null); + factory SKError.fromJson(Map map) { return _$SKErrorFromJson(map); } /// Error [code](https://developer.apple.com/documentation/foundation/1448136-nserror_codes) /// as defined in the Cocoa Framework. + @JsonKey(defaultValue: 0) final int code; /// Error /// [domain](https://developer.apple.com/documentation/foundation/nscocoaerrordomain?language=objc) /// as defined in the Cocoa Framework. + @JsonKey(defaultValue: '') final String domain; /// A map that contains more detailed information about the error. /// /// Any key of the map must be a valid [NSErrorUserInfoKey](https://developer.apple.com/documentation/foundation/nserroruserinfokey?language=objc). + @JsonKey(defaultValue: {}) final Map userInfo; @override @@ -236,7 +261,7 @@ class SKError { if (other.runtimeType != runtimeType) { return false; } - final SKError typedOther = other; + final SKError typedOther = other as SKError; return typedOther.code == code && typedOther.domain == domain && DeepCollectionEquality.unordered() @@ -244,7 +269,11 @@ class SKError { } @override - int get hashCode => hashValues(this.code, this.domain, this.userInfo); + int get hashCode => hashValues( + code, + domain, + userInfo, + ); } /// Dart wrapper around StoreKit's @@ -254,10 +283,12 @@ class SKError { /// not need to create the payment object explicitly; instead, use /// [SKPaymentQueueWrapper.addPayment] directly with a product identifier to /// initiate a payment. -@JsonSerializable(nullable: true) +@immutable +@JsonSerializable() class SKPaymentWrapper { - SKPaymentWrapper( - {@required this.productIdentifier, + /// Creates a new [SKPaymentWrapper] with the provided information. + const SKPaymentWrapper( + {required this.productIdentifier, this.applicationUsername, this.requestData, this.quantity = 1, @@ -268,14 +299,14 @@ class SKPaymentWrapper { /// The map needs to have named string keys with values matching the names and /// types of all of the members on this class. The `map` parameter must not be /// null. - factory SKPaymentWrapper.fromJson(Map map) { + factory SKPaymentWrapper.fromJson(Map map) { assert(map != null); return _$SKPaymentWrapperFromJson(map); } /// Creates a Map object describes the payment object. Map toMap() { - return { + return { 'productIdentifier': productIdentifier, 'applicationUsername': applicationUsername, 'requestData': requestData, @@ -285,6 +316,7 @@ class SKPaymentWrapper { } /// The id for the product that the payment is for. + @JsonKey(defaultValue: '') final String productIdentifier; /// An opaque id for the user's account. @@ -295,7 +327,7 @@ class SKPaymentWrapper { /// account name on your server. Don’t use the Apple ID for your developer /// account, the user’s Apple ID, or the user’s plaintext account name on /// your server. - final String applicationUsername; + final String? applicationUsername; /// Reserved for future use. /// @@ -306,18 +338,26 @@ class SKPaymentWrapper { // We also provide this property to match the iOS platform. Converted to // String from NSData from ios platform using UTF8Encoding. The / default is // null. - final String requestData; + final String? requestData; /// The amount of the product this payment is for. /// /// The default is 1. The minimum is 1. The maximum is 10. + /// + /// If the object is invalid, the value could be 0. + @JsonKey(defaultValue: 0) final int quantity; - /// Produces an "ask to buy" flow in the sandbox if set to true. Default is - /// false. + /// Produces an "ask to buy" flow in the sandbox. + /// + /// Setting it to `true` will cause a transaction to be in the state [SKPaymentTransactionStateWrapper.deferred], + /// which produce an "ask to buy" prompt that interrupts the the payment flow. + /// + /// Default is `false`. /// /// See https://developer.apple.com/in-app-purchase/ for a guide on Sandbox /// testing. + @JsonKey(defaultValue: false) final bool simulatesAskToBuyInSandbox; @override @@ -328,7 +368,7 @@ class SKPaymentWrapper { if (other.runtimeType != runtimeType) { return false; } - final SKPaymentWrapper typedOther = other; + final SKPaymentWrapper typedOther = other as SKPaymentWrapper; return typedOther.productIdentifier == productIdentifier && typedOther.applicationUsername == applicationUsername && typedOther.quantity == quantity && @@ -337,12 +377,8 @@ class SKPaymentWrapper { } @override - int get hashCode => hashValues( - this.productIdentifier, - this.applicationUsername, - this.quantity, - this.simulatesAskToBuyInSandbox, - this.requestData); + 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 new file mode 100644 index 000000000000..2b886597adc5 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart @@ -0,0 +1,44 @@ +// 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/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart similarity index 78% rename from packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart rename to packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart index f90684f374f5..01cd6db0dda1 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart @@ -1,9 +1,8 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:ui' show hashValues; -import 'package:flutter/foundation.dart'; import 'package:json_annotation/json_annotation.dart'; import 'sk_product_wrapper.dart'; import 'sk_payment_queue_wrapper.dart'; @@ -20,13 +19,15 @@ part 'sk_payment_transaction_wrappers.g.dart'; /// This class is a Dart wrapper around [SKTransactionObserver](https://developer.apple.com/documentation/storekit/skpaymenttransactionobserver?language=objc). abstract class SKTransactionObserverWrapper { /// Triggered when any transactions are updated. - void updatedTransactions({List transactions}); + void updatedTransactions( + {required List transactions}); /// Triggered when any transactions are removed from the payment queue. - void removedTransactions({List transactions}); + void removedTransactions( + {required List transactions}); /// Triggered when there is an error while restoring transactions. - void restoreCompletedTransactionsFailed({SKError error}); + void restoreCompletedTransactionsFailed({required SKError error}); /// Triggered when payment queue has finished sending restored transactions. void paymentQueueRestoreCompletedTransactionsFinished(); @@ -41,7 +42,7 @@ abstract class SKTransactionObserverWrapper { /// continue the transaction later by calling [addPayment] with the /// `payment` param from this method. bool shouldAddStorePayment( - {SKPaymentWrapper payment, SKProductWrapper product}); + {required SKPaymentWrapper payment, required SKProductWrapper product}); } /// The state of a transaction. @@ -85,6 +86,10 @@ enum SKPaymentTransactionStateWrapper { /// transaction to update to another state. @JsonValue(4) deferred, + + /// Indicates the transaction is in an unspecified state. + @JsonValue(-1) + unspecified, } /// Created when a payment is added to the [SKPaymentQueueWrapper]. @@ -96,15 +101,16 @@ enum SKPaymentTransactionStateWrapper { /// /// Dart wrapper around StoreKit's /// [SKPaymentTransaction](https://developer.apple.com/documentation/storekit/skpaymenttransaction?language=objc). -@JsonSerializable(nullable: true) +@JsonSerializable() class SKPaymentTransactionWrapper { + /// Creates a new [SKPaymentTransactionWrapper] with the provided information. SKPaymentTransactionWrapper({ - @required this.payment, - @required this.transactionState, - @required this.originalTransaction, - @required this.transactionTimeStamp, - @required this.transactionIdentifier, - @required this.error, + required this.payment, + required this.transactionState, + this.originalTransaction, + this.transactionTimeStamp, + this.transactionIdentifier, + this.error, }); /// Constructs an instance of this from a key value map of data. @@ -112,10 +118,7 @@ class SKPaymentTransactionWrapper { /// The map needs to have named string keys with values matching the names and /// types of all of the members on this class. The `map` parameter must not be /// null. - factory SKPaymentTransactionWrapper.fromJson(Map map) { - if (map == null) { - return null; - } + factory SKPaymentTransactionWrapper.fromJson(Map map) { return _$SKPaymentTransactionWrapperFromJson(map); } @@ -129,18 +132,21 @@ class SKPaymentTransactionWrapper { /// The original Transaction. /// - /// Only available if the [transactionState] is - /// [SKPaymentTransactionStateWrapper.restored]. When the [transactionState] + /// Only available if the [transactionState] is [SKPaymentTransactionStateWrapper.restored]. + /// Otherwise the value is `null`. + /// + /// When the [transactionState] /// is [SKPaymentTransactionStateWrapper.restored], the current transaction /// object holds a new [transactionIdentifier]. - final SKPaymentTransactionWrapper originalTransaction; + final SKPaymentTransactionWrapper? originalTransaction; /// The timestamp of the transaction. /// /// Seconds since epoch. It is only defined when the [transactionState] is /// [SKPaymentTransactionStateWrapper.purchased] or /// [SKPaymentTransactionStateWrapper.restored]. - final double transactionTimeStamp; + /// Otherwise, the value is `null`. + final double? transactionTimeStamp; /// The unique string identifer of the transaction. /// @@ -149,13 +155,15 @@ class SKPaymentTransactionWrapper { /// [SKPaymentTransactionStateWrapper.restored]. You may wish to record this /// string as part of an audit trail for App Store purchases. The value of /// this string corresponds to the same property in the receipt. - final String transactionIdentifier; + /// + /// The value is `null` if it is an unsuccessful transaction. + final String? transactionIdentifier; /// The error object /// /// Only available if the [transactionState] is /// [SKPaymentTransactionStateWrapper.failed]. - final SKError error; + final SKError? error; @override bool operator ==(Object other) { @@ -165,7 +173,8 @@ class SKPaymentTransactionWrapper { if (other.runtimeType != runtimeType) { return false; } - final SKPaymentTransactionWrapper typedOther = other; + final SKPaymentTransactionWrapper typedOther = + other as SKPaymentTransactionWrapper; return typedOther.payment == payment && typedOther.transactionState == transactionState && typedOther.originalTransaction == originalTransaction && @@ -185,4 +194,10 @@ class SKPaymentTransactionWrapper { @override String toString() => _$SKPaymentTransactionWrapperToJson(this).toString(); + + /// The payload that is used to finish this transaction. + Map toFinishMap() => { + "transactionIdentifier": this.transactionIdentifier, + "productIdentifier": this.payment.productIdentifier, + }; } 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 new file mode 100644 index 000000000000..4c7af21bc151 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart @@ -0,0 +1,37 @@ +// 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/lib/src/store_kit_wrappers/sk_product_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.dart similarity index 77% rename from packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart rename to packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.dart index 8f4c815a8f50..ef0e6671d177 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.dart @@ -1,11 +1,11 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:ui' show hashValues; -import 'package:flutter/foundation.dart'; import 'package:collection/collection.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'enum_converters.dart'; // WARNING: Changes to `@JsonSerializable` classes need to be reflected in the // below generated file. Run `flutter packages pub run build_runner watch` to @@ -18,15 +18,14 @@ part 'sk_product_wrapper.g.dart'; /// Contains information about a list of products and a list of invalid product identifiers. @JsonSerializable() class SkProductResponseWrapper { + /// Creates an [SkProductResponseWrapper] with the given product details. SkProductResponseWrapper( - {@required this.products, @required this.invalidProductIdentifiers}); + {required this.products, required this.invalidProductIdentifiers}); /// Constructing an instance from a map from the Objective-C layer. /// /// This method should only be used with `map` values returned by [SKRequestMaker.startProductRequest]. - /// The `map` parameter must not be null. factory SkProductResponseWrapper.fromJson(Map map) { - assert(map != null, 'Map must not be null.'); return _$SkProductResponseWrapperFromJson(map); } @@ -34,6 +33,7 @@ class SkProductResponseWrapper { /// /// One product in this list matches one valid product identifier passed to the [SKRequestMaker.startProductRequest]. /// Will be empty if the [SKRequestMaker.startProductRequest] method does not pass any correct product identifier. + @JsonKey(defaultValue: []) final List products; /// Stores product identifiers in the `productIdentifiers` from [SKRequestMaker.startProductRequest] that are not recognized by the App Store. @@ -41,6 +41,7 @@ class SkProductResponseWrapper { /// The App Store will not recognize a product identifier unless certain criteria are met. A detailed list of the criteria can be /// found here https://developer.apple.com/documentation/storekit/skproductsresponse/1505985-invalidproductidentifiers?language=objc. /// Will be empty if all the product identifiers are valid. + @JsonKey(defaultValue: []) final List invalidProductIdentifiers; @override @@ -51,7 +52,8 @@ class SkProductResponseWrapper { if (other.runtimeType != runtimeType) { return false; } - final SkProductResponseWrapper typedOther = other; + final SkProductResponseWrapper typedOther = + other as SkProductResponseWrapper; return DeepCollectionEquality().equals(typedOther.products, products) && DeepCollectionEquality().equals( typedOther.invalidProductIdentifiers, invalidProductIdentifiers); @@ -67,12 +69,21 @@ class SkProductResponseWrapper { // The values of the enum options are matching the [SKProductPeriodUnit]'s values. Should there be an update or addition // in the [SKProductPeriodUnit], this need to be updated to match. enum SKSubscriptionPeriodUnit { + /// An interval lasting one day. @JsonValue(0) day, + + /// An interval lasting one month. @JsonValue(1) + + /// An interval lasting one week. week, @JsonValue(2) + + /// An interval lasting one month. month, + + /// An interval lasting one year. @JsonValue(3) year, } @@ -81,26 +92,32 @@ enum SKSubscriptionPeriodUnit { /// /// A period is defined by a [numberOfUnits] and a [unit], e.g for a 3 months period [numberOfUnits] is 3 and [unit] is a month. /// It is used as a property in [SKProductDiscountWrapper] and [SKProductWrapper]. -@JsonSerializable(nullable: true) +@JsonSerializable() class SKProductSubscriptionPeriodWrapper { + /// Creates an [SKProductSubscriptionPeriodWrapper] for a `numberOfUnits`x`unit` period. SKProductSubscriptionPeriodWrapper( - {@required this.numberOfUnits, @required this.unit}); + {required this.numberOfUnits, required this.unit}); /// Constructing an instance from a map from the Objective-C layer. /// /// This method should only be used with `map` values returned by [SKProductDiscountWrapper.fromJson] or [SKProductWrapper.fromJson]. - /// The `map` parameter must not be null. - factory SKProductSubscriptionPeriodWrapper.fromJson(Map map) { - assert(map != null, 'Map must not be null.'); + factory SKProductSubscriptionPeriodWrapper.fromJson( + Map? map) { + if (map == null) { + return SKProductSubscriptionPeriodWrapper( + numberOfUnits: 0, unit: SKSubscriptionPeriodUnit.day); + } return _$SKProductSubscriptionPeriodWrapperFromJson(map); } /// The number of [unit] units in this period. /// - /// Must be greater than 0. + /// Must be greater than 0 if the object is valid. + @JsonKey(defaultValue: 0) final int numberOfUnits; /// The time unit used to specify the length of this period. + @SKSubscriptionPeriodUnitConverter() final SKSubscriptionPeriodUnit unit; @override @@ -111,7 +128,8 @@ class SKProductSubscriptionPeriodWrapper { if (other.runtimeType != runtimeType) { return false; } - final SKProductSubscriptionPeriodWrapper typedOther = other; + final SKProductSubscriptionPeriodWrapper typedOther = + other as SKProductSubscriptionPeriodWrapper; return typedOther.numberOfUnits == numberOfUnits && typedOther.unit == unit; } @@ -136,30 +154,34 @@ enum SKProductDiscountPaymentMode { /// User pays nothing during the discounted period. @JsonValue(2) freeTrail, + + /// Unspecified mode. + @JsonValue(-1) + unspecified, } /// Dart wrapper around StoreKit's [SKProductDiscount](https://developer.apple.com/documentation/storekit/skproductdiscount?language=objc). /// /// It is used as a property in [SKProductWrapper]. -@JsonSerializable(nullable: true) +@JsonSerializable() class SKProductDiscountWrapper { + /// Creates an [SKProductDiscountWrapper] with the given discount details. SKProductDiscountWrapper( - {@required this.price, - @required this.priceLocale, - @required this.numberOfPeriods, - @required this.paymentMode, - @required this.subscriptionPeriod}); + {required this.price, + required this.priceLocale, + required this.numberOfPeriods, + required this.paymentMode, + required this.subscriptionPeriod}); /// Constructing an instance from a map from the Objective-C layer. /// /// This method should only be used with `map` values returned by [SKProductWrapper.fromJson]. - /// The `map` parameter must not be null. - factory SKProductDiscountWrapper.fromJson(Map map) { - assert(map != null, 'Map must not be null.'); + factory SKProductDiscountWrapper.fromJson(Map map) { return _$SKProductDiscountWrapperFromJson(map); } /// The discounted price, in the currency that is defined in [priceLocale]. + @JsonKey(defaultValue: '') final String price; /// Includes locale information about the price, e.g. `$` as the currency symbol for US locale. @@ -167,10 +189,12 @@ class SKProductDiscountWrapper { /// The object represent the discount period length. /// - /// The value must be >= 0. + /// The value must be >= 0 if the object is valid. + @JsonKey(defaultValue: 0) final int numberOfPeriods; /// The object indicates how the discount price is charged. + @SKProductDiscountPaymentModeConverter() final SKProductDiscountPaymentMode paymentMode; /// The object represents the duration of single subscription period for the discount. @@ -187,7 +211,8 @@ class SKProductDiscountWrapper { if (other.runtimeType != runtimeType) { return false; } - final SKProductDiscountWrapper typedOther = other; + final SKProductDiscountWrapper typedOther = + other as SKProductDiscountWrapper; return typedOther.price == price && typedOther.priceLocale == priceLocale && typedOther.numberOfPeriods == numberOfPeriods && @@ -204,39 +229,41 @@ class SKProductDiscountWrapper { /// /// A list of [SKProductWrapper] is returned in the [SKRequestMaker.startProductRequest] method, and /// should be stored for use when making a payment. -@JsonSerializable(nullable: true) +@JsonSerializable() class SKProductWrapper { + /// Creates an [SKProductWrapper] with the given product details. SKProductWrapper({ - @required this.productIdentifier, - @required this.localizedTitle, - @required this.localizedDescription, - @required this.priceLocale, - @required this.subscriptionGroupIdentifier, - @required this.price, - @required this.subscriptionPeriod, - @required this.introductoryPrice, + required this.productIdentifier, + required this.localizedTitle, + required this.localizedDescription, + required this.priceLocale, + this.subscriptionGroupIdentifier, + required this.price, + this.subscriptionPeriod, + this.introductoryPrice, }); /// Constructing an instance from a map from the Objective-C layer. /// /// This method should only be used with `map` values returned by [SkProductResponseWrapper.fromJson]. - /// The `map` parameter must not be null. - factory SKProductWrapper.fromJson(Map map) { - assert(map != null, 'Map must not be null.'); + factory SKProductWrapper.fromJson(Map map) { return _$SKProductWrapperFromJson(map); } /// The unique identifier of the product. + @JsonKey(defaultValue: '') final String productIdentifier; /// The localizedTitle of the product. /// /// It is localized based on the current locale. + @JsonKey(defaultValue: '') final String localizedTitle; /// The localized description of the product. /// /// It is localized based on the current locale. + @JsonKey(defaultValue: '') final String localizedDescription; /// Includes locale information about the price, e.g. `$` as the currency symbol for US locale. @@ -244,26 +271,29 @@ class SKProductWrapper { /// The subscription group identifier. /// + /// If the product is not a subscription, the value is `null`. + /// /// A subscription group is a collection of subscription products. /// Check [SubscriptionGroup](https://developer.apple.com/app-store/subscriptions/) for more details about subscription group. - final String subscriptionGroupIdentifier; + final String? subscriptionGroupIdentifier; /// The price of the product, in the currency that is defined in [priceLocale]. + @JsonKey(defaultValue: '') final String price; /// The object represents the subscription period of the product. /// /// Can be [null] is the product is not a subscription. - final SKProductSubscriptionPeriodWrapper subscriptionPeriod; + final SKProductSubscriptionPeriodWrapper? subscriptionPeriod; /// The object represents the duration of single subscription period. /// - /// This is only available if you set up the introductory price in the App Store Connect, otherwise it will be null. + /// This is only available if you set up the introductory price in the App Store Connect, otherwise the value is `null`. /// Programmer is also responsible to determine if the user is eligible to receive it. See https://developer.apple.com/documentation/storekit/in-app_purchase/offering_introductory_pricing_in_your_app?language=objc /// for more details. /// The [subscriptionPeriod] of the discount is independent of the product's [subscriptionPeriod], /// and their units and duration do not have to be matched. - final SKProductDiscountWrapper introductoryPrice; + final SKProductDiscountWrapper? introductoryPrice; @override bool operator ==(Object other) { @@ -273,7 +303,7 @@ class SKProductWrapper { if (other.runtimeType != runtimeType) { return false; } - final SKProductWrapper typedOther = other; + final SKProductWrapper typedOther = other as SKProductWrapper; return typedOther.productIdentifier == productIdentifier && typedOther.localizedTitle == localizedTitle && typedOther.localizedDescription == localizedDescription && @@ -304,22 +334,26 @@ class SKProductWrapper { // https://github.com/flutter/flutter/issues/26610 @JsonSerializable() class SKPriceLocaleWrapper { + /// Creates a new price locale for `currencySymbol` and `currencyCode`. SKPriceLocaleWrapper( - {@required this.currencySymbol, @required this.currencyCode}); + {required this.currencySymbol, required this.currencyCode}); /// Constructing an instance from a map from the Objective-C layer. /// /// This method should only be used with `map` values returned by [SKProductWrapper.fromJson] and [SKProductDiscountWrapper.fromJson]. - /// The `map` parameter must not be null. - factory SKPriceLocaleWrapper.fromJson(Map map) { - assert(map != null, 'Map must not be null.'); + factory SKPriceLocaleWrapper.fromJson(Map? map) { + if (map == null) { + return SKPriceLocaleWrapper(currencyCode: '', currencySymbol: ''); + } return _$SKPriceLocaleWrapperFromJson(map); } ///The currency symbol for the locale, e.g. $ for US locale. + @JsonKey(defaultValue: '') final String currencySymbol; ///The currency code for the locale, e.g. USD for US locale. + @JsonKey(defaultValue: '') final String currencyCode; @override @@ -330,7 +364,7 @@ class SKPriceLocaleWrapper { if (other.runtimeType != runtimeType) { return false; } - final SKPriceLocaleWrapper typedOther = other; + final SKPriceLocaleWrapper typedOther = other as SKPriceLocaleWrapper; return typedOther.currencySymbol == currencySymbol && typedOther.currencyCode == currencyCode; } diff --git a/packages/in_app_purchase/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 new file mode 100644 index 000000000000..8c2eed3d6070 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart @@ -0,0 +1,123 @@ +// 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? ?? '', + ); +} + +Map _$SKPriceLocaleWrapperToJson( + SKPriceLocaleWrapper instance) => + { + 'currencySymbol': instance.currencySymbol, + 'currencyCode': instance.currencyCode, + }; diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_manager.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_receipt_manager.dart similarity index 78% rename from packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_manager.dart rename to packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_receipt_manager.dart index 85af9dedc7c3..3eb41cb66a14 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_receipt_manager.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_receipt_manager.dart @@ -1,9 +1,10 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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:in_app_purchase/src/channel.dart'; + +import '../channel.dart'; ///This class contains static methods to manage StoreKit receipts. class SKReceiptManager { @@ -14,8 +15,9 @@ class SKReceiptManager { /// There are 2 ways to do so. Either validate locally or validate with App Store. /// For more details on how to validate the receipt data, you can refer to Apple's document about [`About Receipt Validation`](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573-CH105-SW1). /// If the receipt is invalid or missing, you can use [SKRequestMaker.startRefreshReceiptRequest] to request a new receipt. - static Future retrieveReceiptData() { - return channel.invokeMethod( - '-[InAppPurchasePlugin retrieveReceiptData:result:]'); + static Future retrieveReceiptData() async { + return (await channel.invokeMethod( + '-[InAppPurchasePlugin retrieveReceiptData:result:]')) ?? + ''; } } diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_request_maker.dart similarity index 93% rename from packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart rename to packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_request_maker.dart index 959113cd66d8..d59f66fce2c9 100644 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_request_maker.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_request_maker.dart @@ -1,10 +1,12 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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:in_app_purchase/src/channel.dart'; + +import '../channel.dart'; import 'sk_product_wrapper.dart'; /// A request maker that handles all the requests made by SKRequest subclasses. @@ -24,7 +26,7 @@ class SKRequestMaker { /// A [PlatformException] is thrown if the platform code making the request fails. Future startProductRequest( List productIdentifiers) async { - final Map productResponseMap = + final Map? productResponseMap = await channel.invokeMapMethod( '-[InAppPurchasePlugin startProductRequest:result:]', productIdentifiers, @@ -47,7 +49,8 @@ class SKRequestMaker { /// * isExpired: whether the receipt is expired. /// * isRevoked: whether the receipt has been revoked. /// * isVolumePurchase: whether the receipt is a Volume Purchase Plan receipt. - Future startRefreshReceiptRequest({Map receiptProperties}) { + Future startRefreshReceiptRequest( + {Map? receiptProperties}) { return channel.invokeMethod( '-[InAppPurchasePlugin refreshReceipt:result:]', receiptProperties, diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_product_details.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_product_details.dart new file mode 100644 index 000000000000..96386c5ef5ad --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_product_details.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:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; + +import '../../store_kit_wrappers.dart'; + +/// The class represents the information of a product as registered in the Apple +/// AppStore. +class AppStoreProductDetails extends ProductDetails { + /// Creates a new AppStore specific product details object with the provided + /// details. + AppStoreProductDetails({ + required String id, + required String title, + required String description, + required String price, + required double rawPrice, + required String currencyCode, + required this.skProduct, + }) : super( + id: id, + title: title, + description: description, + price: price, + rawPrice: rawPrice, + currencyCode: currencyCode, + ); + + /// 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( + id: product.productIdentifier, + title: product.localizedTitle, + description: product.localizedDescription, + price: product.priceLocale.currencySymbol + product.price, + rawPrice: double.parse(product.price), + currencyCode: product.priceLocale.currencyCode, + skProduct: product, + ); + } +} 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_ios/lib/src/types/app_store_purchase_details.dart new file mode 100644 index 000000000000..6d6f241d6ca8 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_details.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:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; + +import '../../in_app_purchase_ios.dart'; +import '../../store_kit_wrappers.dart'; +import '../store_kit_wrappers/enum_converters.dart'; + +/// The class represents the information of a purchase made with the Apple +/// AppStore. +class AppStorePurchaseDetails extends PurchaseDetails { + /// Creates a new AppStore specific purchase details object with the provided + /// details. + AppStorePurchaseDetails( + {String? purchaseID, + required String productID, + required PurchaseVerificationData verificationData, + required String? transactionDate, + required this.skPaymentTransaction, + required PurchaseStatus status}) + : super( + productID: productID, + purchaseID: purchaseID, + transactionDate: transactionDate, + verificationData: verificationData, + status: status) { + 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( + SKPaymentTransactionWrapper transaction, + String base64EncodedReceipt, + ) { + final AppStorePurchaseDetails purchaseDetails = AppStorePurchaseDetails( + productID: transaction.payment.productIdentifier, + purchaseID: transaction.transactionIdentifier, + skPaymentTransaction: transaction, + status: SKTransactionStatusConverter() + .toPurchaseStatus(transaction.transactionState), + transactionDate: transaction.transactionTimeStamp != null + ? (transaction.transactionTimeStamp! * 1000).toInt().toString() + : null, + verificationData: PurchaseVerificationData( + localVerificationData: base64EncodedReceipt, + serverVerificationData: base64EncodedReceipt, + source: kIAPSource), + ); + + if (purchaseDetails.status == PurchaseStatus.error) { + purchaseDetails.error = IAPError( + source: kIAPSource, + code: kPurchaseErrorCode, + message: transaction.error?.domain ?? '', + details: transaction.error?.userInfo, + ); + } + + return purchaseDetails; + } +} 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_ios/lib/src/types/app_store_purchase_param.dart new file mode 100644 index 000000000000..b2d8eea9d791 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_purchase_param.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. + +import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; + +import '../../store_kit_wrappers.dart'; + +/// Apple AppStore specific parameter object for generating a purchase. +class AppStorePurchaseParam extends PurchaseParam { + /// Creates a new [AppStorePurchaseParam] object with the given data. + AppStorePurchaseParam({ + required ProductDetails productDetails, + String? applicationUserName, + this.simulatesAskToBuyInSandbox = false, + }) : super( + productDetails: productDetails, + applicationUserName: applicationUserName, + ); + + /// Set it to `true` to produce an "ask to buy" flow for this payment in the + /// sandbox. + /// + /// If you want to test [simulatesAskToBuyInSandbox], you should ensure that + /// you create an instance of the [AppStorePurchaseParam] class and set its + /// [simulateAskToBuyInSandbox] field to `true` and use it with the + /// `buyNonConsumable` or `buyConsumable` methods. + /// + /// See also [SKPaymentWrapper.simulatesAskToBuyInSandbox]. + final bool simulatesAskToBuyInSandbox; +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/types.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/types.dart new file mode 100644 index 000000000000..a21bd4b5fbb1 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/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 'app_store_product_details.dart'; +export 'app_store_purchase_details.dart'; +export 'app_store_purchase_param.dart'; diff --git a/packages/in_app_purchase/lib/store_kit_wrappers.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/store_kit_wrappers.dart similarity index 86% rename from packages/in_app_purchase/lib/store_kit_wrappers.dart rename to packages/in_app_purchase/in_app_purchase_ios/lib/store_kit_wrappers.dart index 12af4ba0a18f..b687d238083c 100644 --- a/packages/in_app_purchase/lib/store_kit_wrappers.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/store_kit_wrappers.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/in_app_purchase/in_app_purchase_ios/pubspec.yaml b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml new file mode 100644 index 000000000000..7a3885a8a3b1 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml @@ -0,0 +1,30 @@ +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.0+1 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +flutter: + plugin: + platforms: + ios: + pluginClass: InAppPurchasePlugin + +dependencies: + collection: ^1.15.0 + flutter: + sdk: flutter + in_app_purchase_platform_interface: ^1.0.0 + json_annotation: ^4.0.1 + meta: ^1.3.0 + test: ^1.16.0 + +dev_dependencies: + build_runner: ^1.11.1 + flutter_test: + sdk: flutter + json_serializable: ^4.1.1 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 new file mode 100644 index 000000000000..f39241318670 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart @@ -0,0 +1,182 @@ +// Copyright 2013 The Flutter Authors. All 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; + + void reset() { + transactions = []; + receiptData = 'dummy base64data'; + validProductIDs = ['123', '456'].toSet(); + validProducts = Map(); + for (String validID in validProductIDs) { + Map productWrapperMap = + buildProductMap(dummyProductWrapper); + productWrapperMap['productIdentifier'] = validID; + 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; + } + + 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); + assert(productIDS is List, 'invalid argument type'); + 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; + } + 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 new file mode 100644 index 000000000000..f8b75195fc6e --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_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_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 new file mode 100644 index 000000000000..b15249c81947 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_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 '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); + }); + + 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); + }); + }); +} 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 new file mode 100644 index 000000000000..e71279edca4f --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart @@ -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 '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; + }); + + 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.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}); + }); + }); + + 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'); + }); + }); + + 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; + + // refresh receipt request + int refreshReceipt = 0; + late Map refreshReceiptParam; + + // payment queue + List payments = []; + List> transactionsFinished = []; + String applicationNameHasTransactionRestored = ''; + + // present Code Redemption + bool presentCodeRedemption = false; + + Future onMethodCall(MethodCall call) { + switch (call.method) { + // request makers + case '-[InAppPurchasePlugin startProductRequest:result:]': + List productIDS = + List.castFrom(call.arguments); + assert(productIDS is List, 'invalid argument type'); + 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:]': + 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(() {}); + } + return Future.sync(() {}); + } +} + +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_ios/test/store_kit_wrappers/sk_product_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_product_test.dart new file mode 100644 index 000000000000..9454a9d4ebee --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_product_test.dart @@ -0,0 +1,187 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +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:test/test.dart'; + +import 'sk_test_stub_objects.dart'; + +void main() { + group('product related object wrapper test', () { + test( + 'SKProductSubscriptionPeriodWrapper should have property values consistent with map', + () { + final SKProductSubscriptionPeriodWrapper wrapper = + SKProductSubscriptionPeriodWrapper.fromJson( + buildSubscriptionPeriodMap(dummySubscription)!); + expect(wrapper, equals(dummySubscription)); + }); + + test( + 'SKProductSubscriptionPeriodWrapper should have properties to be default values if map is empty', + () { + final SKProductSubscriptionPeriodWrapper wrapper = + SKProductSubscriptionPeriodWrapper.fromJson({}); + expect(wrapper.numberOfUnits, 0); + expect(wrapper.unit, SKSubscriptionPeriodUnit.day); + }); + + test( + 'SKProductDiscountWrapper should have property values consistent with map', + () { + final SKProductDiscountWrapper wrapper = + SKProductDiscountWrapper.fromJson(buildDiscountMap(dummyDiscount)); + expect(wrapper, equals(dummyDiscount)); + }); + + test( + 'SKProductDiscountWrapper should have properties to be default if map is empty', + () { + final SKProductDiscountWrapper wrapper = + SKProductDiscountWrapper.fromJson({}); + expect(wrapper.price, ''); + expect(wrapper.priceLocale, + SKPriceLocaleWrapper(currencyCode: '', currencySymbol: '')); + expect(wrapper.numberOfPeriods, 0); + expect(wrapper.paymentMode, SKProductDiscountPaymentMode.payAsYouGo); + expect( + wrapper.subscriptionPeriod, + SKProductSubscriptionPeriodWrapper( + numberOfUnits: 0, unit: SKSubscriptionPeriodUnit.day)); + }); + + test('SKProductWrapper should have property values consistent with map', + () { + final SKProductWrapper wrapper = + SKProductWrapper.fromJson(buildProductMap(dummyProductWrapper)); + expect(wrapper, equals(dummyProductWrapper)); + }); + + test( + 'SKProductWrapper should have properties to be default if map is empty', + () { + final SKProductWrapper wrapper = + SKProductWrapper.fromJson({}); + expect(wrapper.productIdentifier, ''); + expect(wrapper.localizedTitle, ''); + expect(wrapper.localizedDescription, ''); + expect(wrapper.priceLocale, + SKPriceLocaleWrapper(currencyCode: '', currencySymbol: '')); + expect(wrapper.subscriptionGroupIdentifier, null); + expect(wrapper.price, ''); + expect(wrapper.subscriptionPeriod, null); + }); + + test('toProductDetails() should return correct Product object', () { + final SKProductWrapper wrapper = + SKProductWrapper.fromJson(buildProductMap(dummyProductWrapper)); + final AppStoreProductDetails product = + AppStoreProductDetails.fromSKProduct(wrapper); + 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.skProduct, wrapper); + }); + + test('SKProductResponse wrapper should match', () { + final SkProductResponseWrapper wrapper = + SkProductResponseWrapper.fromJson( + buildProductResponseMap(dummyProductResponseWrapper)); + expect(wrapper, equals(dummyProductResponseWrapper)); + }); + test('SKProductResponse wrapper should default to empty list', () { + final Map> productResponseMapEmptyList = + >{ + 'products': >[], + 'invalidProductIdentifiers': [], + }; + final SkProductResponseWrapper wrapper = + SkProductResponseWrapper.fromJson(productResponseMapEmptyList); + expect(wrapper.products.length, 0); + expect(wrapper.invalidProductIdentifiers.length, 0); + }); + + test('LocaleWrapper should have property values consistent with map', () { + final SKPriceLocaleWrapper wrapper = + SKPriceLocaleWrapper.fromJson(buildLocaleMap(dummyLocale)); + expect(wrapper, equals(dummyLocale)); + }); + }); + + group('Payment queue related object tests', () { + test('Should construct correct SKPaymentWrapper from json', () { + SKPaymentWrapper payment = + SKPaymentWrapper.fromJson(dummyPayment.toMap()); + expect(payment, equals(dummyPayment)); + }); + + test('Should construct correct SKError from json', () { + SKError error = SKError.fromJson(buildErrorMap(dummyError)); + expect(error, equals(dummyError)); + }); + + test('Should construct correct SKTransactionWrapper from json', () { + SKPaymentTransactionWrapper transaction = + SKPaymentTransactionWrapper.fromJson( + buildTransactionMap(dummyTransaction)); + expect(transaction, equals(dummyTransaction)); + }); + + test('toPurchaseDetails() should return correct PurchaseDetail object', () { + AppStorePurchaseDetails details = + AppStorePurchaseDetails.fromSKTransaction( + dummyTransaction, 'receipt data'); + expect(dummyTransaction.transactionIdentifier, details.purchaseID); + expect(dummyTransaction.payment.productIdentifier, details.productID); + expect(dummyTransaction.transactionTimeStamp, isNotNull); + expect((dummyTransaction.transactionTimeStamp! * 1000).toInt().toString(), + details.transactionDate); + expect(details.verificationData.localVerificationData, 'receipt data'); + expect(details.verificationData.serverVerificationData, 'receipt data'); + expect(details.verificationData.source, 'app_store'); + expect(details.skPaymentTransaction, dummyTransaction); + expect(details.pendingCompletePurchase, true); + }); + + test('SKPaymentTransactionWrapper.toFinishMap set correct value', () { + final SKPaymentTransactionWrapper transactionWrapper = + SKPaymentTransactionWrapper( + payment: dummyPayment, + transactionState: SKPaymentTransactionStateWrapper.failed, + transactionIdentifier: 'abcd'); + final Map finishMap = transactionWrapper.toFinishMap(); + expect(finishMap['transactionIdentifier'], 'abcd'); + expect(finishMap['productIdentifier'], dummyPayment.productIdentifier); + }); + + test( + 'SKPaymentTransactionWrapper.toFinishMap should set transactionIdentifier to null when necessary', + () { + final SKPaymentTransactionWrapper transactionWrapper = + SKPaymentTransactionWrapper( + payment: dummyPayment, + transactionState: SKPaymentTransactionStateWrapper.failed); + final Map finishMap = transactionWrapper.toFinishMap(); + expect(finishMap['transactionIdentifier'], null); + }); + + test('Should generate correct map of the payment object', () { + Map map = dummyPayment.toMap(); + expect(map['productIdentifier'], dummyPayment.productIdentifier); + expect(map['applicationUsername'], dummyPayment.applicationUsername); + + expect(map['requestData'], dummyPayment.requestData); + + expect(map['quantity'], dummyPayment.quantity); + + expect(map['simulatesAskToBuyInSandbox'], + dummyPayment.simulatesAskToBuyInSandbox); + }); + }); +} diff --git a/packages/in_app_purchase/test/store_kit_wrappers/sk_test_stub_objects.dart b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_test_stub_objects.dart similarity index 89% rename from packages/in_app_purchase/test/store_kit_wrappers/sk_test_stub_objects.dart rename to packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_test_stub_objects.dart index c976e80a90a5..d6c24460761e 100644 --- a/packages/in_app_purchase/test/store_kit_wrappers/sk_test_stub_objects.dart +++ b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_test_stub_objects.dart @@ -1,8 +1,8 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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/store_kit_wrappers.dart'; +import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; final dummyPayment = SKPaymentWrapper( productIdentifier: 'prod-id', @@ -74,8 +74,11 @@ Map buildLocaleMap(SKPriceLocaleWrapper local) { }; } -Map buildSubscriptionPeriodMap( - SKProductSubscriptionPeriodWrapper sub) { +Map? buildSubscriptionPeriodMap( + SKProductSubscriptionPeriodWrapper? sub) { + if (sub == null) { + return null; + } return { 'numberOfUnits': sub.numberOfUnits, 'unit': SKSubscriptionPeriodUnit.values.indexOf(sub.unit), @@ -104,7 +107,7 @@ Map buildProductMap(SKProductWrapper product) { 'price': product.price, 'subscriptionPeriod': buildSubscriptionPeriodMap(product.subscriptionPeriod), - 'introductoryPrice': buildDiscountMap(product.introductoryPrice), + 'introductoryPrice': buildDiscountMap(product.introductoryPrice!), }; } @@ -129,17 +132,16 @@ Map buildErrorMap(SKError error) { Map buildTransactionMap( SKPaymentTransactionWrapper transaction) { - if (transaction == null) { - return null; - } - Map map = { + Map map = { 'transactionState': SKPaymentTransactionStateWrapper.values .indexOf(SKPaymentTransactionStateWrapper.purchased), 'payment': transaction.payment.toMap(), - 'originalTransaction': buildTransactionMap(transaction.originalTransaction), + 'originalTransaction': transaction.originalTransaction == null + ? null + : buildTransactionMap(transaction.originalTransaction!), 'transactionTimeStamp': transaction.transactionTimeStamp, 'transactionIdentifier': transaction.transactionIdentifier, - 'error': buildErrorMap(transaction.error), + 'error': buildErrorMap(transaction.error!), }; return map; } diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md b/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md new file mode 100644 index 000000000000..2f529b31655d --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +* Initial open-source release. \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/LICENSE b/packages/in_app_purchase/in_app_purchase_platform_interface/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/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/in_app_purchase/in_app_purchase_platform_interface/README.md b/packages/in_app_purchase/in_app_purchase_platform_interface/README.md new file mode 100644 index 000000000000..91585dbfc88f --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/README.md @@ -0,0 +1,33 @@ +# in_app_purchase_platform_interface + +A common platform interface for the [`in_app_purchase`][1] plugin. + +This interface allows platform-specific implementations of the `in_app_purchase` +plugin, as well as the plugin itself, to ensure they are supporting the +same interface. + +# Usage + +To implement a new platform-specific implementation of `in_app_purchase`, extend +[`InAppPurchasePlatform`][2] with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`InAppPurchasePlatform` by calling +`InAppPurchasePlatform.setInstance(MyPlatformInAppPurchase())`. + +To implement functionality that is specific to the platform and is not covered +by the [`InAppPurchasePlatform`][2] idiomatic API, extend +[`InAppPurchasePlatformAddition`][3] with the platform-specific functionality, +and when the plugin is registered, set the addition instance by calling +`InAppPurchasePlatformAddition.instance = MyPlatformInAppPurchaseAddition()`. + +# 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]: ../in_app_purchase +[2]: lib/in_app_purchase_platform_interface.dart +[3]: lib/in_app_purchase_platform_addition.dart \ No newline at end of file diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/in_app_purchase_platform_interface.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/in_app_purchase_platform_interface.dart new file mode 100644 index 000000000000..25eb4a44c4b4 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/in_app_purchase_platform_interface.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 'src/errors/errors.dart'; +export 'src/in_app_purchase_platform.dart'; +export 'src/in_app_purchase_platform_addition.dart'; +export 'src/in_app_purchase_platform_addition_provider.dart'; +export 'src/types/types.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/errors.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/errors.dart new file mode 100644 index 000000000000..7b788aaef490 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/errors.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 'in_app_purchase_exception.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/in_app_purchase_exception.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/in_app_purchase_exception.dart new file mode 100644 index 000000000000..0a89a6e39a5e --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/in_app_purchase_exception.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. + +/// Thrown to indicate that an action failed while interacting with the +/// in_app_purchase plugin. +class InAppPurchaseException implements Exception { + /// Creates a [InAppPurchaseException] with the specified source and error + /// [code] and optional [message]. + InAppPurchaseException({ + required this.source, + required this.code, + this.message, + }) : assert(code != null); + + /// An error code. + final String code; + + /// A human-readable error message, possibly null. + final String? message; + + /// Which source is the error on. + final String source; + + @override + String toString() => 'InAppPurchaseException($code, $message, $source)'; +} 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 new file mode 100644 index 000000000000..eac4a0712078 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform.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. + +import 'dart:async'; + +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'types/types.dart'; + +/// The interface that implementations of in_app_purchase must implement. +/// +/// Platform implementations should extend this class rather than implement it as `in_app_purchase` +/// 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 +/// [InAppPurchasePlatform] methods. +abstract class InAppPurchasePlatform extends PlatformInterface { + /// Constructs a InAppPurchasePlatform. + InAppPurchasePlatform() : super(token: _token); + + static final Object _token = Object(); + + /// The instance of [InAppPurchasePlatform] to use. + /// + /// Must be set before accessing. + static InAppPurchasePlatform get instance => _instance; + + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [InAppPurchasePlatform] when they register themselves. + // TODO(amirh): Extract common platform interface logic. + // https://github.com/flutter/flutter/issues/43368 + static set instance(InAppPurchasePlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + // Should only be accessed after setter is called. + static late InAppPurchasePlatform _instance; + + /// Listen to this broadcast stream to get real time update for purchases. + /// + /// This stream will never close as long as the app is active. + /// + /// Purchase updates can happen in several situations: + /// * When a purchase is triggered by user in the app. + /// * When a purchase is triggered by user from the platform-specific store front. + /// * When a purchase is restored on the device by the user in the app. + /// * If a purchase is not completed ([completePurchase] is not called on the + /// purchase object) from the last app session. Purchase updates will happen + /// when a new app session starts instead. + /// + /// IMPORTANT! You must subscribe to this stream as soon as your app launches, + /// preferably before returning your main App Widget in main(). Otherwise you + /// will miss purchase updated made before this stream is subscribed to. + /// + /// We also recommend listening to the stream with one subscription at a given + /// time. If you choose to have multiple subscription at the same time, you + /// should be careful at the fact that each subscription will receive all the + /// events after they start to listen. + Stream> get purchaseStream => + throw UnimplementedError('purchaseStream has not been implemented.'); + + /// Returns `true` if the payment platform is ready and available. + Future isAvailable() => + throw UnimplementedError('isAvailable() has not been implemented.'); + + /// Query product details for the given set of IDs. + /// + /// Identifiers in the underlying payment platform, for example, [App Store + /// Connect](https://appstoreconnect.apple.com/) for iOS and [Google Play + /// Console](https://play.google.com/) for Android. + Future queryProductDetails(Set identifiers) => + throw UnimplementedError( + 'queryProductDetails() had not been implemented.'); + + /// Buy a non consumable product or subscription. + /// + /// Non consumable items can only be bought once. For example, a purchase that + /// unlocks a special content in your app. Subscriptions are also non + /// consumable products. + /// + /// You always need to restore all the non consumable products for user when + /// they switch their phones. + /// + /// This method does not return the result of the purchase. Instead, after + /// triggering this method, purchase updates will be sent to + /// [purchaseStream]. You should [Stream.listen] to [purchaseStream] to get + /// [PurchaseDetails] objects in different [PurchaseDetails.status] and update + /// your UI accordingly. When the [PurchaseDetails.status] is + /// [PurchaseStatus.purchased], [PurchaseStatus.restored] or + /// [PurchaseStatus.error] you should deliver the content or handle the error, + /// then call [completePurchase] to finish the purchasing process. + /// + /// This method does return whether or not the purchase request was initially + /// sent successfully. + /// + /// Consumable items are defined differently by the different underlying + /// payment platforms, and there's no way to query for whether or not the + /// [ProductDetail] is a consumable at runtime. + /// + /// See also: + /// + /// * [buyConsumable], for buying a consumable product. + /// * [restorePurchases], for restoring non consumable products. + /// + /// Calling this method for consumable items will cause unwanted behaviors! + Future buyNonConsumable({required PurchaseParam purchaseParam}) => + throw UnimplementedError('buyNonConsumable() has not been implemented.'); + + /// Buy a consumable product. + /// + /// Consumable items can be "consumed" to mark that they've been used and then + /// bought additional times. For example, a health potion. + /// + /// To restore consumable purchases across devices, you should keep track of + /// those purchase on your own server and restore the purchase for your users. + /// Consumed products are no longer considered to be "owned" by payment + /// platforms and will not be delivered by calling [restorePurchases]. + /// + /// Consumable items are defined differently by the different underlying + /// payment platforms, and there's no way to query for whether or not the + /// [ProductDetail] is a consumable at runtime. + /// + /// `autoConsume` is provided as a utility and will instruct the plugin to + /// automatically consume the product after a succesful purchase. + /// `autoConsume` is `true` by default. + /// + /// This method does not return the result of the purchase. Instead, after + /// triggering this method, purchase updates will be sent to + /// [purchaseStream]. You should [Stream.listen] to + /// [purchaseStream] to get [PurchaseDetails] objects in different + /// [PurchaseDetails.status] and update your UI accordingly. When the + /// [PurchaseDetails.status] is [PurchaseStatus.purchased] or + /// [PurchaseStatus.error], you should deliver the content or handle the + /// error, then call [completePurchase] to finish the purchasing process. + /// + /// This method does return whether or not the purchase request was initially + /// sent succesfully. + /// + /// See also: + /// + /// * [buyNonConsumable], for buying a non consumable product or + /// subscription. + /// * [restorePurchases], for restoring non consumable products. + /// + /// Calling this method for non consumable items will cause unwanted + /// behaviors! + Future buyConsumable({ + required PurchaseParam purchaseParam, + bool autoConsume = true, + }) => + throw UnimplementedError('buyConsumable() has not been implemented.'); + + /// Mark that purchased content has been delivered to the user. + /// + /// You are responsible for completing every [PurchaseDetails] whose + /// [PurchaseDetails.status] is [PurchaseStatus.purchased] or + /// [PurchaseStatus.restored]. + /// Completing a [PurchaseStatus.pending] purchase will cause an exception. + /// For convenience, [PurchaseDetails.pendingCompletePurchase] indicates if a + /// purchase is pending for completion. + /// + /// The method will throw a [PurchaseException] when the purchase could not be + /// finished. Depending on the [PurchaseException.errorCode] the developer + /// should try to complete the purchase via this method again, or retry the + /// [completePurchase] method at a later time. If the + /// [PurchaseException.errorCode] indicates you should not retry there might + /// be some issue with the app's code or the configuration of the app in the + /// respective store. The developer is responsible to fix this issue. The + /// [PurchaseException.message] field might provide more information on what + /// went wrong. + Future completePurchase(PurchaseDetails purchase) => + throw UnimplementedError('completePurchase() has not been implemented.'); + + /// Restore all previous purchases. + /// + /// The `applicationUserName` should match whatever was sent in the initial + /// `PurchaseParam`, if anything. If no `applicationUserName` was specified in the initial + /// `PurchaseParam`, use `null`. + /// + /// Restored purchases are delivered through the [purchaseStream] with a + /// status of [PurchaseStatus.restored]. You should listen for these purchases, + /// validate their receipts, deliver the content and mark the purchase complete + /// by calling the [finishPurchase] method for each purchase. + /// + /// This does not return consumed products. If you want to restore unused + /// consumable products, you need to persist consumable product information + /// for your user on your own server. + /// + /// See also: + /// + /// * [refreshPurchaseVerificationData], for reloading failed + /// [PurchaseDetails.verificationData]. + Future restorePurchases({String? applicationUserName}) => + throw UnimplementedError('restorePurchases() has not been implemented.'); +} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform_addition.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform_addition.dart new file mode 100644 index 000000000000..746675549295 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform_addition.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:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; + +// ignore: avoid_classes_with_only_static_members +/// The interface that platform implementations must implement when they want to +/// provide platform-specific in_app_purchase features. +/// +/// Platforms that wants to introduce platform-specific public APIs should create +/// a class that either extend or implements [InAppPurchasePlatformAddition]. Then set +/// the [InAppPurchasePlatformAddition.instance] to an instance of that class. +/// +/// All the APIs added by [InAppPurchasePlatformAddition] implementations will be accessed from +/// [InAppPurchasePlatformAdditionProvider.getPlatformAddition] by the client APPs. +/// To avoid clients directly calling [InAppPurchasePlatform] APIs, +/// an [InAppPurchasePlatformAddition] implementation should not be a type of [InAppPurchasePlatform]. +abstract class InAppPurchasePlatformAddition { + static InAppPurchasePlatformAddition? _instance; + + /// The instance containing the platform-specific in_app_purchase + /// functionality. + /// + /// Returns `null` by default. + /// + /// To implement additional functionality extend + /// [`InAppPurchasePlatformAddition`][3] with the platform-specific + /// functionality, and when the plugin is registered, set the + /// `InAppPurchasePlatformAddition.instance` with the new addition + /// implementation instance. + /// + /// Example implementation might look like this: + /// ```dart + /// class InAppPurchaseMyPlatformAddition extends InAppPurchasePlatformAddition { + /// Future myPlatformMethod() {} + /// } + /// ``` + /// + /// The following snippet shows how to register the `InAppPurchaseMyPlatformAddition`: + /// ```dart + /// class InAppPurchaseMyPlatformPlugin { + /// static void registerWith(Registrar registrar) { + /// // Register the platform-specific implementation of the idiomatic + /// // InAppPurchase API. + /// InAppPurchasePlatform.instance = InAppPurchaseMyPlatformPlugin(); + /// + /// // Register the [InAppPurchaseMyPlatformAddition] containing the + /// // platform-specific functionality. + /// InAppPurchasePlatformAddition.instance = InAppPurchaseMyPlatformAddition(); + /// } + /// } + /// ``` + static InAppPurchasePlatformAddition? get instance => _instance; + + /// Sets the instance to a desired [InAppPurchasePlatformAddition] implementation. + /// + /// The `instance` should not be a type of [InAppPurchasePlatform]. + static set instance(InAppPurchasePlatformAddition? instance) { + assert(instance is! InAppPurchasePlatform); + _instance = instance; + } +} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform_addition_provider.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform_addition_provider.dart new file mode 100644 index 000000000000..642bbb419c6e --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/in_app_purchase_platform_addition_provider.dart @@ -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 'package:in_app_purchase_platform_interface/src/in_app_purchase_platform_addition.dart'; + +/// The [InAppPurchasePlatformAdditionProvider] is responsible for providing +/// a platform-specific [InAppPurchasePlatformAddition]. +/// +/// [InAppPurchasePlatformAddition] implementation contain platform-specific +/// features that are not available from the platform idiomatic +/// [InAppPurchasePlatform] API. +abstract class InAppPurchasePlatformAdditionProvider { + /// Provides a platform-specific implementation of the [InAppPurchasePlatformAddition] + /// class. + T getPlatformAddition(); +} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_error.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_error.dart new file mode 100644 index 000000000000..f305f578f54a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/in_app_purchase_error.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. + +/// Captures an error from the underlying purchase platform. +/// +/// The error can happen during the purchase, restoring a purchase, or querying product. +/// Errors from restoring a purchase are not indicative of any errors during the original purchase. +/// See also: +/// * [ProductDetailsResponse] for error when querying product details. +/// * [PurchaseDetails] for error happened in purchase. +class IAPError { + /// Creates a new IAP error object with the given error details. + IAPError( + {required this.source, + required this.code, + required this.message, + this.details}); + + /// Which source is the error on. + final String source; + + /// The error code. + final String code; + + /// A human-readable error message. + final String message; + + /// Error details, possibly null. + final dynamic details; +} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details.dart new file mode 100644 index 000000000000..2d82d04ae71e --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details.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. + +/// The class represents the information of a product. +class ProductDetails { + /// Creates a new product details object with the provided details. + ProductDetails({ + required this.id, + required this.title, + required this.description, + required this.price, + required this.rawPrice, + required this.currencyCode, + }); + + /// The identifier of the product. + /// + /// For example, on iOS it is specified in App Store Connect; on Android, it is specified in Google Play Console. + final String id; + + /// The title of the product. + /// + /// For example, on iOS it is specified in App Store Connect; on Android, it is specified in Google Play Console. + final String title; + + /// The description of the product. + /// + /// For example, on iOS it is specified in App Store Connect; on Android, it is specified in Google Play Console. + final String description; + + /// The price of the product, formatted with currency symbol ("$0.99"). + /// + /// For example, on iOS it is specified in App Store Connect; on Android, it is specified in Google Play Console. + final String price; + + /// The unformatted price of the product, specified in the App Store Connect or Sku in Google Play console based on the platform. + /// The currency unit for this value can be found in the [currencyCode] property. + /// The value always describes full units of the currency. (e.g. 2.45 in the case of $2.45) + final double rawPrice; + + /// The currency code for the price of the product. + /// Based on the price specified in the App Store Connect or Sku in Google Play console based on the platform. + final String currencyCode; +} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details_response.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details_response.dart new file mode 100644 index 000000000000..11b244a84ae3 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details_response.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 'in_app_purchase_error.dart'; +import 'product_details.dart'; + +/// The response returned by [InAppPurchasePlatform.queryProductDetails]. +/// +/// A list of [ProductDetails] can be obtained from the this response. +class ProductDetailsResponse { + /// Creates a new [ProductDetailsResponse] with the provided response details. + ProductDetailsResponse( + {required this.productDetails, required this.notFoundIDs, this.error}); + + /// Each [ProductDetails] uniquely matches one valid identifier in [identifiers] of [InAppPurchasePlatform.queryProductDetails]. + final List productDetails; + + /// The list of identifiers that are in the `identifiers` of [InAppPurchasePlatform.queryProductDetails] but failed to be fetched. + /// + /// There are multiple platform-specific reasons that product information could fail to be fetched, + /// ranging from products not being correctly configured in the storefront to the queried IDs not existing. + final List notFoundIDs; + + /// A caught platform exception thrown while querying the purchases. + /// + /// The value is `null` if there is no error. + /// + /// It's possible for this to be null but for there still to be notFoundIds in cases where the request itself was a success but the + /// requested IDs could not be found. + final IAPError? error; +} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_details.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_details.dart new file mode 100644 index 000000000000..08d0efe09878 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_details.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 'in_app_purchase_error.dart'; +import 'purchase_status.dart'; +import 'purchase_verification_data.dart'; + +/// Represents the transaction details of a purchase. +class PurchaseDetails { + /// Creates a new PurchaseDetails object with the provided data. + PurchaseDetails({ + this.purchaseID, + required this.productID, + required this.verificationData, + required this.transactionDate, + required this.status, + }); + + /// A unique identifier of the purchase. + final String? purchaseID; + + /// The product identifier of the purchase. + final String productID; + + /// The verification data of the purchase. + /// + /// Use this to verify the purchase. See [PurchaseVerificationData] for + /// details on how to verify purchase use this data. You should never use any + /// purchase data until verified. + final PurchaseVerificationData verificationData; + + /// The timestamp of the transaction. + /// + /// Milliseconds since epoch. + /// + /// The value is `null` if [status] is not [PurchaseStatus.purchased]. + final String? transactionDate; + + /// The status that this [PurchaseDetails] is currently on. + PurchaseStatus status; + + /// The error details when the [status] is [PurchaseStatus.error]. + /// + /// The value is `null` if [status] is not [PurchaseStatus.error]. + IAPError? error; + + /// The developer has to call [InAppPurchasePlatform.completePurchase] if the value is `true` + /// and the product has been delivered to the user. + /// + /// The initial value is `false`. + /// * See also [InAppPurchasePlatform.completePurchase] for more details on completing purchases. + bool pendingCompletePurchase = false; +} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_param.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_param.dart new file mode 100644 index 000000000000..df75159c152b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_param.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 'product_details.dart'; + +/// The parameter object for generating a purchase. +class PurchaseParam { + /// Creates a new purchase parameter object with the given data. + PurchaseParam({ + required this.productDetails, + this.applicationUserName, + }); + + /// The product to create payment for. + /// + /// It has to match one of the valid [ProductDetails] objects that you get from [ProductDetailsResponse] after calling [InAppPurchasePlatform.queryProductDetails]. + final ProductDetails productDetails; + + /// An opaque id for the user's account that's unique to your app. (Optional) + /// + /// Used to help the store detect irregular activity. + /// Do not pass in a clear text, your developer ID, the user’s Apple ID, or the + /// user's Google ID for this field. + /// For example, you can use a one-way hash of the user’s account name on your server. + final String? applicationUserName; +} 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 new file mode 100644 index 000000000000..69f31c8f0641 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_status.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. + +/// Status for a [PurchaseDetails]. +/// +/// This is the type for [PurchaseDetails.status]. +enum PurchaseStatus { + /// The purchase process is pending. + /// + /// You can update UI to let your users know the purchase is pending. + pending, + + /// The purchase is finished and successful. + /// + /// Update your UI to indicate the purchase is finished and deliver the product. + purchased, + + /// Some error occurred in the purchase. The purchasing process if aborted. + error, + + /// The purchase has been restored to the device. + /// + /// You should validate the purchase and if valid deliver the content. Once the + /// content has been delivered or if the receipt is invalid you should finish + /// the purchase by calling the `completePurchase` method. More information on + /// verifying purchases can be found [here](https://pub.dev/packages/in_app_purchase#loading-previous-purchases). + restored, +} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_verification_data.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_verification_data.dart new file mode 100644 index 000000000000..49f2a7539d62 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/purchase_verification_data.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. + +/// Represents the data that is used to verify purchases. +/// +/// The property [source] helps you to determine the method to verify purchases. +/// Different source of purchase has different methods of verifying purchases. +/// +/// Both platforms have 2 ways to verify purchase data. You can either choose to +/// verify the data locally using [localVerificationData] or verify the data +/// using your own server with [serverVerificationData]. It is preferable to +/// verify purchases using a server with [serverVerificationData]. +/// +/// You should never use any purchase data until verified. +class PurchaseVerificationData { + /// Creates a [PurchaseVerificationData] object with the provided information. + PurchaseVerificationData({ + required this.localVerificationData, + required this.serverVerificationData, + required this.source, + }); + + /// The data used for local verification. + /// + /// The data is formatted according to the specifications of the respective + /// store. You can use the [source] field to determine the store from which + /// the data originated and proces the data accordingly. + final String localVerificationData; + + /// The data used for server verification. + final String serverVerificationData; + + /// Indicates the source of the purchase. + final String source; +} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/types.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/types.dart new file mode 100644 index 000000000000..33d183c51d04 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/types.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. + +export 'in_app_purchase_error.dart'; +export 'product_details.dart'; +export 'product_details_response.dart'; +export 'purchase_details.dart'; +export 'purchase_param.dart'; +export 'purchase_status.dart'; +export 'purchase_verification_data.dart'; 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 new file mode 100644 index 000000000000..ce0357dc1919 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/pubspec.yaml @@ -0,0 +1,22 @@ +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 +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.0.0 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + mockito: ^5.0.0 + pedantic: ^1.10.0 diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart new file mode 100644 index 000000000000..9c0f2dc00020 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/test/in_app_purchase_platform_test.dart @@ -0,0 +1,211 @@ +// Copyright 2013 The Flutter Authors. All 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:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; +import 'package:mockito/mockito.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$InAppPurchasePlatform', () { + test('Cannot be implemented with `implements`', () { + expect(() { + InAppPurchasePlatform.instance = ImplementsInAppPurchasePlatform(); + }, throwsNoSuchMethodError); + }); + + test('Can be extended', () { + InAppPurchasePlatform.instance = ExtendsInAppPurchasePlatform(); + }); + + test('Can be mocked with `implements`', () { + InAppPurchasePlatform.instance = MockInAppPurchasePlatform(); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of purchaseStream should throw unimplemented error', + () { + final ExtendsInAppPurchasePlatform inAppPurchasePlatform = + ExtendsInAppPurchasePlatform(); + + expect( + () => inAppPurchasePlatform.purchaseStream, + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of isAvailable should throw unimplemented error', + () { + final ExtendsInAppPurchasePlatform inAppPurchasePlatform = + ExtendsInAppPurchasePlatform(); + + expect( + () => inAppPurchasePlatform.isAvailable(), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of queryProductDetails should throw unimplemented error', + () { + final ExtendsInAppPurchasePlatform inAppPurchasePlatform = + ExtendsInAppPurchasePlatform(); + + expect( + () => inAppPurchasePlatform.queryProductDetails({''}), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of buyNonConsumable should throw unimplemented error', + () { + final ExtendsInAppPurchasePlatform inAppPurchasePlatform = + ExtendsInAppPurchasePlatform(); + + expect( + () => inAppPurchasePlatform.buyNonConsumable( + purchaseParam: MockPurchaseParam(), + ), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of buyConsumable should throw unimplemented error', + () { + final ExtendsInAppPurchasePlatform inAppPurchasePlatform = + ExtendsInAppPurchasePlatform(); + + expect( + () => inAppPurchasePlatform.buyConsumable( + purchaseParam: MockPurchaseParam(), + ), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of completePurchase should throw unimplemented error', + () { + final ExtendsInAppPurchasePlatform inAppPurchasePlatform = + ExtendsInAppPurchasePlatform(); + + expect( + () => inAppPurchasePlatform.completePurchase(MockPurchaseDetails()), + throwsUnimplementedError, + ); + }); + + test( + // ignore: lines_longer_than_80_chars + 'Default implementation of restorePurchases should throw unimplemented error', + () { + final ExtendsInAppPurchasePlatform inAppPurchasePlatform = + ExtendsInAppPurchasePlatform(); + + expect( + () => inAppPurchasePlatform.restorePurchases(), + throwsUnimplementedError, + ); + }); + }); + + group('$InAppPurchasePlatformAddition', () { + setUp(() { + InAppPurchasePlatformAddition.instance = null; + }); + + test('Cannot be implemented with `implements`', () { + expect(InAppPurchasePlatformAddition.instance, isNull); + }); + + test('Can be implemented.', () { + InAppPurchasePlatformAddition.instance = + ImplementsInAppPurchasePlatformAddition(); + }); + + test('InAppPurchasePlatformAddition Can be extended', () { + InAppPurchasePlatformAddition.instance = + ExtendsInAppPurchasePlatformAddition(); + }); + + test('Can not be a `InAppPurchasePlatform`', () { + expect( + () => InAppPurchasePlatformAddition.instance = + ExtendsInAppPurchasePlatformAdditionIsPlatformInterface(), + throwsAssertionError); + }); + + test('Provider can provide', () { + ImplementsInAppPurchasePlatformAdditionProvider.register(); + final ImplementsInAppPurchasePlatformAdditionProvider provider = + ImplementsInAppPurchasePlatformAdditionProvider(); + final InAppPurchasePlatformAddition? addition = + provider.getPlatformAddition(); + expect(addition.runtimeType, ExtendsInAppPurchasePlatformAddition); + }); + + test('Provider can provide `null`', () { + final ImplementsInAppPurchasePlatformAdditionProvider provider = + ImplementsInAppPurchasePlatformAdditionProvider(); + final InAppPurchasePlatformAddition? addition = + provider.getPlatformAddition(); + expect(addition, isNull); + }); + }); +} + +class ImplementsInAppPurchasePlatform implements InAppPurchasePlatform { + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class MockInAppPurchasePlatform extends Mock + with + // ignore: prefer_mixin + MockPlatformInterfaceMixin + implements + InAppPurchasePlatform {} + +class ExtendsInAppPurchasePlatform extends InAppPurchasePlatform {} + +class MockPurchaseParam extends Mock implements PurchaseParam {} + +class MockPurchaseDetails extends Mock implements PurchaseDetails {} + +class ImplementsInAppPurchasePlatformAddition + implements InAppPurchasePlatformAddition { + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class ExtendsInAppPurchasePlatformAddition + extends InAppPurchasePlatformAddition {} + +class ImplementsInAppPurchasePlatformAdditionProvider + implements InAppPurchasePlatformAdditionProvider { + static void register() { + InAppPurchasePlatformAddition.instance = + ExtendsInAppPurchasePlatformAddition(); + } + + @override + T getPlatformAddition() { + return InAppPurchasePlatformAddition.instance as T; + } +} + +class ExtendsInAppPurchasePlatformAdditionIsPlatformInterface + extends InAppPurchasePlatform + implements ExtendsInAppPurchasePlatformAddition {} diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/test/src/errors/in_app_purchase_exception_test.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/test/src/errors/in_app_purchase_exception_test.dart new file mode 100644 index 000000000000..ff9468ec2d88 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/test/src/errors/in_app_purchase_exception_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:in_app_purchase_platform_interface/src/errors/in_app_purchase_exception.dart'; + +void main() { + test('toString: Should return a description of the exception', () { + final InAppPurchaseException exception = InAppPurchaseException( + code: 'error_code', + message: 'dummy message', + source: 'dummy_source', + ); + + // Act + final String actual = exception.toString(); + + // Assert + expect(actual, + 'InAppPurchaseException(error_code, dummy message, dummy_source)'); + }); +} 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 new file mode 100644 index 000000000000..d6cbce04c64a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/test/src/types/product_details_test.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:flutter_test/flutter_test.dart'; +import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; + +void main() { + group('Constructor Tests', () { + test( + 'fromSkProduct should correctly parse data from a SKProductWrapper instance.', + () { + final ProductDetails productDetails = ProductDetails( + id: 'id', + title: 'title', + description: 'description', + price: '13.37', + currencyCode: 'USD', + rawPrice: 13.37); + + expect(productDetails.id, 'id'); + expect(productDetails.title, 'title'); + expect(productDetails.description, 'description'); + expect(productDetails.rawPrice, 13.37); + expect(productDetails.currencyCode, 'USD'); + }); + }); +} diff --git a/packages/in_app_purchase/integration_test/in_app_purchase_test.dart b/packages/in_app_purchase/integration_test/in_app_purchase_test.dart deleted file mode 100644 index a5bfdb0eb409..000000000000 --- a/packages/in_app_purchase/integration_test/in_app_purchase_test.dart +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'package:flutter_test/flutter_test.dart'; -import 'package:in_app_purchase/in_app_purchase.dart'; -import 'package:integration_test/integration_test.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('Can create InAppPurchaseConnection instance', - (WidgetTester tester) async { - final InAppPurchaseConnection connection = InAppPurchaseConnection.instance; - expect(connection, isNotNull); - }); -} diff --git a/packages/in_app_purchase/ios/Assets/.gitkeep b/packages/in_app_purchase/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/in_app_purchase/ios/Tests/InAppPurchasePluginTest.m b/packages/in_app_purchase/ios/Tests/InAppPurchasePluginTest.m deleted file mode 100644 index 369e0ead0ba7..000000000000 --- a/packages/in_app_purchase/ios/Tests/InAppPurchasePluginTest.m +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import -#import "FIAPaymentQueueHandler.h" -#import "Stubs.h" - -@import in_app_purchase; - -@interface InAppPurchasePluginTest : XCTestCase - -@property(strong, nonatomic) InAppPurchasePlugin* plugin; - -@end - -@implementation InAppPurchasePluginTest - -- (void)setUp { - self.plugin = - [[InAppPurchasePluginStub alloc] initWithReceiptManager:[FIAPReceiptManagerStub new]]; -} - -- (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, [NSNumber numberWithBool: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)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)testRetrieveReceiptData { - 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]; - NSLog(@"%@", result); - XCTAssertNotNil(result); -} - -- (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)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 ]); -} - -@end diff --git a/packages/in_app_purchase/ios/Tests/PaymentQueueTest.m b/packages/in_app_purchase/ios/Tests/PaymentQueueTest.m deleted file mode 100644 index 07b6bbb42a65..000000000000 --- a/packages/in_app_purchase/ios/Tests/PaymentQueueTest.m +++ /dev/null @@ -1,212 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import "Stubs.h" - -@import in_app_purchase; - -@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/ios/Tests/ProductRequestHandlerTest.m b/packages/in_app_purchase/ios/Tests/ProductRequestHandlerTest.m deleted file mode 100644 index 5e214e8c795e..000000000000 --- a/packages/in_app_purchase/ios/Tests/ProductRequestHandlerTest.m +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import "Stubs.h" - -@import in_app_purchase; - -#pragma tests start here - -@interface RequestHandlerTest : XCTestCase - -@end - -@implementation RequestHandlerTest - -- (void)testRequestHandlerWithProductRequestSuccess { - SKProductRequestStub *request = - [[SKProductRequestStub alloc] initWithProductIdentifiers:[NSSet setWithArray:@[ @"123" ]]]; - FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; - XCTestExpectation *expectation = - [self expectationWithDescription:@"expect to get response with 1 product"]; - __block SKProductsResponse *response; - [handler - startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable r, NSError *error) { - response = r; - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertNotNil(response); - XCTAssertEqual(response.products.count, 1); - SKProduct *product = response.products.firstObject; - XCTAssertTrue([product.productIdentifier isEqualToString:@"123"]); -} - -- (void)testRequestHandlerWithProductRequestFailure { - SKProductRequestStub *request = [[SKProductRequestStub alloc] - initWithFailureError:[NSError errorWithDomain:@"test" code:123 userInfo:@{}]]; - FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; - XCTestExpectation *expectation = - [self expectationWithDescription:@"expect to get response with 1 product"]; - __block NSError *error; - __block SKProductsResponse *response; - [handler startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable r, NSError *e) { - error = e; - response = r; - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertNotNil(error); - XCTAssertEqual(error.domain, @"test"); - XCTAssertNil(response); -} - -- (void)testRequestHandlerWithRefreshReceiptSuccess { - SKReceiptRefreshRequestStub *request = - [[SKReceiptRefreshRequestStub alloc] initWithReceiptProperties:nil]; - FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; - XCTestExpectation *expectation = [self expectationWithDescription:@"expect no error"]; - __block NSError *e; - [handler - startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable r, NSError *error) { - e = error; - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertNil(e); -} - -- (void)testRequestHandlerWithRefreshReceiptFailure { - SKReceiptRefreshRequestStub *request = [[SKReceiptRefreshRequestStub alloc] - initWithFailureError:[NSError errorWithDomain:@"test" code:123 userInfo:@{}]]; - FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; - XCTestExpectation *expectation = [self expectationWithDescription:@"expect error"]; - __block NSError *error; - __block SKProductsResponse *response; - [handler startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable r, NSError *e) { - error = e; - response = r; - [expectation fulfill]; - }]; - [self waitForExpectations:@[ expectation ] timeout:5]; - XCTAssertNotNil(error); - XCTAssertEqual(error.domain, @"test"); - XCTAssertNil(response); -} - -@end diff --git a/packages/in_app_purchase/ios/Tests/TranslatorTest.m b/packages/in_app_purchase/ios/Tests/TranslatorTest.m deleted file mode 100644 index 135c7f3616f4..000000000000 --- a/packages/in_app_purchase/ios/Tests/TranslatorTest.m +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import "Stubs.h" - -@import in_app_purchase; - -@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; - -@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", - } - }; -} - -- (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.systemLocale; - NSDictionary *map = [FIAObjectTranslator getMapFromNSLocale:system]; - XCTAssertEqualObjects(map[@"currencySymbol"], system.currencySymbol); - } -} - -@end diff --git a/packages/in_app_purchase/ios/in_app_purchase.podspec b/packages/in_app_purchase/ios/in_app_purchase.podspec deleted file mode 100644 index 8da9d7894380..000000000000 --- a/packages/in_app_purchase/ios/in_app_purchase.podspec +++ /dev/null @@ -1,27 +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' - s.version = '0.0.1' - s.summary = 'Flutter In App Purchase' - 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' } - 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, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS' => 'armv7 arm64 x86_64' } - - s.test_spec 'Tests' do |test_spec| - test_spec.source_files = 'Tests/**/*' - test_spec.dependency 'OCMock','3.5' - end -end diff --git a/packages/in_app_purchase/lib/in_app_purchase.dart b/packages/in_app_purchase/lib/in_app_purchase.dart deleted file mode 100644 index 5a68075db3e5..000000000000 --- a/packages/in_app_purchase/lib/in_app_purchase.dart +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -export 'src/in_app_purchase/in_app_purchase_connection.dart'; -export 'src/in_app_purchase/product_details.dart'; -export 'src/in_app_purchase/purchase_details.dart'; diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/billing_client_wrapper.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/billing_client_wrapper.dart deleted file mode 100644 index ebbd90aba0f4..000000000000 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/billing_client_wrapper.dart +++ /dev/null @@ -1,363 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'package:flutter/services.dart'; -import 'package:flutter/foundation.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'; - -@visibleForTesting -const String kOnPurchasesUpdated = - 'PurchasesUpdatedListener#onPurchasesUpdated(int, List)'; -const String _kOnBillingServiceDisconnected = - 'BillingClientStateListener#onBillingServiceDisconnected()'; - -/// Callback triggered by Play in response to purchase activity. -/// -/// This callback is triggered in response to all purchase activity while an -/// instance of `BillingClient` is active. This includes purchases initiated by -/// the app ([BillingClient.launchBillingFlow]) as well as purchases made in -/// Play itself while this app is open. -/// -/// This does not provide any hooks for purchases made in the past. See -/// [BillingClient.queryPurchases] and [BillingClient.queryPurchaseHistory]. -/// -/// All purchase information should also be verified manually, with your server -/// if at all possible. See ["Verify a -/// purchase"](https://developer.android.com/google/play/billing/billing_library_overview#Verify). -/// -/// Wraps a -/// [`PurchasesUpdatedListener`](https://developer.android.com/reference/com/android/billingclient/api/PurchasesUpdatedListener.html). -typedef void PurchasesUpdatedListener(PurchasesResultWrapper purchasesResult); - -/// This class can be used directly instead of [InAppPurchaseConnection] to call -/// Play-specific billing APIs. -/// -/// Wraps a -/// [`com.android.billingclient.api.BillingClient`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient) -/// instance. -/// -/// -/// In general this API conforms to the Java -/// `com.android.billingclient.api.BillingClient` API as much as possible, with -/// some minor changes to account for language differences. Callbacks have been -/// converted to futures where appropriate. -class BillingClient { - bool _enablePendingPurchases = false; - - BillingClient(PurchasesUpdatedListener onPurchasesUpdated) { - assert(onPurchasesUpdated != null); - channel.setMethodCallHandler(callHandler); - _callbacks[kOnPurchasesUpdated] = [onPurchasesUpdated]; - } - - // Occasionally methods in the native layer require a Dart callback to be - // triggered in response to a Java callback. For example, - // [startConnection] registers an [OnBillingServiceDisconnected] callback. - // This list of names to callbacks is used to trigger Dart callbacks in - // response to those Java callbacks. Dart sends the Java layer a handle to the - // 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 = >{}; - - /// Calls - /// [`BillingClient#isReady()`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#isReady()) - /// to get the ready status of the BillingClient instance. - Future isReady() async => - await channel.invokeMethod('BillingClient#isReady()'); - - /// 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. - void enablePendingPurchases() { - _enablePendingPurchases = true; - } - - /// Calls - /// [`BillingClient#startConnection(BillingClientStateListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#startconnection) - /// to create and connect a `BillingClient` instance. - /// - /// [onBillingServiceConnected] has been converted from a callback parameter - /// to the Future result returned by this function. This returns the - /// `BillingClient.BillingResultWrapper` describing the connection result. - /// - /// This triggers the creation of a new `BillingClient` instance in Java if - /// one doesn't already exist. - Future startConnection( - {@required - OnBillingServiceDisconnected onBillingServiceDisconnected}) async { - assert(_enablePendingPurchases, - 'enablePendingPurchases() must be called before calling startConnection'); - List disconnectCallbacks = - _callbacks[_kOnBillingServiceDisconnected] ??= []; - disconnectCallbacks.add(onBillingServiceDisconnected); - return BillingResultWrapper.fromJson(await channel - .invokeMapMethod( - "BillingClient#startConnection(BillingClientStateListener)", - { - 'handle': disconnectCallbacks.length - 1, - 'enablePendingPurchases': _enablePendingPurchases - })); - } - - /// Calls - /// [`BillingClient#endConnection(BillingClientStateListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#endconnect - /// to disconnect a `BillingClient` instance. - /// - /// Will trigger the [OnBillingServiceDisconnected] callback passed to [startConnection]. - /// - /// This triggers the destruction of the `BillingClient` instance in Java. - Future endConnection() async { - return channel.invokeMethod("BillingClient#endConnection()", null); - } - - /// Returns a list of [SkuDetailsWrapper]s that have [SkuDetailsWrapper.sku] - /// in `skusList`, and [SkuDetailsWrapper.type] matching `skuType`. - /// - /// Calls through to [`BillingClient#querySkuDetailsAsync(SkuDetailsParams, - /// SkuDetailsResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#querySkuDetailsAsync(com.android.billingclient.api.SkuDetailsParams,%20com.android.billingclient.api.SkuDetailsResponseListener)) - /// Instead of taking a callback parameter, it returns a Future - /// [SkuDetailsResponseWrapper]. It also takes the values of - /// `SkuDetailsParams` as direct arguments instead of requiring it constructed - /// and passed in as a class. - Future querySkuDetails( - {@required SkuType skuType, @required List skusList}) async { - final Map arguments = { - 'skuType': SkuTypeConverter().toJson(skuType), - 'skusList': skusList - }; - return SkuDetailsResponseWrapper.fromJson(await channel.invokeMapMethod< - String, dynamic>( - 'BillingClient#querySkuDetailsAsync(SkuDetailsParams, SkuDetailsResponseListener)', - arguments)); - } - - /// Attempt to launch the Play Billing Flow for a given [skuDetails]. - /// - /// The [skuDetails] needs to have already been fetched in a [querySkuDetails] - /// call. The [accountId] is an optional hashed string associated with the user - /// that's unique to your app. It's used by Google to detect unusual behavior. - /// Do not pass in a cleartext [accountId], use your developer ID, or use the - /// user's Google ID for this field. - /// - /// Calling this attemps to show the Google Play purchase UI. The user is free - /// to complete the transaction there. - /// - /// This method returns a [BillingResultWrapper] representing the initial attempt - /// to show the Google Play billing flow. Actual purchase updates are - /// delivered via the [PurchasesUpdatedListener]. - /// - /// This method calls through to - /// [`BillingClient#launchBillingFlow`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#launchbillingflow). - /// It constructs a - /// [`BillingFlowParams`](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams) - /// instance by [setting the given - /// skuDetails](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder.html#setskudetails) - /// and [the given - /// accountId](https://developer.android.com/reference/com/android/billingclient/api/BillingFlowParams.Builder.html#setAccountId(java.lang.String)). - Future launchBillingFlow( - {@required String sku, String accountId}) async { - assert(sku != null); - final Map arguments = { - 'sku': sku, - 'accountId': accountId, - }; - return BillingResultWrapper.fromJson( - await channel.invokeMapMethod( - 'BillingClient#launchBillingFlow(Activity, BillingFlowParams)', - arguments)); - } - - /// Fetches recent purchases for the given [SkuType]. - /// - /// Unlike [queryPurchaseHistory], This does not make a network request and - /// does not return items that are no longer owned. - /// - /// All purchase information should also be verified manually, with your - /// server if at all possible. See ["Verify a - /// purchase"](https://developer.android.com/google/play/billing/billing_library_overview#Verify). - /// - /// This wraps [`BillingClient#queryPurchases(String - /// skutype)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#querypurchases). - Future queryPurchases(SkuType skuType) async { - assert(skuType != null); - return PurchasesResultWrapper.fromJson(await channel - .invokeMapMethod( - 'BillingClient#queryPurchases(String)', - {'skuType': SkuTypeConverter().toJson(skuType)})); - } - - /// Fetches purchase history for the given [SkuType]. - /// - /// Unlike [queryPurchases], this makes a network request via Play and returns - /// the most recent purchase for each [SkuDetailsWrapper] of the given - /// [SkuType] even if the item is no longer owned. - /// - /// All purchase information should also be verified manually, with your - /// server if at all possible. See ["Verify a - /// purchase"](https://developer.android.com/google/play/billing/billing_library_overview#Verify). - /// - /// This wraps [`BillingClient#queryPurchaseHistoryAsync(String skuType, - /// PurchaseHistoryResponseListener - /// listener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#querypurchasehistoryasync). - Future queryPurchaseHistory(SkuType skuType) async { - assert(skuType != null); - return PurchasesHistoryResult.fromJson(await channel.invokeMapMethod( - 'BillingClient#queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)', - {'skuType': SkuTypeConverter().toJson(skuType)})); - } - - /// Consumes a given in-app product. - /// - /// Consuming can only be done on an item that's owned, and as a result of consumption, the user will no longer own it. - /// Consumption is done asynchronously. The method returns a Future containing a [BillingResultWrapper]. - /// - /// The `purchaseToken` must not be null. - /// The `developerPayload` is the developer data associated with the purchase to be consumed, it defaults to null. - /// - /// This wraps [`BillingClient#consumeAsync(String, ConsumeResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#consumeAsync(java.lang.String,%20com.android.billingclient.api.ConsumeResponseListener)) - Future consumeAsync(String purchaseToken, - {String developerPayload}) async { - assert(purchaseToken != null); - return BillingResultWrapper.fromJson(await channel - .invokeMapMethod( - 'BillingClient#consumeAsync(String, ConsumeResponseListener)', - { - 'purchaseToken': purchaseToken, - 'developerPayload': developerPayload, - })); - } - - /// Acknowledge an in-app purchase. - /// - /// The developer must acknowledge all in-app purchases after they have been granted to the user. - /// If this doesn't happen within three days of the purchase, the purchase will be refunded. - /// - /// Consumables are already implicitly acknowledged by calls to [consumeAsync] and - /// do not need to be explicitly acknowledged by using this method. - /// However this method can be called for them in order to explicitly acknowledge them if desired. - /// - /// Be sure to only acknowledge a purchase after it has been granted to the user. - /// [PurchaseWrapper.purchaseState] should be [PurchaseStateWrapper.purchased] and - /// the purchase should be validated. See [Verify a purchase](https://developer.android.com/google/play/billing/billing_library_overview#Verify) on verifying purchases. - /// - /// Please refer to [acknowledge](https://developer.android.com/google/play/billing/billing_library_overview#acknowledge) for more - /// details. - /// - /// The `purchaseToken` must not be null. - /// The `developerPayload` is the developer data associated with the purchase to be consumed, it defaults to null. - /// - /// This wraps [`BillingClient#acknowledgePurchase(String, AcknowledgePurchaseResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#acknowledgePurchase(com.android.billingclient.api.AcknowledgePurchaseParams,%20com.android.billingclient.api.AcknowledgePurchaseResponseListener)) - Future acknowledgePurchase(String purchaseToken, - {String developerPayload}) async { - assert(purchaseToken != null); - return BillingResultWrapper.fromJson(await channel.invokeMapMethod( - 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)', - { - 'purchaseToken': purchaseToken, - 'developerPayload': developerPayload, - })); - } - - @visibleForTesting - Future callHandler(MethodCall call) async { - switch (call.method) { - case kOnPurchasesUpdated: - // The purchases updated listener is a singleton. - assert(_callbacks[kOnPurchasesUpdated].length == 1); - final PurchasesUpdatedListener listener = - _callbacks[kOnPurchasesUpdated].first; - listener(PurchasesResultWrapper.fromJson( - call.arguments.cast())); - break; - case _kOnBillingServiceDisconnected: - final int handle = call.arguments['handle']; - await _callbacks[_kOnBillingServiceDisconnected][handle](); - break; - } - } -} - -/// Callback triggered when the [BillingClientWrapper] is disconnected. -/// -/// 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(); - -/// Possible `BillingClient` response statuses. -/// -/// Wraps -/// [`BillingClient.BillingResponse`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponse). -/// See the `BillingResponse` docs for more explanation of the different -/// constants. -enum BillingResponse { - // WARNING: Changes to this class need to be reflected in our generated code. - // Run `flutter packages pub run build_runner watch` to rebuild and watch for - // further changes. - @JsonValue(-2) - featureNotSupported, - - @JsonValue(-1) - serviceDisconnected, - - @JsonValue(0) - ok, - - @JsonValue(1) - userCanceled, - - @JsonValue(2) - serviceUnavailable, - - @JsonValue(3) - billingUnavailable, - - @JsonValue(4) - itemUnavailable, - - @JsonValue(5) - developerError, - - @JsonValue(6) - error, - - @JsonValue(7) - itemAlreadyOwned, - - @JsonValue(8) - itemNotOwned, -} - -/// 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. -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 - // further changes. - - /// A one time product. Acquired in a single transaction. - @JsonValue('inapp') - inapp, - - /// A product requiring a recurring charge over time. - @JsonValue('subs') - subs, -} diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.dart deleted file mode 100644 index 1e81895438c3..000000000000 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.dart +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:in_app_purchase/billing_client_wrappers.dart'; -import 'package:in_app_purchase/in_app_purchase.dart'; -import 'package:json_annotation/json_annotation.dart'; - -part 'enum_converters.g.dart'; - -/// Serializer for [BillingResponse]. -/// -/// Use these in `@JsonSerializable()` classes by annotating them with -/// `@BillingResponseConverter()`. -class BillingResponseConverter implements JsonConverter { - const BillingResponseConverter(); - - @override - BillingResponse fromJson(int json) => _$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 { - const SkuTypeConverter(); - - @override - SkuType fromJson(String json) => - _$enumDecode(_$SkuTypeEnumMap.cast(), json); - - @override - String toJson(SkuType object) => _$SkuTypeEnumMap[object]; -} - -// Define a class so we generate serializer helper methods for the enums -@JsonSerializable() -class _SerializedEnums { - BillingResponse response; - SkuType type; - PurchaseStateWrapper purchaseState; -} - -/// Serializer for [PurchaseStateWrapper]. -/// -/// Use these in `@JsonSerializable()` classes by annotating them with -/// `@PurchaseStateConverter()`. -class PurchaseStateConverter - implements JsonConverter { - const PurchaseStateConverter(); - - @override - PurchaseStateWrapper fromJson(int json) => _$enumDecode( - _$PurchaseStateWrapperEnumMap.cast(), - json); - - @override - int toJson(PurchaseStateWrapper object) => - _$PurchaseStateWrapperEnumMap[object]; - - 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; - } - - throw ArgumentError('$object isn\'t mapped to PurchaseStatus'); - } -} diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.g.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.g.dart deleted file mode 100644 index 899304b08273..000000000000 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/enum_converters.g.dart +++ /dev/null @@ -1,68 +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']); -} - -Map _$_SerializedEnumsToJson(_SerializedEnums instance) => - { - 'response': _$BillingResponseEnumMap[instance.response], - 'type': _$SkuTypeEnumMap[instance.type], - 'purchaseState': _$PurchaseStateWrapperEnumMap[instance.purchaseState], - }; - -T _$enumDecode( - Map enumValues, - dynamic source, { - T unknownValue, -}) { - if (source == null) { - throw ArgumentError('A value must be provided. Supported values: ' - '${enumValues.values.join(', ')}'); - } - - final value = enumValues.entries - .singleWhere((e) => e.value == source, orElse: () => null) - ?.key; - - if (value == null && unknownValue == null) { - throw ArgumentError('`$source` is not one of the supported values: ' - '${enumValues.values.join(', ')}'); - } - return value ?? unknownValue; -} - -const _$BillingResponseEnumMap = { - 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, -}; diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.dart deleted file mode 100644 index 0d4b74f41ab5..000000000000 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.dart +++ /dev/null @@ -1,299 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -import 'dart:ui' show hashValues; -import 'package:flutter/foundation.dart'; -import 'package:json_annotation/json_annotation.dart'; -import 'enum_converters.dart'; -import 'billing_client_wrapper.dart'; -import 'sku_details_wrapper.dart'; - -// WARNING: Changes to `@JsonSerializable` classes need to be reflected in the -// below generated file. Run `flutter packages pub run build_runner watch` to -// rebuild and watch for further changes. -part 'purchase_wrapper.g.dart'; - -/// Data structure representing a successful purchase. -/// -/// All purchase information should also be verified manually, with your -/// server if at all possible. See ["Verify a -/// purchase"](https://developer.android.com/google/play/billing/billing_library_overview#Verify). -/// -/// This wraps [`com.android.billlingclient.api.Purchase`](https://developer.android.com/reference/com/android/billingclient/api/Purchase) -@JsonSerializable() -@PurchaseStateConverter() -class PurchaseWrapper { - @visibleForTesting - PurchaseWrapper( - {@required this.orderId, - @required this.packageName, - @required this.purchaseTime, - @required this.purchaseToken, - @required this.signature, - @required this.sku, - @required this.isAutoRenewing, - @required this.originalJson, - @required this.developerPayload, - @required this.isAcknowledged, - @required this.purchaseState}); - - factory PurchaseWrapper.fromJson(Map map) => _$PurchaseWrapperFromJson(map); - - @override - bool operator ==(Object other) { - if (identical(other, this)) return true; - if (other.runtimeType != runtimeType) return false; - final PurchaseWrapper typedOther = other; - 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; - } - - @override - int get hashCode => hashValues( - orderId, - packageName, - purchaseTime, - purchaseToken, - signature, - sku, - isAutoRenewing, - originalJson, - isAcknowledged, - purchaseState); - - /// The unique ID for this purchase. Corresponds to the Google Payments order - /// ID. - final String orderId; - - /// The package name the purchase was made from. - final String packageName; - - /// When the purchase was made, as an epoch timestamp. - final int purchaseTime; - - /// A unique ID for a given [SkuDetailsWrapper], user, and purchase. - final String purchaseToken; - - /// Signature of purchase data, signed with the developer's private key. Uses - /// RSASSA-PKCS1-v1_5. - final String signature; - - /// The product ID of this purchase. - final String sku; - - /// True for subscriptions that renew automatically. Does not apply to - /// [SkuType.inapp] products. - /// - /// For [SkuType.subs] this means that the subscription is canceled when it is - /// false. - final bool isAutoRenewing; - - /// Details about this purchase, in JSON. - /// - /// This can be used verify a purchase. See ["Verify a purchase on a - /// device"](https://developer.android.com/google/play/billing/billing_library_overview#Verify-purchase-device). - /// Note though that verifying a purchase locally is inherently insecure (see - /// the article for more details). - final String originalJson; - - /// The payload specified by the developer when the purchase was acknowledged or consumed. - final String developerPayload; - - /// Whether the purchase has been acknowledged. - /// - /// A successful purchase has to be acknowledged within 3 days after the purchase via [BillingClient.acknowledgePurchase]. - /// * See also [BillingClient.acknowledgePurchase] for more details on acknowledging purchases. - final bool isAcknowledged; - - /// Determines the current state of the purchase. - /// - /// [BillingClient.acknowledgePurchase] should only be called when the `purchaseState` is [PurchaseStateWrapper.purchased]. - /// * See also [BillingClient.acknowledgePurchase] for more details on acknowledging purchases. - final PurchaseStateWrapper purchaseState; -} - -/// Data structure representing a purchase history record. -/// -/// This class includes a subset of fields in [PurchaseWrapper]. -/// -/// This wraps [`com.android.billlingclient.api.PurchaseHistoryRecord`](https://developer.android.com/reference/com/android/billingclient/api/PurchaseHistoryRecord) -/// -/// * See also: [BillingClient.queryPurchaseHistory] for obtaining a [PurchaseHistoryRecordWrapper]. -// 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() -class PurchaseHistoryRecordWrapper { - @visibleForTesting - PurchaseHistoryRecordWrapper({ - @required this.purchaseTime, - @required this.purchaseToken, - @required this.signature, - @required this.sku, - @required this.originalJson, - @required this.developerPayload, - }); - - factory PurchaseHistoryRecordWrapper.fromJson(Map map) => - _$PurchaseHistoryRecordWrapperFromJson(map); - - /// When the purchase was made, as an epoch timestamp. - final int purchaseTime; - - /// A unique ID for a given [SkuDetailsWrapper], user, and purchase. - final String purchaseToken; - - /// Signature of purchase data, signed with the developer's private key. Uses - /// RSASSA-PKCS1-v1_5. - final String signature; - - /// The product ID of this purchase. - final String sku; - - /// Details about this purchase, in JSON. - /// - /// This can be used verify a purchase. See ["Verify a purchase on a - /// device"](https://developer.android.com/google/play/billing/billing_library_overview#Verify-purchase-device). - /// Note though that verifying a purchase locally is inherently insecure (see - /// the article for more details). - final String originalJson; - - /// The payload specified by the developer when the purchase was acknowledged or consumed. - final String developerPayload; - - @override - bool operator ==(Object other) { - if (identical(other, this)) return true; - if (other.runtimeType != runtimeType) return false; - final PurchaseHistoryRecordWrapper typedOther = other; - return typedOther.purchaseTime == purchaseTime && - typedOther.purchaseToken == purchaseToken && - typedOther.signature == signature && - typedOther.sku == sku && - typedOther.originalJson == originalJson && - typedOther.developerPayload == developerPayload; - } - - @override - int get hashCode => hashValues(purchaseTime, purchaseToken, signature, sku, - originalJson, developerPayload); -} - -/// A data struct representing the result of a transaction. -/// -/// Contains a potentially empty list of [PurchaseWrapper]s, a [BillingResultWrapper] -/// that contains a detailed description of the status and a -/// [BillingResponse] to signify the overall state of the transaction. -/// -/// Wraps [`com.android.billingclient.api.Purchase.PurchasesResult`](https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchasesResult). -@JsonSerializable() -@BillingResponseConverter() -class PurchasesResultWrapper { - PurchasesResultWrapper( - {@required this.responseCode, - @required this.billingResult, - @required this.purchasesList}); - - factory PurchasesResultWrapper.fromJson(Map map) => - _$PurchasesResultWrapperFromJson(map); - - @override - bool operator ==(Object other) { - if (identical(other, this)) return true; - if (other.runtimeType != runtimeType) return false; - final PurchasesResultWrapper typedOther = other; - return typedOther.responseCode == responseCode && - typedOther.purchasesList == purchasesList && - typedOther.billingResult == billingResult; - } - - @override - int get hashCode => hashValues(billingResult, responseCode, purchasesList); - - /// The detailed description of the status of the operation. - final BillingResultWrapper billingResult; - - /// The status of the operation. - /// - /// This can represent either the status of the "query purchase history" half - /// of the operation and the "user made purchases" transaction itself. - final BillingResponse responseCode; - - /// The list of successful purchases made in this transaction. - /// - /// May be empty, especially if [responseCode] is not [BillingResponse.ok]. - final List purchasesList; -} - -/// A data struct representing the result of a purchase history. -/// -/// Contains a potentially empty list of [PurchaseHistoryRecordWrapper]s and a [BillingResultWrapper] -/// that contains a detailed description of the status. -@JsonSerializable() -@BillingResponseConverter() -class PurchasesHistoryResult { - PurchasesHistoryResult( - {@required this.billingResult, @required this.purchaseHistoryRecordList}); - - factory PurchasesHistoryResult.fromJson(Map map) => - _$PurchasesHistoryResultFromJson(map); - - @override - bool operator ==(Object other) { - if (identical(other, this)) return true; - if (other.runtimeType != runtimeType) return false; - final PurchasesHistoryResult typedOther = other; - return typedOther.purchaseHistoryRecordList == purchaseHistoryRecordList && - typedOther.billingResult == billingResult; - } - - @override - int get hashCode => hashValues(billingResult, purchaseHistoryRecordList); - - /// The detailed description of the status of the [BillingClient.queryPurchaseHistory]. - final BillingResultWrapper billingResult; - - /// The list of queried purchase history records. - /// - /// May be empty, especially if [billingResult.responseCode] is not [BillingResponse.ok]. - final List purchaseHistoryRecordList; -} - -/// Possible state of a [PurchaseWrapper]. -/// -/// Wraps -/// [`BillingClient.api.Purchase.PurchaseState`](https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchaseState.html). -/// * See also: [PurchaseWrapper]. -enum PurchaseStateWrapper { - /// The state is unspecified. - /// - /// No actions on the [PurchaseWrapper] should be performed on this state. - /// This is a catch-all. It should never be returned by the Play Billing Library. - @JsonValue(0) - unspecified_state, - - /// The user has completed the purchase process. - /// - /// The production should be delivered and then the purchase should be acknowledged. - /// * See also [BillingClient.acknowledgePurchase] for more details on acknowledging purchases. - @JsonValue(1) - purchased, - - /// The user has started the purchase process. - /// - /// The user should follow the instructions that were given to them by the Play - /// Billing Library to complete the purchase. - /// - /// You can also choose to remind the user to complete the purchase if you detected a - /// [PurchaseWrapper] is still in the `pending` state in the future while calling [BillingClient.queryPurchases]. - @JsonValue(2) - pending, -} diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.g.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.g.dart deleted file mode 100644 index 3d555890b31e..000000000000 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/purchase_wrapper.g.dart +++ /dev/null @@ -1,98 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -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, - 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, - purchaseState: - const PurchaseStateConverter().fromJson(json['purchaseState'] as int), - ); -} - -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), - }; - -PurchaseHistoryRecordWrapper _$PurchaseHistoryRecordWrapperFromJson(Map json) { - return PurchaseHistoryRecordWrapper( - purchaseTime: json['purchaseTime'] as int, - purchaseToken: json['purchaseToken'] as String, - signature: json['signature'] as String, - sku: json['sku'] as String, - originalJson: json['originalJson'] as String, - developerPayload: json['developerPayload'] as String, - ); -} - -Map _$PurchaseHistoryRecordWrapperToJson( - PurchaseHistoryRecordWrapper instance) => - { - 'purchaseTime': instance.purchaseTime, - 'purchaseToken': instance.purchaseToken, - 'signature': instance.signature, - 'sku': instance.sku, - 'originalJson': instance.originalJson, - 'developerPayload': instance.developerPayload, - }; - -PurchasesResultWrapper _$PurchasesResultWrapperFromJson(Map json) { - return PurchasesResultWrapper( - responseCode: - const BillingResponseConverter().fromJson(json['responseCode'] as int), - billingResult: BillingResultWrapper.fromJson(json['billingResult'] as Map), - purchasesList: (json['purchasesList'] as List) - .map((e) => PurchaseWrapper.fromJson(e as Map)) - .toList(), - ); -} - -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), - purchaseHistoryRecordList: (json['purchaseHistoryRecordList'] as List) - .map((e) => PurchaseHistoryRecordWrapper.fromJson(e as Map)) - .toList(), - ); -} - -Map _$PurchasesHistoryResultToJson( - PurchasesHistoryResult instance) => - { - 'billingResult': instance.billingResult, - 'purchaseHistoryRecordList': instance.purchaseHistoryRecordList, - }; diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.dart deleted file mode 100644 index 4d6a9307a53a..000000000000 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.dart +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -import '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 -// rebuild and watch for further changes. -part 'sku_details_wrapper.g.dart'; - -/// Dart wrapper around [`com.android.billingclient.api.SkuDetails`](https://developer.android.com/reference/com/android/billingclient/api/SkuDetails). -/// -/// Contains the details of an available product in Google Play Billing. -@JsonSerializable() -@SkuTypeConverter() -class SkuDetailsWrapper { - @visibleForTesting - SkuDetailsWrapper({ - @required this.description, - @required this.freeTrialPeriod, - @required this.introductoryPrice, - @required this.introductoryPriceMicros, - @required this.introductoryPriceCycles, - @required this.introductoryPricePeriod, - @required this.price, - @required this.priceAmountMicros, - @required this.priceCurrencyCode, - @required this.sku, - @required this.subscriptionPeriod, - @required this.title, - @required this.type, - @required this.isRewarded, - @required this.originalPrice, - @required this.originalPriceAmountMicros, - }); - - /// 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. - @visibleForTesting - factory SkuDetailsWrapper.fromJson(Map map) => - _$SkuDetailsWrapperFromJson(map); - - final String description; - - /// Trial period in ISO 8601 format. - final String freeTrialPeriod; - - /// Introductory price, only applies to [SkuType.subs]. Formatted ("$0.99"). - final String introductoryPrice; - - /// [introductoryPrice] in micro-units 990000 - final String introductoryPriceMicros; - - /// The number of billing perios that [introductoryPrice] is valid for ("2"). - final String introductoryPriceCycles; - - /// The billing period of [introductoryPrice], in ISO 8601 format. - final String introductoryPricePeriod; - - /// Formatted with currency symbol ("$0.99"). - final String price; - - /// [price] in micro-units ("990000"). - final int priceAmountMicros; - - /// [price] ISO 4217 currency code. - final String priceCurrencyCode; - - /// The product ID in Google Play Console. - final String sku; - - /// Applies to [SkuType.subs], formatted in ISO 8601. - final String subscriptionPeriod; - final String title; - - /// The [SkuType] of the product. - final SkuType type; - - /// False if the product is paid. - final bool isRewarded; - - /// The original price that the user purchased this product for. - final String originalPrice; - - /// [originalPrice] in micro-units ("990000"). - final int originalPriceAmountMicros; - - @override - bool operator ==(dynamic other) { - if (other.runtimeType != runtimeType) { - return false; - } - - final SkuDetailsWrapper typedOther = other; - return typedOther is SkuDetailsWrapper && - typedOther.description == description && - typedOther.freeTrialPeriod == freeTrialPeriod && - typedOther.introductoryPrice == introductoryPrice && - typedOther.introductoryPriceMicros == introductoryPriceMicros && - typedOther.introductoryPriceCycles == introductoryPriceCycles && - typedOther.introductoryPricePeriod == introductoryPricePeriod && - typedOther.price == price && - typedOther.priceAmountMicros == priceAmountMicros && - typedOther.sku == sku && - typedOther.subscriptionPeriod == subscriptionPeriod && - typedOther.title == title && - typedOther.type == type && - typedOther.isRewarded == isRewarded && - typedOther.originalPrice == originalPrice && - typedOther.originalPriceAmountMicros == originalPriceAmountMicros; - } - - @override - int get hashCode { - return hashValues( - description.hashCode, - freeTrialPeriod.hashCode, - introductoryPrice.hashCode, - introductoryPriceMicros.hashCode, - introductoryPriceCycles.hashCode, - introductoryPricePeriod.hashCode, - price.hashCode, - priceAmountMicros.hashCode, - sku.hashCode, - subscriptionPeriod.hashCode, - title.hashCode, - type.hashCode, - isRewarded.hashCode, - originalPrice, - originalPriceAmountMicros); - } -} - -/// Translation of [`com.android.billingclient.api.SkuDetailsResponseListener`](https://developer.android.com/reference/com/android/billingclient/api/SkuDetailsResponseListener.html). -/// -/// Returned by [BillingClient.querySkuDetails]. -@JsonSerializable() -class SkuDetailsResponseWrapper { - @visibleForTesting - SkuDetailsResponseWrapper( - {@required this.billingResult, this.skuDetailsList}); - - /// 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 SkuDetailsResponseWrapper.fromJson(Map map) => - _$SkuDetailsResponseWrapperFromJson(map); - - /// The final result of the [BillingClient.querySkuDetails] call. - final BillingResultWrapper billingResult; - - /// A list of [SkuDetailsWrapper] matching the query to [BillingClient.querySkuDetails]. - final List skuDetailsList; - - @override - bool operator ==(dynamic other) { - if (other.runtimeType != runtimeType) { - return false; - } - - final SkuDetailsResponseWrapper typedOther = other; - return typedOther is SkuDetailsResponseWrapper && - typedOther.billingResult == billingResult && - typedOther.skuDetailsList == skuDetailsList; - } - - @override - int get hashCode => hashValues(billingResult, skuDetailsList); -} - -/// Params containing the response code and the debug message from the Play Billing API response. -@JsonSerializable() -@BillingResponseConverter() -class BillingResultWrapper { - /// Constructs the object with [responseCode] and [debugMessage]. - BillingResultWrapper({@required this.responseCode, this.debugMessage}); - - /// Constructs an instance of this from a key value map of data. - /// - /// The map needs to have named string keys with values matching the names and - /// types of all of the members on this class. - factory BillingResultWrapper.fromJson(Map map) => - _$BillingResultWrapperFromJson(map); - - /// Response code returned in the Play Billing API calls. - final BillingResponse responseCode; - - /// Debug message returned in the Play Billing API calls. - /// - /// This message uses an en-US locale and should not be shown to users. - final String debugMessage; - - @override - bool operator ==(dynamic other) { - if (other.runtimeType != runtimeType) { - return false; - } - - final BillingResultWrapper typedOther = other; - return typedOther is BillingResultWrapper && - typedOther.responseCode == responseCode && - typedOther.debugMessage == debugMessage; - } - - @override - int get hashCode => hashValues(responseCode, debugMessage); -} diff --git a/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart b/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart deleted file mode 100644 index 70bde9318f03..000000000000 --- a/packages/in_app_purchase/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart +++ /dev/null @@ -1,80 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -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['introductoryPriceMicros'] as String, - introductoryPriceCycles: json['introductoryPriceCycles'] as String, - introductoryPricePeriod: json['introductoryPricePeriod'] as String, - price: json['price'] as String, - priceAmountMicros: json['priceAmountMicros'] as int, - priceCurrencyCode: json['priceCurrencyCode'] as String, - sku: json['sku'] as String, - subscriptionPeriod: json['subscriptionPeriod'] as String, - title: json['title'] as String, - type: const SkuTypeConverter().fromJson(json['type'] as String), - isRewarded: json['isRewarded'] as bool, - originalPrice: json['originalPrice'] as String, - originalPriceAmountMicros: json['originalPriceAmountMicros'] as int, - ); -} - -Map _$SkuDetailsWrapperToJson(SkuDetailsWrapper instance) => - { - 'description': instance.description, - 'freeTrialPeriod': instance.freeTrialPeriod, - 'introductoryPrice': instance.introductoryPrice, - 'introductoryPriceMicros': instance.introductoryPriceMicros, - 'introductoryPriceCycles': instance.introductoryPriceCycles, - 'introductoryPricePeriod': instance.introductoryPricePeriod, - 'price': instance.price, - 'priceAmountMicros': instance.priceAmountMicros, - 'priceCurrencyCode': instance.priceCurrencyCode, - 'sku': instance.sku, - 'subscriptionPeriod': instance.subscriptionPeriod, - 'title': instance.title, - 'type': const SkuTypeConverter().toJson(instance.type), - 'isRewarded': instance.isRewarded, - 'originalPrice': instance.originalPrice, - 'originalPriceAmountMicros': instance.originalPriceAmountMicros, - }; - -SkuDetailsResponseWrapper _$SkuDetailsResponseWrapperFromJson(Map json) { - return SkuDetailsResponseWrapper( - billingResult: BillingResultWrapper.fromJson(json['billingResult'] as Map), - skuDetailsList: (json['skuDetailsList'] as List) - .map((e) => SkuDetailsWrapper.fromJson(e as Map)) - .toList(), - ); -} - -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, - }; diff --git a/packages/in_app_purchase/lib/src/channel.dart b/packages/in_app_purchase/lib/src/channel.dart deleted file mode 100644 index b10507067ca5..000000000000 --- a/packages/in_app_purchase/lib/src/channel.dart +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/services.dart'; - -const MethodChannel channel = - MethodChannel('plugins.flutter.io/in_app_purchase'); - -const MethodChannel callbackChannel = - MethodChannel('plugins.flutter.io/in_app_purchase_callback'); diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/README.md b/packages/in_app_purchase/lib/src/in_app_purchase/README.md deleted file mode 100644 index 8e064c67ef56..000000000000 --- a/packages/in_app_purchase/lib/src/in_app_purchase/README.md +++ /dev/null @@ -1,23 +0,0 @@ -# in_app_purchase - -A simplified, generic API for handling in app purchases with a single code base. - -You can use this to: - -* Display a list of products for sale from App Store (on iOS) or Google Play (on - Android) -* Purchase a product. From the App Store this supports consumables, - non-consumables, and subscriptions. From Google Play this supports both in app - purchases and subscriptions. -* Load previously purchased products, to the extent that this is supported in - both underlying platforms. - -This can be used in addition to or as an alternative to -[billing_client_wrappers](../billing_client_wrappers/README.md) and -[store_kit_wrappers](../store_kit_wrappers/README.md). - -`InAppPurchaseConnection` tries to be as platform agnostic as possible, but in -some cases differentiating between the underlying platforms is unavoidable. - -You can see a sample usage of this in the [example -app](../../../example/README.md). diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/app_store_connection.dart b/packages/in_app_purchase/lib/src/in_app_purchase/app_store_connection.dart deleted file mode 100644 index da6fc7417585..000000000000 --- a/packages/in_app_purchase/lib/src/in_app_purchase/app_store_connection.dart +++ /dev/null @@ -1,250 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:in_app_purchase/src/in_app_purchase/purchase_details.dart'; -import 'in_app_purchase_connection.dart'; -import 'product_details.dart'; -import 'package:in_app_purchase/store_kit_wrappers.dart'; -import 'package:in_app_purchase/src/store_kit_wrappers/enum_converters.dart'; -import '../../billing_client_wrappers.dart'; - -/// An [InAppPurchaseConnection] that wraps StoreKit. -/// -/// This translates various `StoreKit` calls and responses into the -/// generic plugin API. -class AppStoreConnection implements InAppPurchaseConnection { - static AppStoreConnection get instance => _getOrCreateInstance(); - static AppStoreConnection _instance; - static SKPaymentQueueWrapper _skPaymentQueueWrapper; - static _TransactionObserver _observer; - - Stream> get purchaseUpdatedStream => - _observer.purchaseUpdatedController.stream; - - static SKTransactionObserverWrapper get observer => _observer; - - static AppStoreConnection _getOrCreateInstance() { - if (_instance != null) { - return _instance; - } - - _instance = AppStoreConnection(); - _skPaymentQueueWrapper = SKPaymentQueueWrapper(); - _observer = _TransactionObserver(StreamController.broadcast()); - _skPaymentQueueWrapper.setTransactionObserver(observer); - return _instance; - } - - @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.sandboxTesting, - 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, - {String developerPayload}) async { - await _skPaymentQueueWrapper - .finishTransaction(purchase.skPaymentTransaction); - return BillingResultWrapper(responseCode: BillingResponse.ok); - } - - @override - Future consumePurchase(PurchaseDetails purchase, - {String developerPayload}) { - throw UnsupportedError('consume purchase is not available on Android'); - } - - @override - Future queryPastPurchases( - {String applicationUserName}) async { - IAPError error; - List pastPurchases = []; - - try { - String receiptData = await _observer.getReceiptData(); - final List restoredTransactions = - await _observer.getRestoredTransactions( - queue: _skPaymentQueueWrapper, - applicationUserName: applicationUserName); - _observer.cleanUpRestoredTransactions(); - pastPurchases = - restoredTransactions.map((SKPaymentTransactionWrapper transaction) { - assert(transaction.transactionState == - SKPaymentTransactionStateWrapper.restored); - return PurchaseDetails.fromSKTransaction(transaction, receiptData) - ..status = SKTransactionStatusConverter() - .toPurchaseStatus(transaction.transactionState) - ..error = transaction.error != null - ? IAPError( - source: IAPSource.AppStore, - code: kPurchaseErrorCode, - message: transaction.error.domain, - details: transaction.error.userInfo, - ) - : null; - }).toList(); - } on PlatformException catch (e) { - error = IAPError( - source: IAPSource.AppStore, - code: e.code, - message: e.message, - details: e.details); - } on SKError catch (e) { - error = IAPError( - source: IAPSource.AppStore, - code: kRestoredPurchaseErrorCode, - message: e.domain, - details: e.userInfo); - } - return QueryPurchaseDetailsResponse( - pastPurchases: pastPurchases, error: error); - } - - @override - Future refreshPurchaseVerificationData() async { - await SKRequestMaker().startRefreshReceiptRequest(); - String receipt = await SKReceiptManager.retrieveReceiptData(); - return PurchaseVerificationData( - localVerificationData: receipt, - serverVerificationData: receipt, - source: IAPSource.AppStore); - } - - /// 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) => - ProductDetails.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: IAPSource.AppStore, - code: exception.code, - message: exception.message, - details: exception.details), - ); - return productDetailsResponse; - } -} - -class _TransactionObserver implements SKTransactionObserverWrapper { - final StreamController> purchaseUpdatedController; - - Completer> _restoreCompleter; - List _restoredTransactions; - String _receiptData; - - _TransactionObserver(this.purchaseUpdatedController); - - Future> getRestoredTransactions( - {@required SKPaymentQueueWrapper queue, String applicationUserName}) { - assert(queue != null); - _restoreCompleter = Completer(); - queue.restoreTransactions(applicationUserName: applicationUserName); - return _restoreCompleter.future; - } - - void cleanUpRestoredTransactions() { - _restoredTransactions = null; - _restoreCompleter = null; - } - - void updatedTransactions( - {List transactions}) async { - if (_restoreCompleter != null) { - if (_restoredTransactions == null) { - _restoredTransactions = []; - } - _restoredTransactions - .addAll(transactions.where((SKPaymentTransactionWrapper wrapper) { - return wrapper.transactionState == - SKPaymentTransactionStateWrapper.restored; - }).map((SKPaymentTransactionWrapper wrapper) => wrapper)); - } - - String receiptData = await getReceiptData(); - purchaseUpdatedController - .add(transactions.where((SKPaymentTransactionWrapper wrapper) { - return wrapper.transactionState != - SKPaymentTransactionStateWrapper.restored; - }).map((SKPaymentTransactionWrapper transaction) { - PurchaseDetails purchaseDetails = - PurchaseDetails.fromSKTransaction(transaction, receiptData); - return purchaseDetails; - }).toList()); - } - - void removedTransactions({List transactions}) {} - - /// Triggered when there is an error while restoring transactions. - void restoreCompletedTransactionsFailed({SKError error}) { - _restoreCompleter.completeError(error); - } - - void paymentQueueRestoreCompletedTransactionsFinished() { - _restoreCompleter.complete(_restoredTransactions ?? []); - } - - bool shouldAddStorePayment( - {SKPaymentWrapper payment, 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 = null; - } - return _receiptData; - } -} diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart b/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart deleted file mode 100644 index 581a7bd9f8fe..000000000000 --- a/packages/in_app_purchase/lib/src/in_app_purchase/google_play_connection.dart +++ /dev/null @@ -1,287 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; -import 'package:in_app_purchase/src/in_app_purchase/purchase_details.dart'; -import '../../billing_client_wrappers.dart'; -import 'in_app_purchase_connection.dart'; -import 'product_details.dart'; - -/// An [InAppPurchaseConnection] that wraps Google Play Billing. -/// -/// This translates various [BillingClient] calls and responses into the -/// common plugin API. -class GooglePlayConnection - with WidgetsBindingObserver - implements InAppPurchaseConnection { - GooglePlayConnection._() - : billingClient = - BillingClient((PurchasesResultWrapper resultWrapper) async { - _purchaseUpdatedController - .add(await _getPurchaseDetailsFromResult(resultWrapper)); - }) { - if (InAppPurchaseConnection.enablePendingPurchase) { - billingClient.enablePendingPurchases(); - } - _readyFuture = _connect(); - WidgetsBinding.instance.addObserver(this); - _purchaseUpdatedController = StreamController.broadcast(); - ; - } - static GooglePlayConnection get instance => _getOrCreateInstance(); - static GooglePlayConnection _instance; - - Stream> get purchaseUpdatedStream => - _purchaseUpdatedController.stream; - static StreamController> _purchaseUpdatedController; - - @visibleForTesting - final BillingClient billingClient; - - Future _readyFuture; - static Set _productIdsToConsume = Set(); - - @override - Future isAvailable() async { - await _readyFuture; - return billingClient.isReady(); - } - - @override - Future buyNonConsumable({@required PurchaseParam purchaseParam}) async { - BillingResultWrapper billingResultWrapper = - await billingClient.launchBillingFlow( - sku: purchaseParam.productDetails.id, - accountId: purchaseParam.applicationUserName); - return billingResultWrapper.responseCode == BillingResponse.ok; - } - - @override - Future buyConsumable( - {@required PurchaseParam purchaseParam, bool autoConsume = true}) { - if (autoConsume) { - _productIdsToConsume.add(purchaseParam.productDetails.id); - } - return buyNonConsumable(purchaseParam: purchaseParam); - } - - @override - Future completePurchase(PurchaseDetails purchase, - {String developerPayload}) async { - if (purchase.billingClientPurchase.isAcknowledged) { - return BillingResultWrapper(responseCode: BillingResponse.ok); - } - return await billingClient.acknowledgePurchase( - purchase.verificationData.serverVerificationData, - developerPayload: developerPayload); - } - - @override - Future consumePurchase(PurchaseDetails purchase, - {String developerPayload}) { - return billingClient.consumeAsync( - purchase.verificationData.serverVerificationData, - developerPayload: developerPayload); - } - - @override - Future queryPastPurchases( - {String applicationUserName}) async { - List responses; - PlatformException exception; - try { - responses = await Future.wait([ - billingClient.queryPurchases(SkuType.inapp), - billingClient.queryPurchases(SkuType.subs) - ]); - } on PlatformException catch (e) { - exception = e; - responses = [ - PurchasesResultWrapper( - responseCode: BillingResponse.error, - purchasesList: [], - billingResult: BillingResultWrapper( - responseCode: BillingResponse.error, - debugMessage: e.details.toString(), - ), - ), - PurchasesResultWrapper( - responseCode: BillingResponse.error, - purchasesList: [], - billingResult: BillingResultWrapper( - responseCode: BillingResponse.error, - debugMessage: e.details.toString(), - ), - ) - ]; - } - - Set errorCodeSet = responses - .where((PurchasesResultWrapper response) => - response.responseCode != BillingResponse.ok) - .map((PurchasesResultWrapper response) => - response.responseCode.toString()) - .toSet(); - - String errorMessage = - errorCodeSet.isNotEmpty ? errorCodeSet.join(', ') : null; - - List pastPurchases = - responses.expand((PurchasesResultWrapper response) { - return response.purchasesList; - }).map((PurchaseWrapper purchaseWrapper) { - return PurchaseDetails.fromPurchase(purchaseWrapper); - }).toList(); - - IAPError error; - if (exception != null) { - error = IAPError( - source: IAPSource.GooglePlay, - code: exception.code, - message: exception.message, - details: exception.details); - } else if (errorMessage != null) { - error = IAPError( - source: IAPSource.GooglePlay, - code: kRestoredPurchaseErrorCode, - message: errorMessage); - } - - return QueryPurchaseDetailsResponse( - pastPurchases: pastPurchases, error: error); - } - - @override - Future refreshPurchaseVerificationData() async { - throw UnsupportedError( - 'The method only works on iOS.'); - } - - @visibleForTesting - static void reset() => _instance = null; - - static GooglePlayConnection _getOrCreateInstance() { - if (_instance != null) { - return _instance; - } - - _instance = GooglePlayConnection._(); - return _instance; - } - - Future _connect() => - billingClient.startConnection(onBillingServiceDisconnected: () {}); - - /// Query the product detail list. - /// - /// This method only returns [ProductDetailsResponse]. - /// To get detailed Google Play sku list, use [BillingClient.querySkuDetails] - /// to get the [SkuDetailsResponseWrapper]. - Future queryProductDetails( - Set identifiers) async { - List responses; - PlatformException exception; - try { - responses = await Future.wait([ - billingClient.querySkuDetails( - skuType: SkuType.inapp, skusList: identifiers.toList()), - billingClient.querySkuDetails( - skuType: SkuType.subs, skusList: identifiers.toList()) - ]); - } on PlatformException catch (e) { - exception = e; - responses = [ - // ignore: invalid_use_of_visible_for_testing_member - SkuDetailsResponseWrapper( - billingResult: BillingResultWrapper( - responseCode: BillingResponse.error, debugMessage: e.code), - skuDetailsList: []), - // ignore: invalid_use_of_visible_for_testing_member - SkuDetailsResponseWrapper( - billingResult: BillingResultWrapper( - responseCode: BillingResponse.error, debugMessage: e.code), - skuDetailsList: []) - ]; - } - List productDetailsList = - responses.expand((SkuDetailsResponseWrapper response) { - return response.skuDetailsList; - }).map((SkuDetailsWrapper skuDetailWrapper) { - return ProductDetails.fromSkuDetails(skuDetailWrapper); - }).toList(); - - Set successIDS = productDetailsList - .map((ProductDetails productDetails) => productDetails.id) - .toSet(); - List notFoundIDS = identifiers.difference(successIDS).toList(); - return ProductDetailsResponse( - productDetails: productDetailsList, - notFoundIDs: notFoundIDS, - error: exception == null - ? null - : IAPError( - source: IAPSource.GooglePlay, - code: exception.code, - message: exception.message, - details: exception.details)); - } - - static Future> _getPurchaseDetailsFromResult( - PurchasesResultWrapper resultWrapper) async { - IAPError error; - if (resultWrapper.responseCode != BillingResponse.ok) { - error = IAPError( - source: IAPSource.GooglePlay, - code: kPurchaseErrorCode, - message: resultWrapper.responseCode.toString(), - details: resultWrapper.billingResult.debugMessage, - ); - } - final List> purchases = - resultWrapper.purchasesList.map((PurchaseWrapper purchase) { - return _maybeAutoConsumePurchase( - PurchaseDetails.fromPurchase(purchase)..error = error); - }).toList(); - if (purchases.isNotEmpty) { - return Future.wait(purchases); - } else { - return [ - PurchaseDetails( - purchaseID: null, - productID: null, - transactionDate: null, - verificationData: null) - ..status = PurchaseStatus.error - ..error = error - ]; - } - } - - static Future _maybeAutoConsumePurchase( - PurchaseDetails purchaseDetails) async { - if (!(purchaseDetails.status == PurchaseStatus.purchased && - _productIdsToConsume.contains(purchaseDetails.productID))) { - return purchaseDetails; - } - - final BillingResultWrapper billingResult = - await instance.consumePurchase(purchaseDetails); - final BillingResponse consumedResponse = billingResult.responseCode; - if (consumedResponse != BillingResponse.ok) { - purchaseDetails.status = PurchaseStatus.error; - purchaseDetails.error = IAPError( - source: IAPSource.GooglePlay, - code: kConsumptionFailedErrorCode, - message: consumedResponse.toString(), - details: billingResult.debugMessage, - ); - } - _productIdsToConsume.remove(purchaseDetails.productID); - - return purchaseDetails; - } -} diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/in_app_purchase_connection.dart b/packages/in_app_purchase/lib/src/in_app_purchase/in_app_purchase_connection.dart deleted file mode 100644 index ba3932f73878..000000000000 --- a/packages/in_app_purchase/lib/src/in_app_purchase/in_app_purchase_connection.dart +++ /dev/null @@ -1,299 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:io'; -import 'app_store_connection.dart'; -import 'google_play_connection.dart'; -import 'product_details.dart'; -import 'package:flutter/foundation.dart'; -import 'package:in_app_purchase/billing_client_wrappers.dart'; -import './purchase_details.dart'; - -export 'package:in_app_purchase/billing_client_wrappers.dart'; - -/// Basic API for making in app purchases across multiple platforms. -/// -/// This is a generic abstraction built from `billing_client_wrapers` and -/// `store_kit_wrappers`. Either library can be used for their respective -/// platform instead of this. -abstract class InAppPurchaseConnection { - /// Listen to this broadcast stream to get real time update for purchases. - /// - /// This stream will never close as long as the app is active. - /// - /// Purchase updates can happen in several situations: - /// * When a purchase is triggered by user in the app. - /// * When a purchase is triggered by user from App Store or Google Play. - /// * If a purchase is not completed ([completePurchase] is not called on the - /// purchase object) from the last app session. Purchase updates will happen - /// when a new app session starts instead. - /// - /// IMPORTANT! You must subscribe to this stream as soon as your app launches, - /// preferably before returning your main App Widget in main(). Otherwise you - /// will miss purchase updated made before this stream is subscribed to. - /// - /// We also recommend listening to the stream with one subscription at a given - /// time. If you choose to have multiple subscription at the same time, you - /// should be careful at the fact that each subscription will receive all the - /// events after they start to listen. - Stream> get purchaseUpdatedStream => _getStream(); - - Stream> _purchaseUpdatedStream; - - Stream> _getStream() { - if (_purchaseUpdatedStream != null) { - return _purchaseUpdatedStream; - } - - if (Platform.isAndroid) { - _purchaseUpdatedStream = - GooglePlayConnection.instance.purchaseUpdatedStream; - } else if (Platform.isIOS) { - _purchaseUpdatedStream = - AppStoreConnection.instance.purchaseUpdatedStream; - } else { - throw UnsupportedError( - 'InAppPurchase plugin only works on Android and iOS.'); - } - return _purchaseUpdatedStream; - } - - /// Whether pending purchase is enabled. - /// - /// See also [enablePendingPurchases] for more on pending purchases. - static bool get enablePendingPurchase => _enablePendingPurchase; - static bool _enablePendingPurchase = false; - - /// Returns true if the payment platform is ready and available. - Future isAvailable(); - - /// 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. - /// - /// It is an no-op on iOS. - static void enablePendingPurchases() { - _enablePendingPurchase = true; - } - - /// Query product details for the given set of IDs. - /// - /// The [identifiers] need to exactly match existing configured product - /// identifiers in the underlying payment platform, whether that's [App Store - /// Connect](https://appstoreconnect.apple.com/) or [Google Play - /// Console](https://play.google.com/). - /// - /// See the [example readme](../../../../example/README.md) for steps on how - /// to initialize products on both payment platforms. - Future queryProductDetails(Set identifiers); - - /// Buy a non consumable product or subscription. - /// - /// Non consumable items can only be bought once. For example, a purchase that - /// unlocks a special content in your app. Subscriptions are also non - /// consumable products. - /// - /// You always need to restore all the non consumable products for user when - /// they switch their phones. - /// - /// This method does not return the result of the purchase. Instead, after - /// triggering this method, purchase updates will be sent to - /// [purchaseUpdatedStream]. You should [Stream.listen] to - /// [purchaseUpdatedStream] to get [PurchaseDetails] objects in different - /// [PurchaseDetails.status] and update your UI accordingly. When the - /// [PurchaseDetails.status] is [PurchaseStatus.purchased] or - /// [PurchaseStatus.error], you should deliver the content or handle the - /// error, then call [completePurchase] to finish the purchasing process. - /// - /// This method does return whether or not the purchase request was initially - /// sent successfully. - /// - /// Consumable items are defined differently by the different underlying - /// payment platforms, and there's no way to query for whether or not the - /// [ProductDetail] is a consumable at runtime. On iOS, products are defined - /// as non consumable items in the [App Store - /// Connect](https://appstoreconnect.apple.com/). [Google Play - /// Console](https://play.google.com/) products are considered consumable if - /// and when they are actively consumed manually. - /// - /// You can find more details on testing payments on iOS - /// [here](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Chapters/ShowUI.html#//apple_ref/doc/uid/TP40008267-CH3-SW11). - /// You can find more details on testing payments on Android - /// [here](https://developer.android.com/google/play/billing/billing_testing). - /// - /// See also: - /// - /// * [buyConsumable], for buying a consumable product. - /// * [queryPastPurchases], for restoring non consumable products. - /// - /// Calling this method for consumable items will cause unwanted behaviors! - Future buyNonConsumable({@required PurchaseParam purchaseParam}); - - /// Buy a consumable product. - /// - /// Consumable items can be "consumed" to mark that they've been used and then - /// bought additional times. For example, a health potion. - /// - /// To restore consumable purchases across devices, you should keep track of - /// those purchase on your own server and restore the purchase for your users. - /// Consumed products are no longer considered to be "owned" by payment - /// platforms and will not be delivered by calling [queryPastPurchases]. - /// - /// Consumable items are defined differently by the different underlying - /// payment platforms, and there's no way to query for whether or not the - /// [ProductDetail] is a consumable at runtime. On iOS, products are defined - /// as consumable items in the [App Store - /// Connect](https://appstoreconnect.apple.com/). [Google Play - /// Console](https://play.google.com/) products are considered consumable if - /// and when they are actively consumed manually. - /// - /// `autoConsume` is provided as a utility for Android only. It's meaningless - /// on iOS because the App Store automatically considers all potentially - /// consumable purchases "consumed" once the initial transaction is complete. - /// `autoConsume` is `true` by default, and we will call [consumePurchase] - /// after a successful purchase for you so that Google Play considers a - /// purchase consumed after the initial transaction, like iOS. If you'd like - /// to manually consume purchases in Play, you should set it to `false` and - /// manually call [consumePurchase] instead. Failing to consume a purchase - /// will cause user never be able to buy the same item again. Manually setting - /// this to `false` on iOS will throw an `Exception`. - /// - /// This method does not return the result of the purchase. Instead, after - /// triggering this method, purchase updates will be sent to - /// [purchaseUpdatedStream]. You should [Stream.listen] to - /// [purchaseUpdatedStream] to get [PurchaseDetails] objects in different - /// [PurchaseDetails.status] and update your UI accordingly. When the - /// [PurchaseDetails.status] is [PurchaseStatus.purchased] or - /// [PurchaseStatus.error], you should deliver the content or handle the - /// error, then call [completePurchase] to finish the purchasing process. - /// - /// This method does return whether or not the purchase request was initially - /// sent succesfully. - /// - /// See also: - /// - /// * [buyNonConsumable], for buying a non consumable product or - /// subscription. - /// * [queryPastPurchases], for restoring non consumable products. - /// * [consumePurchase], for manually consuming products on Android. - /// - /// Calling this method for non consumable items will cause unwanted - /// behaviors! - Future buyConsumable( - {@required PurchaseParam purchaseParam, bool autoConsume = true}); - - /// Mark that purchased content has been delivered to the - /// user. - /// - /// You are responsible for completing every [PurchaseDetails] whose - /// [PurchaseDetails.status] is [PurchaseStatus.purchased]. Additionally on iOS, - /// the purchase needs to be completed if the [PurchaseDetails.status] is [PurchaseStatus.error]. - /// Completing a [PurchaseStatus.pending] purchase will cause an exception. - /// For convenience, [PurchaseDetails.pendingCompletePurchase] indicates if a purchase is pending for completion. - /// - /// The method returns a [BillingResultWrapper] to indicate a detailed status of the complete process. - /// If the result contains [BillingResponse.error] or [BillingResponse.serviceUnavailable], the developer should try - /// to complete the purchase via this method again, or retry the [completePurchase] it at a later time. - /// If the result indicates other errors, there might be some issue with - /// the app's code. The developer is responsible to fix the issue. - /// - /// Warning! Failure to call this method and get a successful response within 3 days of the purchase will result a refund on Android. - /// The [consumePurchase] acts as an implicit [completePurchase] on Android. - /// - /// The optional parameter `developerPayload` only works on Android. - Future completePurchase(PurchaseDetails purchase, - {String developerPayload}); - - /// (Play only) Mark that the user has consumed a product. - /// - /// You are responsible for consuming all consumable purchases once they are - /// delivered. The user won't be able to buy the same product again until the - /// purchase of the product is consumed. - /// - /// The `developerPayload` can be specified to be associated with this consumption. - /// - /// This throws an [UnsupportedError] on iOS. - Future consumePurchase(PurchaseDetails purchase, - {String developerPayload}); - - /// Query all previous purchases. - /// - /// The `applicationUserName` should match whatever was sent in the initial - /// `PurchaseParam`, if anything. - /// - /// This does not return consumed products. If you want to restore unused - /// consumable products, you need to persist consumable product information - /// for your user on your own server. - /// - /// See also: - /// - /// * [refreshPurchaseVerificationData], for reloading failed - /// [PurchaseDetails.verificationData]. - Future queryPastPurchases( - {String applicationUserName}); - - /// (App Store only) retry loading purchase data after an initial failure. - /// - /// Throws an [UnsupportedError] on Android. - Future refreshPurchaseVerificationData(); - - /// The [InAppPurchaseConnection] implemented for this platform. - /// - /// Throws an [UnsupportedError] when accessed on a platform other than - /// Android or iOS. - static InAppPurchaseConnection get instance => _getOrCreateInstance(); - static InAppPurchaseConnection _instance; - - static InAppPurchaseConnection _getOrCreateInstance() { - if (_instance != null) { - return _instance; - } - - if (Platform.isAndroid) { - _instance = GooglePlayConnection.instance; - } else if (Platform.isIOS) { - _instance = AppStoreConnection.instance; - } else { - throw UnsupportedError( - 'InAppPurchase plugin only works on Android and iOS.'); - } - - return _instance; - } -} - -/// Which platform the request is on. -enum IAPSource { GooglePlay, AppStore } - -/// Captures an error from the underlying purchase platform. -/// -/// The error can happen during the purchase, restoring a purchase, or querying product. -/// Errors from restoring a purchase are not indicative of any errors during the original purchase. -/// See also: -/// * [ProductDetailsResponse] for error when querying product details. -/// * [PurchaseDetails] for error happened in purchase. -class IAPError { - IAPError( - {@required this.source, - @required this.code, - @required this.message, - this.details}); - - /// Which source is the error on. - final IAPSource source; - - /// The error code. - final String code; - - /// A human-readable error message, possibly null. - final String message; - - /// Error details, possibly null. - final dynamic details; -} diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/product_details.dart b/packages/in_app_purchase/lib/src/in_app_purchase/product_details.dart deleted file mode 100644 index 9808bba999fe..000000000000 --- a/packages/in_app_purchase/lib/src/in_app_purchase/product_details.dart +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/foundation.dart'; -import 'package:in_app_purchase/store_kit_wrappers.dart'; -import 'package:in_app_purchase/billing_client_wrappers.dart'; -import 'in_app_purchase_connection.dart'; - -/// The class represents the information of a product. -/// -/// This class unifies the BillingClient's [SkuDetailsWrapper] and StoreKit's [SKProductWrapper]. You can use the common attributes in -/// This class for simple operations. If you would like to see the detailed representation of the product, instead, use [skuDetails] on Android and [skProduct] on iOS. -class ProductDetails { - ProductDetails( - {@required this.id, - @required this.title, - @required this.description, - @required this.price, - this.skProduct, - this.skuDetail}); - - /// The identifier of the product, specified in App Store Connect or Sku in Google Play console. - final String id; - - /// The title of the product, specified in the App Store Connect or Sku in Google Play console based on the platform. - final String title; - - /// The description of the product, specified in the App Store Connect or Sku in Google Play console based on the platform. - final String description; - - /// The price of the product, specified in the App Store Connect or Sku in Google Play console based on the platform. - /// Formatted with currency symbol ("$0.99"). - final String price; - - /// Points back to the `StoreKits`'s [SKProductWrapper] object that generated this [ProductDetails] object. - /// - /// This is null on Android. - final SKProductWrapper skProduct; - - /// Points back to the `BillingClient1`'s [SkuDetailsWrapper] object that generated this [ProductDetails] object. - /// - /// This is null on iOS. - final SkuDetailsWrapper skuDetail; - - /// Generate a [ProductDetails] object based on an iOS [SKProductWrapper] object. - ProductDetails.fromSKProduct(SKProductWrapper product) - : this.id = product.productIdentifier, - this.title = product.localizedTitle, - this.description = product.localizedDescription, - this.price = product.priceLocale.currencySymbol + product.price, - this.skProduct = product, - this.skuDetail = null; - - /// Generate a [ProductDetails] object based on an Android [SkuDetailsWrapper] object. - ProductDetails.fromSkuDetails(SkuDetailsWrapper skuDetails) - : this.id = skuDetails.sku, - this.title = skuDetails.title, - this.description = skuDetails.description, - this.price = skuDetails.price, - this.skProduct = null, - this.skuDetail = skuDetails; -} - -/// The response returned by [InAppPurchaseConnection.queryProductDetails]. -/// -/// A list of [ProductDetails] can be obtained from the this response. -class ProductDetailsResponse { - ProductDetailsResponse( - {@required this.productDetails, @required this.notFoundIDs, this.error}); - - /// Each [ProductDetails] uniquely matches one valid identifier in [identifiers] of [InAppPurchaseConnection.queryProductDetails]. - final List productDetails; - - /// The list of identifiers that are in the `identifiers` of [InAppPurchaseConnection.queryProductDetails] but failed to be fetched. - /// - /// There's multiple platform specific reasons that product information could fail to be fetched, - /// ranging from products not being correctly configured in the storefront to the queried IDs not existing. - final List notFoundIDs; - - /// A caught platform exception thrown while querying the purchases. - /// - /// It's possible for this to be null but for there still to be notFoundIds in cases where the request itself was a success but the - /// requested IDs could not be found. - final IAPError error; -} diff --git a/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart b/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart deleted file mode 100644 index 2321bd00abdc..000000000000 --- a/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart +++ /dev/null @@ -1,249 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/foundation.dart'; -import 'package:in_app_purchase/src/billing_client_wrappers/enum_converters.dart'; -import 'package:in_app_purchase/src/billing_client_wrappers/purchase_wrapper.dart'; -import 'package:in_app_purchase/src/store_kit_wrappers/enum_converters.dart'; -import 'package:in_app_purchase/src/store_kit_wrappers/sk_payment_transaction_wrappers.dart'; -import './in_app_purchase_connection.dart'; -import './product_details.dart'; - -final String kPurchaseErrorCode = 'purchase_error'; -final String kRestoredPurchaseErrorCode = 'restore_transactions_failed'; -final String kConsumptionFailedErrorCode = 'consume_purchase_failed'; -final String _kPlatformIOS = 'ios'; -final String _kPlatformAndroid = 'android'; - -/// Represents the data that is used to verify purchases. -/// -/// The property [source] helps you to determine the method to verify purchases. -/// Different source of purchase has different methods of verifying purchases. -/// -/// Both platforms have 2 ways to verify purchase data. You can either choose to verify the data locally using [localVerificationData] -/// or verify the data using your own server with [serverVerificationData]. -/// -/// For details on how to verify your purchase on iOS, -/// you can refer to Apple's document about [`About Receipt Validation`](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573-CH105-SW1). -/// -/// On Android, all purchase information should also be verified manually. See [`Verify a purchase`](https://developer.android.com/google/play/billing/billing_library_overview#Verify). -/// -/// It is preferable to verify purchases using a server with [serverVerificationData]. -/// -/// If the platform is iOS, it is possible the data can be null or your validation of this data turns out invalid. When this happens, -/// Call [InAppPurchaseConnection.refreshPurchaseVerificationData] to get a new [PurchaseVerificationData] object. And then you can -/// validate the receipt data again using one of the methods mentioned in [`Receipt Validation`](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Introduction.html#//apple_ref/doc/uid/TP40010573-CH105-SW1). -/// -/// You should never use any purchase data until verified. -class PurchaseVerificationData { - /// The data used for local verification. - /// - /// If the [source] is [IAPSource.AppStore], this data is a based64 encoded string. The structure of the payload is defined using ASN.1. - /// If the [source] is [IAPSource.GooglePlay], this data is a JSON String. - final String localVerificationData; - - /// The data used for server verification. - /// - /// If the platform is iOS, this data is identical to [localVerificationData]. - final String serverVerificationData; - - /// Indicates the source of the purchase. - final IAPSource source; - - PurchaseVerificationData( - {@required this.localVerificationData, - @required this.serverVerificationData, - @required this.source}); -} - -enum PurchaseStatus { - /// The purchase process is pending. - /// - /// You can update UI to let your users know the purchase is pending. - pending, - - /// The purchase is finished and successful. - /// - /// Update your UI to indicate the purchase is finished and deliver the product. - /// On Android, the google play store is handling the purchase, so we set the status to - /// `purchased` as long as we can successfully launch play store purchase flow. - purchased, - - /// Some error occurred in the purchase. The purchasing process if aborted. - error -} - -/// The parameter object for generating a purchase. -class PurchaseParam { - PurchaseParam( - {@required this.productDetails, - this.applicationUserName, - this.sandboxTesting}); - - /// The product to create payment for. - /// - /// It has to match one of the valid [ProductDetails] objects that you get from [ProductDetailsResponse] after calling [InAppPurchaseConnection.queryProductDetails]. - final ProductDetails productDetails; - - /// An opaque id for the user's account that's unique to your app. (Optional) - /// - /// Used to help the store detect irregular activity. - /// Do not pass in a clear text, your developer ID, the user’s Apple ID, or the - /// user's Google ID for this field. - /// For example, you can use a one-way hash of the user’s account name on your server. - final String applicationUserName; - - /// The 'sandboxTesting' is only available on iOS, set it to `true` for testing in AppStore's sandbox environment. The default value is `false`. - final bool sandboxTesting; -} - -/// Represents the transaction details of a purchase. -/// -/// This class unifies the BillingClient's [PurchaseWrapper] and StoreKit's [SKPaymentTransactionWrapper]. You can use the common attributes in -/// This class for simple operations. If you would like to see the detailed representation of the product, instead, use [PurchaseWrapper] on Android and [SKPaymentTransactionWrapper] on iOS. -class PurchaseDetails { - /// A unique identifier of the purchase. - final String purchaseID; - - /// The product identifier of the purchase. - final String productID; - - /// The verification data of the purchase. - /// - /// Use this to verify the purchase. See [PurchaseVerificationData] for - /// details on how to verify purchase use this data. You should never use any - /// purchase data until verified. - /// - /// On iOS, this may be null. Call - /// [InAppPurchaseConnection.refreshPurchaseVerificationData] to get a new - /// [PurchaseVerificationData] object for further validation. - final PurchaseVerificationData verificationData; - - /// The timestamp of the transaction. - /// - /// Milliseconds since epoch. - final String transactionDate; - - /// The status that this [PurchaseDetails] is currently on. - PurchaseStatus get status => _status; - set status(PurchaseStatus status) { - if (_platform == _kPlatformIOS) { - if (status == PurchaseStatus.purchased || - status == PurchaseStatus.error) { - _pendingCompletePurchase = true; - } - } - if (_platform == _kPlatformAndroid) { - if (status == PurchaseStatus.purchased) { - _pendingCompletePurchase = true; - } - } - _status = status; - } - - PurchaseStatus _status; - - /// The error is only available when [status] is [PurchaseStatus.error]. - IAPError error; - - /// Points back to the `StoreKits`'s [SKPaymentTransactionWrapper] object that generated this [PurchaseDetails] object. - /// - /// This is null on Android. - final SKPaymentTransactionWrapper skPaymentTransaction; - - /// Points back to the `BillingClient`'s [PurchaseWrapper] object that generated this [PurchaseDetails] object. - /// - /// This is null on iOS. - final PurchaseWrapper billingClientPurchase; - - /// The developer has to call [InAppPurchaseConnection.completePurchase] if the value is `true` - /// and the product has been delivered to the user. - /// - /// The initial value is `false`. - /// * See also [InAppPurchaseConnection.completePurchase] for more details on completing purchases. - bool get pendingCompletePurchase => _pendingCompletePurchase; - bool _pendingCompletePurchase = false; - - // The platform that the object is created on. - // - // The value is either '_kPlatformIOS' or '_kPlatformAndroid'. - String _platform; - - PurchaseDetails({ - @required this.purchaseID, - @required this.productID, - @required this.verificationData, - @required this.transactionDate, - this.skPaymentTransaction, - this.billingClientPurchase, - }); - - /// Generate a [PurchaseDetails] object based on an iOS [SKTransactionWrapper] object. - PurchaseDetails.fromSKTransaction( - SKPaymentTransactionWrapper transaction, String base64EncodedReceipt) - : this.purchaseID = transaction.transactionIdentifier, - this.productID = transaction.payment.productIdentifier, - this.verificationData = PurchaseVerificationData( - localVerificationData: base64EncodedReceipt, - serverVerificationData: base64EncodedReceipt, - source: IAPSource.AppStore), - this.transactionDate = transaction.transactionTimeStamp != null - ? (transaction.transactionTimeStamp * 1000).toInt().toString() - : null, - this.skPaymentTransaction = transaction, - this.billingClientPurchase = null, - _platform = _kPlatformIOS { - status = SKTransactionStatusConverter() - .toPurchaseStatus(transaction.transactionState); - if (status == PurchaseStatus.error) { - error = IAPError( - source: IAPSource.AppStore, - code: kPurchaseErrorCode, - message: transaction.error.domain, - details: transaction.error.userInfo, - ); - } - } - - /// Generate a [PurchaseDetails] object based on an Android [Purchase] object. - PurchaseDetails.fromPurchase(PurchaseWrapper purchase) - : this.purchaseID = purchase.orderId, - this.productID = purchase.sku, - this.verificationData = PurchaseVerificationData( - localVerificationData: purchase.originalJson, - serverVerificationData: purchase.purchaseToken, - source: IAPSource.GooglePlay), - this.transactionDate = purchase.purchaseTime.toString(), - this.skPaymentTransaction = null, - this.billingClientPurchase = purchase, - _platform = _kPlatformAndroid { - status = PurchaseStateConverter().toPurchaseStatus(purchase.purchaseState); - if (status == PurchaseStatus.error) { - error = IAPError( - source: IAPSource.GooglePlay, - code: kPurchaseErrorCode, - message: null, - ); - } - } -} - -/// The response object for fetching the past purchases. -/// -/// An instance of this class is returned in [InAppPurchaseConnection.queryPastPurchases]. -class QueryPurchaseDetailsResponse { - QueryPurchaseDetailsResponse({@required this.pastPurchases, this.error}); - - /// A list of successfully fetched past purchases. - /// - /// If there are no past purchases, or there is an [error] fetching past purchases, - /// this variable is an empty List. - /// You should verify the purchase data using [PurchaseDetails.verificationData] before using the [PurchaseDetails] object. - final List pastPurchases; - - /// The error when fetching past purchases. - /// - /// If the fetch is successful, the value is null. - final IAPError error; -} diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/enum_converters.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/enum_converters.dart deleted file mode 100644 index 49cfb78a686b..000000000000 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/enum_converters.dart +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:in_app_purchase/store_kit_wrappers.dart'; -import 'package:json_annotation/json_annotation.dart'; -import 'package:in_app_purchase/in_app_purchase.dart'; - -part 'enum_converters.g.dart'; - -/// Serializer for [SKPaymentTransactionStateWrapper]. -/// -/// Use these in `@JsonSerializable()` classes by annotating them with -/// `@SKTransactionStatusConverter()`. -class SKTransactionStatusConverter - implements JsonConverter { - const SKTransactionStatusConverter(); - - @override - SKPaymentTransactionStateWrapper fromJson(int json) => - _$enumDecode( - _$SKPaymentTransactionStateWrapperEnumMap - .cast(), - json); - - PurchaseStatus toPurchaseStatus(SKPaymentTransactionStateWrapper object) { - switch (object) { - case SKPaymentTransactionStateWrapper.purchasing: - case SKPaymentTransactionStateWrapper.deferred: - return PurchaseStatus.pending; - case SKPaymentTransactionStateWrapper.purchased: - case SKPaymentTransactionStateWrapper.restored: - return PurchaseStatus.purchased; - case SKPaymentTransactionStateWrapper.failed: - return PurchaseStatus.error; - } - - throw ArgumentError('$object isn\'t mapped to PurchaseStatus'); - } - - @override - int toJson(SKPaymentTransactionStateWrapper object) => - _$SKPaymentTransactionStateWrapperEnumMap[object]; -} - -// Define a class so we generate serializer helper methods for the enums -@JsonSerializable() -class _SerializedEnums { - SKPaymentTransactionStateWrapper response; -} diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/enum_converters.g.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/enum_converters.g.dart deleted file mode 100644 index f4f17df846a7..000000000000 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/enum_converters.g.dart +++ /dev/null @@ -1,47 +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']); -} - -Map _$_SerializedEnumsToJson(_SerializedEnums instance) => - { - 'response': _$SKPaymentTransactionStateWrapperEnumMap[instance.response], - }; - -T _$enumDecode( - Map enumValues, - dynamic source, { - T unknownValue, -}) { - if (source == null) { - throw ArgumentError('A value must be provided. Supported values: ' - '${enumValues.values.join(', ')}'); - } - - final value = enumValues.entries - .singleWhere((e) => e.value == source, orElse: () => null) - ?.key; - - if (value == null && unknownValue == null) { - throw ArgumentError('`$source` is not one of the supported values: ' - '${enumValues.values.join(', ')}'); - } - return value ?? unknownValue; -} - -const _$SKPaymentTransactionStateWrapperEnumMap = { - SKPaymentTransactionStateWrapper.purchasing: 0, - SKPaymentTransactionStateWrapper.purchased: 1, - SKPaymentTransactionStateWrapper.failed: 2, - SKPaymentTransactionStateWrapper.restored: 3, - SKPaymentTransactionStateWrapper.deferred: 4, -}; diff --git a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart deleted file mode 100644 index 48a18e61d4d9..000000000000 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart +++ /dev/null @@ -1,42 +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, - 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, - simulatesAskToBuyInSandbox: json['simulatesAskToBuyInSandbox'] as bool, - ); -} - -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/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_transaction_wrappers.g.dart deleted file mode 100644 index bc520826d9fe..000000000000 --- a/packages/in_app_purchase/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: json['payment'] == null - ? null - : SKPaymentWrapper.fromJson(json['payment'] as Map), - transactionState: const SKTransactionStatusConverter() - .fromJson(json['transactionState'] as int), - originalTransaction: json['originalTransaction'] == null - ? null - : SKPaymentTransactionWrapper.fromJson( - json['originalTransaction'] as Map), - transactionTimeStamp: (json['transactionTimeStamp'] as num)?.toDouble(), - transactionIdentifier: json['transactionIdentifier'] as String, - error: - json['error'] == null ? null : SKError.fromJson(json['error'] as Map), - ); -} - -Map _$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/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart deleted file mode 100644 index cf27852263ba..000000000000 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart +++ /dev/null @@ -1,158 +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(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, - unit: _$enumDecodeNullable(_$SKSubscriptionPeriodUnitEnumMap, json['unit']), - ); -} - -Map _$SKProductSubscriptionPeriodWrapperToJson( - SKProductSubscriptionPeriodWrapper instance) => - { - 'numberOfUnits': instance.numberOfUnits, - 'unit': _$SKSubscriptionPeriodUnitEnumMap[instance.unit], - }; - -T _$enumDecode( - Map enumValues, - dynamic source, { - T unknownValue, -}) { - if (source == null) { - throw ArgumentError('A value must be provided. Supported values: ' - '${enumValues.values.join(', ')}'); - } - - final value = enumValues.entries - .singleWhere((e) => e.value == source, orElse: () => null) - ?.key; - - if (value == null && unknownValue == null) { - throw ArgumentError('`$source` is not one of the supported values: ' - '${enumValues.values.join(', ')}'); - } - return value ?? unknownValue; -} - -T _$enumDecodeNullable( - Map enumValues, - dynamic source, { - T unknownValue, -}) { - if (source == null) { - return null; - } - return _$enumDecode(enumValues, source, unknownValue: unknownValue); -} - -const _$SKSubscriptionPeriodUnitEnumMap = { - SKSubscriptionPeriodUnit.day: 0, - SKSubscriptionPeriodUnit.week: 1, - SKSubscriptionPeriodUnit.month: 2, - SKSubscriptionPeriodUnit.year: 3, -}; - -SKProductDiscountWrapper _$SKProductDiscountWrapperFromJson(Map json) { - return SKProductDiscountWrapper( - price: json['price'] as String, - priceLocale: json['priceLocale'] == null - ? null - : SKPriceLocaleWrapper.fromJson(json['priceLocale'] as Map), - numberOfPeriods: json['numberOfPeriods'] as int, - paymentMode: _$enumDecodeNullable( - _$SKProductDiscountPaymentModeEnumMap, json['paymentMode']), - subscriptionPeriod: json['subscriptionPeriod'] == null - ? null - : SKProductSubscriptionPeriodWrapper.fromJson( - json['subscriptionPeriod'] as Map), - ); -} - -Map _$SKProductDiscountWrapperToJson( - SKProductDiscountWrapper instance) => - { - 'price': instance.price, - 'priceLocale': instance.priceLocale, - 'numberOfPeriods': instance.numberOfPeriods, - 'paymentMode': - _$SKProductDiscountPaymentModeEnumMap[instance.paymentMode], - 'subscriptionPeriod': instance.subscriptionPeriod, - }; - -const _$SKProductDiscountPaymentModeEnumMap = { - SKProductDiscountPaymentMode.payAsYouGo: 0, - SKProductDiscountPaymentMode.payUpFront: 1, - SKProductDiscountPaymentMode.freeTrail: 2, -}; - -SKProductWrapper _$SKProductWrapperFromJson(Map json) { - return SKProductWrapper( - productIdentifier: json['productIdentifier'] as String, - localizedTitle: json['localizedTitle'] as String, - localizedDescription: json['localizedDescription'] as String, - priceLocale: json['priceLocale'] == null - ? null - : SKPriceLocaleWrapper.fromJson(json['priceLocale'] as Map), - subscriptionGroupIdentifier: json['subscriptionGroupIdentifier'] as String, - price: json['price'] as String, - subscriptionPeriod: json['subscriptionPeriod'] == null - ? null - : SKProductSubscriptionPeriodWrapper.fromJson( - json['subscriptionPeriod'] as Map), - introductoryPrice: json['introductoryPrice'] == null - ? null - : SKProductDiscountWrapper.fromJson(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, - ); -} - -Map _$SKPriceLocaleWrapperToJson( - SKPriceLocaleWrapper instance) => - { - 'currencySymbol': instance.currencySymbol, - 'currencyCode': instance.currencyCode, - }; diff --git a/packages/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/pubspec.yaml deleted file mode 100644 index 6b1ddf9d08f8..000000000000 --- a/packages/in_app_purchase/pubspec.yaml +++ /dev/null @@ -1,40 +0,0 @@ -name: in_app_purchase -description: A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store and Google Play. -homepage: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase -version: 0.3.4+10 - -dependencies: - async: ^2.0.8 - collection: ^1.14.11 - flutter: - sdk: flutter - json_annotation: ^3.0.0 - meta: ^1.1.6 - -dev_dependencies: - build_runner: ^1.0.0 - json_serializable: ^3.2.0 - flutter_test: - sdk: flutter - flutter_driver: - sdk: flutter - in_app_purchase_example: - path: example/ - test: ^1.5.2 - shared_preferences: ^0.5.2 - integration_test: - path: ../integration_test - pedantic: ^1.8.0 - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.inapppurchase - pluginClass: InAppPurchasePlugin - ios: - pluginClass: InAppPurchasePlugin - -environment: - sdk: ">=2.3.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" diff --git a/packages/in_app_purchase/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/test/billing_client_wrappers/billing_client_wrapper_test.dart deleted file mode 100644 index 54f7c3eda77f..000000000000 --- a/packages/in_app_purchase/test/billing_client_wrappers/billing_client_wrapper_test.dart +++ /dev/null @@ -1,328 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter/services.dart'; - -import 'package:in_app_purchase/billing_client_wrappers.dart'; -import 'package:in_app_purchase/src/billing_client_wrappers/enum_converters.dart'; -import 'package:in_app_purchase/src/channel.dart'; -import '../stub_in_app_purchase_platform.dart'; -import 'sku_details_wrapper_test.dart'; -import 'purchase_wrapper_test.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); - BillingClient billingClient; - - setUpAll(() => - channel.setMockMethodCallHandler(stubPlatform.fakeMethodCallHandler)); - - setUp(() { - billingClient = BillingClient((PurchasesResultWrapper _) {}); - billingClient.enablePendingPurchases(); - stubPlatform.reset(); - }); - - group('isReady', () { - test('true', () async { - stubPlatform.addResponse(name: 'BillingClient#isReady()', value: true); - expect(await billingClient.isReady(), isTrue); - }); - - test('false', () async { - stubPlatform.addResponse(name: 'BillingClient#isReady()', value: false); - expect(await billingClient.isReady(), isFalse); - }); - }); - - group('startConnection', () { - final String methodName = - 'BillingClient#startConnection(BillingClientStateListener)'; - test('returns BillingResultWrapper', () async { - const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.developerError; - stubPlatform.addResponse( - name: methodName, - value: { - 'responseCode': BillingResponseConverter().toJson(responseCode), - 'debugMessage': debugMessage, - }, - ); - - BillingResultWrapper billingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - expect( - await billingClient.startConnection( - onBillingServiceDisconnected: () {}), - equals(billingResult)); - }); - - test('passes handle to onBillingServiceDisconnected', () async { - const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.developerError; - stubPlatform.addResponse( - name: methodName, - value: { - 'responseCode': BillingResponseConverter().toJson(responseCode), - 'debugMessage': debugMessage, - }, - ); - await billingClient.startConnection(onBillingServiceDisconnected: () {}); - final MethodCall call = stubPlatform.previousCallMatching(methodName); - expect( - call.arguments, - equals( - {'handle': 0, 'enablePendingPurchases': true})); - }); - }); - - test('endConnection', () async { - final String endConnectionName = 'BillingClient#endConnection()'; - expect(stubPlatform.countPreviousCalls(endConnectionName), equals(0)); - stubPlatform.addResponse(name: endConnectionName, value: null); - await billingClient.endConnection(); - expect(stubPlatform.countPreviousCalls(endConnectionName), equals(1)); - }); - - group('querySkuDetails', () { - final String queryMethodName = - 'BillingClient#querySkuDetailsAsync(SkuDetailsParams, SkuDetailsResponseListener)'; - - test('handles empty skuDetails', () async { - const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.developerError; - stubPlatform.addResponse(name: queryMethodName, value: { - 'billingResult': { - 'responseCode': BillingResponseConverter().toJson(responseCode), - 'debugMessage': debugMessage, - }, - 'skuDetailsList': >[] - }); - - final SkuDetailsResponseWrapper response = await billingClient - .querySkuDetails( - skuType: SkuType.inapp, skusList: ['invalid']); - - BillingResultWrapper billingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - expect(response.billingResult, equals(billingResult)); - expect(response.skuDetailsList, isEmpty); - }); - - test('returns SkuDetailsResponseWrapper', () async { - const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.ok; - stubPlatform.addResponse(name: queryMethodName, value: { - 'billingResult': { - 'responseCode': BillingResponseConverter().toJson(responseCode), - 'debugMessage': debugMessage, - }, - 'skuDetailsList': >[buildSkuMap(dummySkuDetails)] - }); - - final SkuDetailsResponseWrapper response = await billingClient - .querySkuDetails( - skuType: SkuType.inapp, skusList: ['invalid']); - - BillingResultWrapper billingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - expect(response.billingResult, equals(billingResult)); - expect(response.skuDetailsList, contains(dummySkuDetails)); - }); - }); - - group('launchBillingFlow', () { - final 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( - responseCode: responseCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: launchMethodName, - value: buildBillingResultMap(expectedBillingResult), - ); - final SkuDetailsWrapper skuDetails = dummySkuDetails; - final String accountId = "hashedAccountId"; - - expect( - await billingClient.launchBillingFlow( - sku: skuDetails.sku, accountId: accountId), - equals(expectedBillingResult)); - Map arguments = - stubPlatform.previousCallMatching(launchMethodName).arguments; - expect(arguments['sku'], equals(skuDetails.sku)); - expect(arguments['accountId'], equals(accountId)); - }); - - test('handles null accountId', () async { - const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: launchMethodName, - value: buildBillingResultMap(expectedBillingResult), - ); - final SkuDetailsWrapper skuDetails = dummySkuDetails; - - expect(await billingClient.launchBillingFlow(sku: skuDetails.sku), - equals(expectedBillingResult)); - Map arguments = - stubPlatform.previousCallMatching(launchMethodName).arguments; - expect(arguments['sku'], equals(skuDetails.sku)); - expect(arguments['accountId'], isNull); - }); - }); - - group('queryPurchases', () { - const String queryPurchasesMethodName = - 'BillingClient#queryPurchases(String)'; - - test('serializes and deserializes data', () async { - final BillingResponse expectedCode = BillingResponse.ok; - final List expectedList = [ - dummyPurchase - ]; - const String debugMessage = 'dummy message'; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: expectedCode, debugMessage: debugMessage); - stubPlatform - .addResponse(name: queryPurchasesMethodName, value: { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': BillingResponseConverter().toJson(expectedCode), - 'purchasesList': expectedList - .map((PurchaseWrapper purchase) => buildPurchaseMap(purchase)) - .toList(), - }); - - final PurchasesResultWrapper response = - await billingClient.queryPurchases(SkuType.inapp); - - expect(response.billingResult, equals(expectedBillingResult)); - expect(response.responseCode, equals(expectedCode)); - expect(response.purchasesList, equals(expectedList)); - }); - - test('checks for null params', () async { - expect(() => billingClient.queryPurchases(null), throwsAssertionError); - }); - - test('handles empty purchases', () async { - final BillingResponse expectedCode = BillingResponse.userCanceled; - const String debugMessage = 'dummy message'; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: expectedCode, debugMessage: debugMessage); - stubPlatform - .addResponse(name: queryPurchasesMethodName, value: { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': BillingResponseConverter().toJson(expectedCode), - 'purchasesList': [], - }); - - final PurchasesResultWrapper response = - await billingClient.queryPurchases(SkuType.inapp); - - expect(response.billingResult, equals(expectedBillingResult)); - expect(response.responseCode, equals(expectedCode)); - expect(response.purchasesList, isEmpty); - }); - }); - - group('queryPurchaseHistory', () { - const String queryPurchaseHistoryMethodName = - 'BillingClient#queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)'; - - test('serializes and deserializes data', () async { - final BillingResponse expectedCode = BillingResponse.ok; - final List expectedList = - [ - dummyPurchaseHistoryRecord, - ]; - const String debugMessage = 'dummy message'; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: expectedCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: queryPurchaseHistoryMethodName, - value: { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'purchaseHistoryRecordList': expectedList - .map((PurchaseHistoryRecordWrapper purchaseHistoryRecord) => - buildPurchaseHistoryRecordMap(purchaseHistoryRecord)) - .toList(), - }); - - final PurchasesHistoryResult response = - await billingClient.queryPurchaseHistory(SkuType.inapp); - expect(response.billingResult, equals(expectedBillingResult)); - expect(response.purchaseHistoryRecordList, equals(expectedList)); - }); - - test('checks for null params', () async { - expect( - () => billingClient.queryPurchaseHistory(null), throwsAssertionError); - }); - - test('handles empty purchases', () async { - final BillingResponse expectedCode = BillingResponse.userCanceled; - const String debugMessage = 'dummy message'; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: expectedCode, debugMessage: debugMessage); - stubPlatform.addResponse(name: queryPurchaseHistoryMethodName, value: { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'purchaseHistoryRecordList': [], - }); - - final PurchasesHistoryResult response = - await billingClient.queryPurchaseHistory(SkuType.inapp); - - expect(response.billingResult, equals(expectedBillingResult)); - expect(response.purchaseHistoryRecordList, isEmpty); - }); - }); - - group('consume purchases', () { - const String consumeMethodName = - 'BillingClient#consumeAsync(String, ConsumeResponseListener)'; - test('consume purchase async success', () async { - final BillingResponse expectedCode = BillingResponse.ok; - const String debugMessage = 'dummy message'; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: expectedCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: consumeMethodName, - value: buildBillingResultMap(expectedBillingResult)); - - final BillingResultWrapper billingResult = await billingClient - .consumeAsync('dummy token', developerPayload: 'dummy payload'); - - expect(billingResult, equals(expectedBillingResult)); - }); - }); - - group('acknowledge purchases', () { - const String acknowledgeMethodName = - 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)'; - test('acknowledge purchase success', () async { - final BillingResponse expectedCode = BillingResponse.ok; - const String debugMessage = 'dummy message'; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: expectedCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: acknowledgeMethodName, - value: buildBillingResultMap(expectedBillingResult)); - - final BillingResultWrapper billingResult = - await billingClient.acknowledgePurchase('dummy token', - developerPayload: 'dummy payload'); - - expect(billingResult, equals(expectedBillingResult)); - }); - }); -} diff --git a/packages/in_app_purchase/test/billing_client_wrappers/purchase_wrapper_test.dart b/packages/in_app_purchase/test/billing_client_wrappers/purchase_wrapper_test.dart deleted file mode 100644 index 978252a3d118..000000000000 --- a/packages/in_app_purchase/test/billing_client_wrappers/purchase_wrapper_test.dart +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -import 'package:in_app_purchase/src/in_app_purchase/purchase_details.dart'; -import 'package:test/test.dart'; -import 'package:in_app_purchase/billing_client_wrappers.dart'; -import 'package:in_app_purchase/src/billing_client_wrappers/enum_converters.dart'; -import 'package:in_app_purchase/src/in_app_purchase/in_app_purchase_connection.dart'; - -final PurchaseWrapper dummyPurchase = PurchaseWrapper( - orderId: 'orderId', - packageName: 'packageName', - purchaseTime: 0, - signature: 'signature', - sku: 'sku', - purchaseToken: 'purchaseToken', - isAutoRenewing: false, - originalJson: '', - developerPayload: 'dummy payload', - isAcknowledged: true, - purchaseState: PurchaseStateWrapper.purchased, -); - -final PurchaseWrapper dummyUnacknowledgedPurchase = PurchaseWrapper( - orderId: 'orderId', - packageName: 'packageName', - purchaseTime: 0, - signature: 'signature', - sku: 'sku', - purchaseToken: 'purchaseToken', - isAutoRenewing: false, - originalJson: '', - developerPayload: 'dummy payload', - isAcknowledged: false, - purchaseState: PurchaseStateWrapper.purchased, -); - -final PurchaseHistoryRecordWrapper dummyPurchaseHistoryRecord = - PurchaseHistoryRecordWrapper( - purchaseTime: 0, - signature: 'signature', - sku: 'sku', - purchaseToken: 'purchaseToken', - originalJson: '', - developerPayload: 'dummy payload', -); - -void main() { - group('PurchaseWrapper', () { - test('converts from map', () { - final PurchaseWrapper expected = dummyPurchase; - final PurchaseWrapper parsed = - PurchaseWrapper.fromJson(buildPurchaseMap(expected)); - - expect(parsed, equals(expected)); - }); - - test('toPurchaseDetails() should return correct PurchaseDetail object', () { - final PurchaseDetails details = - PurchaseDetails.fromPurchase(dummyPurchase); - expect(details.purchaseID, dummyPurchase.orderId); - expect(details.productID, dummyPurchase.sku); - expect(details.transactionDate, dummyPurchase.purchaseTime.toString()); - expect(details.verificationData.source, IAPSource.GooglePlay); - expect(details.verificationData.localVerificationData, - dummyPurchase.originalJson); - expect(details.verificationData.serverVerificationData, - dummyPurchase.purchaseToken); - expect(details.skPaymentTransaction, null); - expect(details.billingClientPurchase, dummyPurchase); - expect(details.pendingCompletePurchase, true); - }); - }); - - group('PurchaseHistoryRecordWrapper', () { - test('converts from map', () { - final PurchaseHistoryRecordWrapper expected = dummyPurchaseHistoryRecord; - final PurchaseHistoryRecordWrapper parsed = - PurchaseHistoryRecordWrapper.fromJson( - buildPurchaseHistoryRecordMap(expected)); - - expect(parsed, equals(expected)); - }); - }); - - group('PurchasesResultWrapper', () { - test('parsed from map', () { - final BillingResponse responseCode = BillingResponse.ok; - final List purchases = [ - dummyPurchase, - dummyPurchase - ]; - const String debugMessage = 'dummy Message'; - final BillingResultWrapper billingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - final PurchasesResultWrapper expected = PurchasesResultWrapper( - billingResult: billingResult, - responseCode: responseCode, - purchasesList: purchases); - final PurchasesResultWrapper parsed = - PurchasesResultWrapper.fromJson({ - 'billingResult': buildBillingResultMap(billingResult), - 'responseCode': BillingResponseConverter().toJson(responseCode), - 'purchasesList': >[ - buildPurchaseMap(dummyPurchase), - buildPurchaseMap(dummyPurchase) - ] - }); - expect(parsed.billingResult, equals(expected.billingResult)); - expect(parsed.responseCode, equals(expected.responseCode)); - expect(parsed.purchasesList, containsAll(expected.purchasesList)); - }); - }); - - group('PurchasesHistoryResult', () { - test('parsed from map', () { - final BillingResponse responseCode = BillingResponse.ok; - final List purchaseHistoryRecordList = - [ - dummyPurchaseHistoryRecord, - dummyPurchaseHistoryRecord - ]; - const String debugMessage = 'dummy Message'; - final BillingResultWrapper billingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - final PurchasesHistoryResult expected = PurchasesHistoryResult( - billingResult: billingResult, - purchaseHistoryRecordList: purchaseHistoryRecordList); - final PurchasesHistoryResult parsed = - PurchasesHistoryResult.fromJson({ - 'billingResult': buildBillingResultMap(billingResult), - 'purchaseHistoryRecordList': >[ - buildPurchaseHistoryRecordMap(dummyPurchaseHistoryRecord), - buildPurchaseHistoryRecordMap(dummyPurchaseHistoryRecord) - ] - }); - expect(parsed.billingResult, equals(billingResult)); - expect(parsed.purchaseHistoryRecordList, - containsAll(expected.purchaseHistoryRecordList)); - }); - }); -} - -Map buildPurchaseMap(PurchaseWrapper original) { - return { - 'orderId': original.orderId, - 'packageName': original.packageName, - 'purchaseTime': original.purchaseTime, - 'signature': original.signature, - 'sku': original.sku, - 'purchaseToken': original.purchaseToken, - 'isAutoRenewing': original.isAutoRenewing, - 'originalJson': original.originalJson, - 'developerPayload': original.developerPayload, - 'purchaseState': PurchaseStateConverter().toJson(original.purchaseState), - 'isAcknowledged': original.isAcknowledged, - }; -} - -Map buildPurchaseHistoryRecordMap( - PurchaseHistoryRecordWrapper original) { - return { - 'purchaseTime': original.purchaseTime, - 'signature': original.signature, - 'sku': original.sku, - 'purchaseToken': original.purchaseToken, - 'originalJson': original.originalJson, - 'developerPayload': original.developerPayload, - }; -} - -Map buildBillingResultMap(BillingResultWrapper original) { - return { - 'responseCode': BillingResponseConverter().toJson(original.responseCode), - 'debugMessage': original.debugMessage, - }; -} diff --git a/packages/in_app_purchase/test/billing_client_wrappers/sku_details_wrapper_test.dart b/packages/in_app_purchase/test/billing_client_wrappers/sku_details_wrapper_test.dart deleted file mode 100644 index c305e6df88cc..000000000000 --- a/packages/in_app_purchase/test/billing_client_wrappers/sku_details_wrapper_test.dart +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -import 'package:test/test.dart'; -import 'package:in_app_purchase/billing_client_wrappers.dart'; -import 'package:in_app_purchase/src/in_app_purchase/product_details.dart'; -import 'package:in_app_purchase/src/billing_client_wrappers/enum_converters.dart'; - -final SkuDetailsWrapper dummySkuDetails = SkuDetailsWrapper( - description: 'description', - freeTrialPeriod: 'freeTrialPeriod', - introductoryPrice: 'introductoryPrice', - introductoryPriceMicros: 'introductoryPriceMicros', - introductoryPriceCycles: 'introductoryPriceCycles', - introductoryPricePeriod: 'introductoryPricePeriod', - price: 'price', - priceAmountMicros: 1000, - priceCurrencyCode: 'priceCurrencyCode', - sku: 'sku', - subscriptionPeriod: 'subscriptionPeriod', - title: 'title', - type: SkuType.inapp, - isRewarded: true, - originalPrice: 'originalPrice', - originalPriceAmountMicros: 1000, -); - -void main() { - group('SkuDetailsWrapper', () { - test('converts from map', () { - final SkuDetailsWrapper expected = dummySkuDetails; - final SkuDetailsWrapper parsed = - SkuDetailsWrapper.fromJson(buildSkuMap(expected)); - - expect(parsed, equals(expected)); - }); - }); - - group('SkuDetailsResponseWrapper', () { - test('parsed from map', () { - final BillingResponse responseCode = BillingResponse.ok; - const String debugMessage = 'dummy message'; - final List skusDetails = [ - dummySkuDetails, - dummySkuDetails - ]; - BillingResultWrapper result = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - final SkuDetailsResponseWrapper expected = SkuDetailsResponseWrapper( - billingResult: result, skuDetailsList: skusDetails); - - final SkuDetailsResponseWrapper parsed = - SkuDetailsResponseWrapper.fromJson({ - 'billingResult': { - 'responseCode': BillingResponseConverter().toJson(responseCode), - 'debugMessage': debugMessage, - }, - 'skuDetailsList': >[ - buildSkuMap(dummySkuDetails), - buildSkuMap(dummySkuDetails) - ] - }); - - expect(parsed.billingResult, equals(expected.billingResult)); - expect(parsed.skuDetailsList, containsAll(expected.skuDetailsList)); - }); - - test('toProductDetails() should return correct Product object', () { - final SkuDetailsWrapper wrapper = - SkuDetailsWrapper.fromJson(buildSkuMap(dummySkuDetails)); - final ProductDetails product = ProductDetails.fromSkuDetails(wrapper); - expect(product.title, wrapper.title); - expect(product.description, wrapper.description); - expect(product.id, wrapper.sku); - expect(product.price, wrapper.price); - expect(product.skuDetail, wrapper); - expect(product.skProduct, null); - }); - - test('handles empty list of skuDetails', () { - final BillingResponse responseCode = BillingResponse.error; - const String debugMessage = 'dummy message'; - final List skusDetails = []; - BillingResultWrapper billingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - final SkuDetailsResponseWrapper expected = SkuDetailsResponseWrapper( - billingResult: billingResult, skuDetailsList: skusDetails); - - final SkuDetailsResponseWrapper parsed = - SkuDetailsResponseWrapper.fromJson({ - 'billingResult': { - 'responseCode': BillingResponseConverter().toJson(responseCode), - 'debugMessage': debugMessage, - }, - 'skuDetailsList': >[] - }); - - expect(parsed.billingResult, equals(expected.billingResult)); - expect(parsed.skuDetailsList, containsAll(expected.skuDetailsList)); - }); - }); -} - -Map buildSkuMap(SkuDetailsWrapper original) { - return { - 'description': original.description, - 'freeTrialPeriod': original.freeTrialPeriod, - 'introductoryPrice': original.introductoryPrice, - 'introductoryPriceMicros': original.introductoryPriceMicros, - 'introductoryPriceCycles': original.introductoryPriceCycles, - 'introductoryPricePeriod': original.introductoryPricePeriod, - 'price': original.price, - 'priceAmountMicros': original.priceAmountMicros, - 'priceCurrencyCode': original.priceCurrencyCode, - 'sku': original.sku, - 'subscriptionPeriod': original.subscriptionPeriod, - 'title': original.title, - 'type': original.type.toString().substring(8), - 'isRewarded': original.isRewarded, - 'originalPrice': original.originalPrice, - 'originalPriceAmountMicros': original.originalPriceAmountMicros, - }; -} diff --git a/packages/in_app_purchase/test/in_app_purchase_connection/app_store_connection_test.dart b/packages/in_app_purchase/test/in_app_purchase_connection/app_store_connection_test.dart deleted file mode 100644 index 881e1fcc75b7..000000000000 --- a/packages/in_app_purchase/test/in_app_purchase_connection/app_store_connection_test.dart +++ /dev/null @@ -1,461 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:io'; - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart' show TestWidgetsFlutterBinding; -import 'package:in_app_purchase/src/in_app_purchase/purchase_details.dart'; -import 'package:test/test.dart'; - -import 'package:in_app_purchase/src/channel.dart'; -import 'package:in_app_purchase/src/in_app_purchase/app_store_connection.dart'; -import 'package:in_app_purchase/src/in_app_purchase/in_app_purchase_connection.dart'; -import 'package:in_app_purchase/src/in_app_purchase/product_details.dart'; -import 'package:in_app_purchase/store_kit_wrappers.dart'; -import '../store_kit_wrappers/sk_test_stub_objects.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final FakeIOSPlatform fakeIOSPlatform = FakeIOSPlatform(); - - setUpAll(() { - SystemChannels.platform - .setMockMethodCallHandler(fakeIOSPlatform.onMethodCall); - }); - - setUp(() => fakeIOSPlatform.reset()); - - tearDown(() => fakeIOSPlatform.reset()); - - group('isAvailable', () { - test('true', () async { - expect(await AppStoreConnection.instance.isAvailable(), isTrue); - }); - }); - - group('query product list', () { - test('should get product list and correct invalid identifiers', () async { - final AppStoreConnection connection = AppStoreConnection(); - 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); - }); - - 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 AppStoreConnection connection = AppStoreConnection(); - final ProductDetailsResponse response = await connection - .queryProductDetails(['123', '456', '789'].toSet()); - expect(response.productDetails, []); - expect(response.notFoundIDs, ['123', '456', '789']); - expect(response.error.source, IAPSource.AppStore); - expect(response.error.code, 'error_code'); - expect(response.error.message, 'error_message'); - expect(response.error.details, {'info': 'error_info'}); - }); - }); - - group('query purchases list', () { - test('should get purchase list', () async { - QueryPurchaseDetailsResponse response = - await AppStoreConnection.instance.queryPastPurchases(); - expect(response.pastPurchases.length, 2); - expect(response.pastPurchases.first.purchaseID, - fakeIOSPlatform.transactions.first.transactionIdentifier); - expect(response.pastPurchases.last.purchaseID, - fakeIOSPlatform.transactions.last.transactionIdentifier); - expect(response.pastPurchases.first.purchaseID, - fakeIOSPlatform.transactions.first.transactionIdentifier); - expect(response.pastPurchases.last.purchaseID, - fakeIOSPlatform.transactions.last.transactionIdentifier); - expect( - response.pastPurchases.first.verificationData.localVerificationData, - 'dummy base64data'); - expect( - response.pastPurchases.first.verificationData.serverVerificationData, - 'dummy base64data'); - expect(response.error, isNull); - }); - - test('queryPastPurchases should not block transaction updates', () async { - fakeIOSPlatform.transactions - .add(fakeIOSPlatform.createPurchasedTransactionWithProductID('foo')); - Completer completer = Completer(); - Stream> stream = - AppStoreConnection.instance.purchaseUpdatedStream; - - StreamSubscription subscription; - subscription = stream.listen((purchaseDetailsList) { - if (purchaseDetailsList.first.status == PurchaseStatus.purchased) { - completer.complete(purchaseDetailsList); - subscription.cancel(); - } - }); - QueryPurchaseDetailsResponse response = - await AppStoreConnection.instance.queryPastPurchases(); - List result = await completer.future; - expect(result.length, 1); - expect(result.first.productID, 'foo'); - expect(response.error, isNull); - }); - - test('should get empty result if there is no restored transactions', - () async { - fakeIOSPlatform.testRestoredTransactionsNull = true; - QueryPurchaseDetailsResponse response = - await AppStoreConnection.instance.queryPastPurchases(); - expect(response.pastPurchases, isEmpty); - expect(response.error, isNull); - fakeIOSPlatform.testRestoredTransactionsNull = false; - }); - - test('test restore error', () async { - fakeIOSPlatform.testRestoredError = SKError( - code: 123, - domain: 'error_test', - userInfo: {'message': 'errorMessage'}); - QueryPurchaseDetailsResponse response = - await AppStoreConnection.instance.queryPastPurchases(); - expect(response.pastPurchases, isEmpty); - expect(response.error.source, IAPSource.AppStore); - expect(response.error.message, 'error_test'); - expect(response.error.details, {'message': 'errorMessage'}); - }); - - test('receipt error should populate null to verificationData.data', - () async { - fakeIOSPlatform.receiptData = null; - QueryPurchaseDetailsResponse response = - await AppStoreConnection.instance.queryPastPurchases(); - expect( - response.pastPurchases.first.verificationData.localVerificationData, - null); - expect( - response.pastPurchases.first.verificationData.serverVerificationData, - null); - }); - }); - - group('refresh receipt data', () { - test('should refresh receipt data', () async { - PurchaseVerificationData receiptData = - await AppStoreConnection.instance.refreshPurchaseVerificationData(); - expect(receiptData.source, IAPSource.AppStore); - expect(receiptData.localVerificationData, 'refreshed receipt data'); - expect(receiptData.serverVerificationData, 'refreshed receipt data'); - }); - }); - - group('make payment', () { - test( - 'buying non consumable, should get purchase objects in the purchase update callback', - () async { - List details = []; - Completer completer = Completer(); - Stream> stream = - AppStoreConnection.instance.purchaseUpdatedStream; - - StreamSubscription subscription; - subscription = stream.listen((purchaseDetailsList) { - details.addAll(purchaseDetailsList); - if (purchaseDetailsList.first.status == PurchaseStatus.purchased) { - completer.complete(details); - subscription.cancel(); - } - }); - final PurchaseParam purchaseParam = PurchaseParam( - productDetails: ProductDetails.fromSKProduct(dummyProductWrapper), - applicationUserName: 'appName'); - await AppStoreConnection.instance - .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 = - AppStoreConnection.instance.purchaseUpdatedStream; - - StreamSubscription subscription; - subscription = stream.listen((purchaseDetailsList) { - details.addAll(purchaseDetailsList); - if (purchaseDetailsList.first.status == PurchaseStatus.purchased) { - completer.complete(details); - subscription.cancel(); - } - }); - final PurchaseParam purchaseParam = PurchaseParam( - productDetails: ProductDetails.fromSKProduct(dummyProductWrapper), - applicationUserName: 'appName'); - await AppStoreConnection.instance - .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 PurchaseParam purchaseParam = PurchaseParam( - productDetails: ProductDetails.fromSKProduct(dummyProductWrapper), - applicationUserName: 'appName'); - expect( - () => AppStoreConnection.instance - .buyConsumable(purchaseParam: purchaseParam, autoConsume: false), - throwsA(TypeMatcher())); - }); - - test('should get failed purchase status', () async { - fakeIOSPlatform.testTransactionFail = true; - List details = []; - Completer completer = Completer(); - IAPError error; - - Stream> stream = - AppStoreConnection.instance.purchaseUpdatedStream; - 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 PurchaseParam purchaseParam = PurchaseParam( - productDetails: ProductDetails.fromSKProduct(dummyProductWrapper), - applicationUserName: 'appName'); - await AppStoreConnection.instance - .buyNonConsumable(purchaseParam: purchaseParam); - - IAPError completerError = await completer.future; - expect(completerError.code, 'purchase_error'); - expect(completerError.source, IAPSource.AppStore); - 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 = - AppStoreConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; - subscription = stream.listen((purchaseDetailsList) { - details.addAll(purchaseDetailsList); - purchaseDetailsList.forEach((purchaseDetails) { - if (purchaseDetails.pendingCompletePurchase) { - AppStoreConnection.instance.completePurchase(purchaseDetails); - completer.complete(details); - subscription.cancel(); - } - }); - }); - final PurchaseParam purchaseParam = PurchaseParam( - productDetails: ProductDetails.fromSKProduct(dummyProductWrapper), - applicationUserName: 'appName'); - await AppStoreConnection.instance - .buyNonConsumable(purchaseParam: purchaseParam); - List result = await completer.future; - expect(result.length, 2); - expect(result.first.productID, dummyProductWrapper.productIdentifier); - expect(fakeIOSPlatform.finishedTransactions.length, 1); - }); - }); - - group('consume purchase', () { - test('should throw when calling consume purchase on iOS', () async { - expect(() => AppStoreConnection.instance.consumePurchase(null), - throwsUnsupportedError); - }); - }); -} - -class FakeIOSPlatform { - FakeIOSPlatform() { - channel.setMockMethodCallHandler(onMethodCall); - } - - // pre-configured store informations - String receiptData; - Set validProductIDs; - Map validProducts; - List transactions; - List finishedTransactions; - bool testRestoredTransactionsNull; - bool testTransactionFail; - PlatformException queryProductException; - PlatformException restoreException; - SKError testRestoredError; - - void reset() { - transactions = []; - receiptData = 'dummy base64data'; - validProductIDs = ['123', '456'].toSet(); - validProducts = Map(); - for (String validID in validProductIDs) { - Map productWrapperMap = buildProductMap(dummyProductWrapper); - productWrapperMap['productIdentifier'] = validID; - 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; - } - - SKPaymentTransactionWrapper createPendingTransactionWithProductID(String id) { - return SKPaymentTransactionWrapper( - transactionIdentifier: null, - payment: SKPaymentWrapper(productIdentifier: id), - transactionState: SKPaymentTransactionStateWrapper.purchasing, - transactionTimeStamp: 123123.121, - error: null, - originalTransaction: null); - } - - SKPaymentTransactionWrapper createPurchasedTransactionWithProductID( - String id) { - return SKPaymentTransactionWrapper( - payment: SKPaymentWrapper(productIdentifier: id), - transactionState: SKPaymentTransactionStateWrapper.purchased, - transactionTimeStamp: 123123.121, - transactionIdentifier: id, - error: null, - originalTransaction: null); - } - - SKPaymentTransactionWrapper createFailedTransactionWithProductID(String id) { - return SKPaymentTransactionWrapper( - transactionIdentifier: null, - payment: SKPaymentWrapper(productIdentifier: id), - 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); - assert(productIDS is List, 'invalid argument type'); - 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) { - AppStoreConnection.observer - .restoreCompletedTransactionsFailed(error: testRestoredError); - return Future.sync(() {}); - } - if (!testRestoredTransactionsNull) { - AppStoreConnection.observer - .updatedTransactions(transactions: transactions); - } - AppStoreConnection.observer - .paymentQueueRestoreCompletedTransactionsFinished(); - return Future.sync(() {}); - case '-[InAppPurchasePlugin retrieveReceiptData:result:]': - if (receiptData != null) { - return Future.value(receiptData); - } else { - throw PlatformException(code: 'no_receipt_data'); - } - break; - case '-[InAppPurchasePlugin refreshReceipt:result:]': - receiptData = 'refreshed receipt data'; - return Future.sync(() {}); - case '-[InAppPurchasePlugin addPayment:result:]': - String id = call.arguments['productIdentifier']; - SKPaymentTransactionWrapper transaction = - createPendingTransactionWithProductID(id); - AppStoreConnection.observer - .updatedTransactions(transactions: [transaction]); - sleep(const Duration(milliseconds: 30)); - if (testTransactionFail) { - SKPaymentTransactionWrapper transaction_failed = - createFailedTransactionWithProductID(id); - AppStoreConnection.observer - .updatedTransactions(transactions: [transaction_failed]); - } else { - SKPaymentTransactionWrapper transaction_finished = - createPurchasedTransactionWithProductID(id); - AppStoreConnection.observer - .updatedTransactions(transactions: [transaction_finished]); - } - break; - case '-[InAppPurchasePlugin finishTransaction:result:]': - finishedTransactions - .add(createPurchasedTransactionWithProductID(call.arguments)); - break; - } - return Future.sync(() {}); - } -} diff --git a/packages/in_app_purchase/test/in_app_purchase_connection/google_play_connection_test.dart b/packages/in_app_purchase/test/in_app_purchase_connection/google_play_connection_test.dart deleted file mode 100644 index f06c4ff7efef..000000000000 --- a/packages/in_app_purchase/test/in_app_purchase_connection/google_play_connection_test.dart +++ /dev/null @@ -1,642 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart' show TestWidgetsFlutterBinding; -import 'package:in_app_purchase/src/in_app_purchase/purchase_details.dart'; -import 'package:test/test.dart'; - -import 'package:flutter/widgets.dart' hide TypeMatcher; -import 'package:in_app_purchase/billing_client_wrappers.dart'; -import 'package:in_app_purchase/src/billing_client_wrappers/enum_converters.dart'; -import 'package:in_app_purchase/src/in_app_purchase/google_play_connection.dart'; -import 'package:in_app_purchase/src/in_app_purchase/in_app_purchase_connection.dart'; -import 'package:in_app_purchase/src/channel.dart'; -import '../stub_in_app_purchase_platform.dart'; -import 'package:in_app_purchase/src/in_app_purchase/product_details.dart'; -import '../billing_client_wrappers/sku_details_wrapper_test.dart'; -import '../billing_client_wrappers/purchase_wrapper_test.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final StubInAppPurchasePlatform stubPlatform = StubInAppPurchasePlatform(); - GooglePlayConnection connection; - const String startConnectionCall = - 'BillingClient#startConnection(BillingClientStateListener)'; - const String endConnectionCall = 'BillingClient#endConnection()'; - - setUpAll(() { - channel.setMockMethodCallHandler(stubPlatform.fakeMethodCallHandler); - }); - - setUp(() { - WidgetsFlutterBinding.ensureInitialized(); - const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: startConnectionCall, - value: buildBillingResultMap(expectedBillingResult)); - stubPlatform.addResponse(name: endConnectionCall, value: null); - InAppPurchaseConnection.enablePendingPurchases(); - connection = GooglePlayConnection.instance; - }); - - tearDown(() { - stubPlatform.reset(); - GooglePlayConnection.reset(); - }); - - group('connection management', () { - test('connects on initialization', () { - expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(1)); - }); - }); - - group('isAvailable', () { - test('true', () async { - stubPlatform.addResponse(name: 'BillingClient#isReady()', value: true); - expect(await connection.isAvailable(), isTrue); - }); - - test('false', () async { - stubPlatform.addResponse(name: 'BillingClient#isReady()', value: false); - expect(await connection.isAvailable(), isFalse); - }); - }); - - group('querySkuDetails', () { - final 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( - responseCode: responseCode, debugMessage: debugMessage); - stubPlatform.addResponse(name: queryMethodName, value: { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'skuDetailsList': [], - }); - - final ProductDetailsResponse response = - await connection.queryProductDetails([''].toSet()); - 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( - responseCode: responseCode, debugMessage: debugMessage); - stubPlatform.addResponse(name: queryMethodName, value: { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'skuDetailsList': >[buildSkuMap(dummySkuDetails)] - }); - // Since queryProductDetails makes 2 platform method calls (one for each SkuType), the result will contain 2 dummyWrapper instead - // of 1. - final ProductDetailsResponse response = - await connection.queryProductDetails(['valid'].toSet()); - expect(response.productDetails.first.title, dummySkuDetails.title); - expect(response.productDetails.first.description, - dummySkuDetails.description); - expect(response.productDetails.first.price, dummySkuDetails.price); - }); - - test('should get the correct notFoundIDs', () async { - const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - stubPlatform.addResponse(name: queryMethodName, value: { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'skuDetailsList': >[buildSkuMap(dummySkuDetails)] - }); - // Since queryProductDetails makes 2 platform method calls (one for each SkuType), the result will contain 2 dummyWrapper instead - // of 1. - final ProductDetailsResponse response = - await connection.queryProductDetails(['invalid'].toSet()); - expect(response.notFoundIDs.first, 'invalid'); - }); - - test( - 'should have error stored in the response when platform exception is thrown', - () async { - final BillingResponse responseCode = BillingResponse.ok; - stubPlatform.addResponse( - name: queryMethodName, - value: { - 'responseCode': BillingResponseConverter().toJson(responseCode), - 'skuDetailsList': >[ - buildSkuMap(dummySkuDetails) - ] - }, - additionalStepBeforeReturn: (_) { - throw PlatformException( - code: 'error_code', - message: 'error_message', - 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 connection.queryProductDetails(['invalid'].toSet()); - expect(response.notFoundIDs, ['invalid']); - expect(response.productDetails, isEmpty); - expect(response.error.source, IAPSource.GooglePlay); - expect(response.error.code, 'error_code'); - expect(response.error.message, 'error_message'); - expect(response.error.details, {'info': 'error_info'}); - }); - }); - - group('queryPurchaseDetails', () { - const String queryMethodName = 'BillingClient#queryPurchases(String)'; - test('handles error', () async { - const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.developerError; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - - stubPlatform.addResponse(name: queryMethodName, value: { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': BillingResponseConverter().toJson(responseCode), - 'purchasesList': >[] - }); - final QueryPurchaseDetailsResponse response = - await connection.queryPastPurchases(); - expect(response.pastPurchases, isEmpty); - expect(response.error.message, BillingResponse.developerError.toString()); - expect(response.error.source, IAPSource.GooglePlay); - }); - - test('returns SkuDetailsResponseWrapper', () async { - const String debugMessage = 'dummy message'; - final BillingResponse responseCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - - stubPlatform.addResponse(name: queryMethodName, value: { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': BillingResponseConverter().toJson(responseCode), - 'purchasesList': >[ - buildPurchaseMap(dummyPurchase), - ] - }); - - // Since queryPastPurchases makes 2 platform method calls (one for each SkuType), the result will contain 2 dummyWrapper instead - // of 1. - final QueryPurchaseDetailsResponse response = - await connection.queryPastPurchases(); - expect(response.error, isNull); - expect(response.pastPurchases.first.purchaseID, dummyPurchase.orderId); - }); - - test('should store platform exception in the response', () async { - const String debugMessage = 'dummy message'; - - final BillingResponse responseCode = BillingResponse.developerError; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: responseCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: queryMethodName, - value: { - 'responseCode': BillingResponseConverter().toJson(responseCode), - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'purchasesList': >[] - }, - additionalStepBeforeReturn: (_) { - throw PlatformException( - code: 'error_code', - message: 'error_message', - details: {'info': 'error_info'}, - ); - }); - final QueryPurchaseDetailsResponse response = - await connection.queryPastPurchases(); - expect(response.pastPurchases, isEmpty); - expect(response.error.code, 'error_code'); - expect(response.error.message, 'error_message'); - expect(response.error.details, {'info': 'error_info'}); - }); - }); - - group('refresh receipt data', () { - test('should throw on android', () { - expect(GooglePlayConnection.instance.refreshPurchaseVerificationData(), - throwsUnsupportedError); - }); - }); - - group('make payment', () { - final 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 String debugMessage = 'dummy message'; - final BillingResponse sentCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: sentCode, debugMessage: debugMessage); - - stubPlatform.addResponse( - name: launchMethodName, - value: buildBillingResultMap(expectedBillingResult), - additionalStepBeforeReturn: (_) { - // Mock java update purchase callback. - MethodCall call = MethodCall(kOnPurchasesUpdated, { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': 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, - } - ] - }); - connection.billingClient.callHandler(call); - }); - Completer completer = Completer(); - PurchaseDetails purchaseDetails; - Stream purchaseStream = - GooglePlayConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; - subscription = purchaseStream.listen((_) { - purchaseDetails = _.first; - completer.complete(purchaseDetails); - subscription.cancel(); - }, onDone: () {}); - final PurchaseParam purchaseParam = PurchaseParam( - productDetails: ProductDetails.fromSkuDetails(skuDetails), - applicationUserName: accountId); - final bool launchResult = await GooglePlayConnection.instance - .buyNonConsumable(purchaseParam: purchaseParam); - - PurchaseDetails result = await completer.future; - expect(launchResult, isTrue); - expect(result.purchaseID, 'orderID1'); - expect(result.status, PurchaseStatus.purchased); - expect(result.productID, dummySkuDetails.sku); - }); - - test('handles an error with an empty purchases list', () async { - final SkuDetailsWrapper skuDetails = dummySkuDetails; - final String accountId = "hashedAccountId"; - const String debugMessage = 'dummy message'; - final BillingResponse sentCode = BillingResponse.error; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: sentCode, debugMessage: debugMessage); - - stubPlatform.addResponse( - name: launchMethodName, - value: buildBillingResultMap(expectedBillingResult), - additionalStepBeforeReturn: (_) { - // Mock java update purchase callback. - MethodCall call = MethodCall(kOnPurchasesUpdated, { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': BillingResponseConverter().toJson(sentCode), - 'purchasesList': [] - }); - connection.billingClient.callHandler(call); - }); - Completer completer = Completer(); - PurchaseDetails purchaseDetails; - Stream purchaseStream = - GooglePlayConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; - subscription = purchaseStream.listen((_) { - purchaseDetails = _.first; - completer.complete(purchaseDetails); - subscription.cancel(); - }, onDone: () {}); - final PurchaseParam purchaseParam = PurchaseParam( - productDetails: ProductDetails.fromSkuDetails(skuDetails), - applicationUserName: accountId); - await GooglePlayConnection.instance - .buyNonConsumable(purchaseParam: purchaseParam); - PurchaseDetails result = await completer.future; - - expect(result.error, isNotNull); - expect(result.error.source, IAPSource.GooglePlay); - expect(result.status, PurchaseStatus.error); - expect(result.purchaseID, isNull); - }); - - test('buy consumable with auto consume, serializes and deserializes data', - () async { - final SkuDetailsWrapper skuDetails = dummySkuDetails; - final String accountId = "hashedAccountId"; - const String debugMessage = 'dummy message'; - final BillingResponse sentCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: sentCode, debugMessage: debugMessage); - - stubPlatform.addResponse( - name: launchMethodName, - value: buildBillingResultMap(expectedBillingResult), - additionalStepBeforeReturn: (_) { - // Mock java update purchase callback. - MethodCall call = MethodCall(kOnPurchasesUpdated, { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': 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, - } - ] - }); - connection.billingClient.callHandler(call); - }); - Completer consumeCompleter = Completer(); - // adding call back for consume purchase - final BillingResponse expectedCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResultForConsume = - BillingResultWrapper( - responseCode: expectedCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: consumeMethodName, - value: buildBillingResultMap(expectedBillingResultForConsume), - additionalStepBeforeReturn: (dynamic args) { - String purchaseToken = args['purchaseToken']; - consumeCompleter.complete((purchaseToken)); - }); - - Completer completer = Completer(); - PurchaseDetails purchaseDetails; - Stream purchaseStream = - GooglePlayConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; - subscription = purchaseStream.listen((_) { - purchaseDetails = _.first; - completer.complete(purchaseDetails); - subscription.cancel(); - }, onDone: () {}); - final PurchaseParam purchaseParam = PurchaseParam( - productDetails: ProductDetails.fromSkuDetails(skuDetails), - applicationUserName: accountId); - final bool launchResult = await GooglePlayConnection.instance - .buyConsumable(purchaseParam: purchaseParam); - - // Verify that the result has succeeded - PurchaseDetails result = await completer.future; - expect(launchResult, isTrue); - expect(result.billingClientPurchase.purchaseToken, - await consumeCompleter.future); - expect(result.status, PurchaseStatus.purchased); - expect(result.error, isNull); - }); - - test('buyNonConsumable propagates failures to launch the billing flow', - () async { - const String debugMessage = 'dummy message'; - final BillingResponse sentCode = BillingResponse.error; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: sentCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: launchMethodName, - value: buildBillingResultMap(expectedBillingResult)); - - final bool result = await GooglePlayConnection.instance.buyNonConsumable( - purchaseParam: PurchaseParam( - productDetails: ProductDetails.fromSkuDetails(dummySkuDetails))); - - // Verify that the failure has been converted and returned - expect(result, isFalse); - }); - - test('buyConsumable propagates failures to launch the billing flow', - () async { - const String debugMessage = 'dummy message'; - final BillingResponse sentCode = BillingResponse.developerError; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: sentCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: launchMethodName, - value: buildBillingResultMap(expectedBillingResult), - ); - - final bool result = await GooglePlayConnection.instance.buyConsumable( - purchaseParam: PurchaseParam( - productDetails: ProductDetails.fromSkuDetails(dummySkuDetails))); - - // Verify that the failure has been converted and returned - expect(result, isFalse); - }); - - test('adds consumption failures to PurchaseDetails objects', () async { - final SkuDetailsWrapper skuDetails = dummySkuDetails; - final String accountId = "hashedAccountId"; - const String debugMessage = 'dummy message'; - final BillingResponse sentCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: sentCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: launchMethodName, - value: buildBillingResultMap(expectedBillingResult), - additionalStepBeforeReturn: (_) { - // Mock java update purchase callback. - MethodCall call = MethodCall(kOnPurchasesUpdated, { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': 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, - } - ] - }); - connection.billingClient.callHandler(call); - }); - Completer consumeCompleter = Completer(); - // adding call back for consume purchase - final BillingResponse expectedCode = BillingResponse.error; - final BillingResultWrapper expectedBillingResultForConsume = - BillingResultWrapper( - responseCode: expectedCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: consumeMethodName, - value: buildBillingResultMap(expectedBillingResultForConsume), - additionalStepBeforeReturn: (dynamic args) { - String purchaseToken = args['purchaseToken']; - consumeCompleter.complete(purchaseToken); - }); - - Completer completer = Completer(); - PurchaseDetails purchaseDetails; - Stream purchaseStream = - GooglePlayConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; - subscription = purchaseStream.listen((_) { - purchaseDetails = _.first; - completer.complete(purchaseDetails); - subscription.cancel(); - }, onDone: () {}); - final PurchaseParam purchaseParam = PurchaseParam( - productDetails: ProductDetails.fromSkuDetails(skuDetails), - applicationUserName: accountId); - await GooglePlayConnection.instance - .buyConsumable(purchaseParam: purchaseParam); - - // Verify that the result has an error for the failed consumption - PurchaseDetails result = await completer.future; - expect(result.billingClientPurchase.purchaseToken, - await consumeCompleter.future); - expect(result.status, PurchaseStatus.error); - expect(result.error, isNotNull); - expect(result.error.code, kConsumptionFailedErrorCode); - }); - - test( - 'buy consumable without auto consume, consume api should not receive calls', - () async { - final SkuDetailsWrapper skuDetails = dummySkuDetails; - final String accountId = "hashedAccountId"; - const String debugMessage = 'dummy message'; - final BillingResponse sentCode = BillingResponse.developerError; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: sentCode, debugMessage: debugMessage); - - stubPlatform.addResponse( - name: launchMethodName, - value: buildBillingResultMap(expectedBillingResult), - additionalStepBeforeReturn: (_) { - // Mock java update purchase callback. - MethodCall call = MethodCall(kOnPurchasesUpdated, { - 'billingResult': buildBillingResultMap(expectedBillingResult), - 'responseCode': 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, - } - ] - }); - connection.billingClient.callHandler(call); - }); - Completer consumeCompleter = Completer(); - // adding call back for consume purchase - final BillingResponse expectedCode = BillingResponse.ok; - final BillingResultWrapper expectedBillingResultForConsume = - BillingResultWrapper( - responseCode: expectedCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: consumeMethodName, - value: buildBillingResultMap(expectedBillingResultForConsume), - additionalStepBeforeReturn: (dynamic args) { - String purchaseToken = args['purchaseToken']; - consumeCompleter.complete((purchaseToken)); - }); - - Stream purchaseStream = - GooglePlayConnection.instance.purchaseUpdatedStream; - StreamSubscription subscription; - subscription = purchaseStream.listen((_) { - consumeCompleter.complete(null); - subscription.cancel(); - }, onDone: () {}); - final PurchaseParam purchaseParam = PurchaseParam( - productDetails: ProductDetails.fromSkuDetails(skuDetails), - applicationUserName: accountId); - await GooglePlayConnection.instance - .buyConsumable(purchaseParam: purchaseParam, autoConsume: false); - expect(null, await consumeCompleter.future); - }); - }); - - group('consume purchases', () { - const String consumeMethodName = - 'BillingClient#consumeAsync(String, ConsumeResponseListener)'; - test('consume purchase async success', () async { - final BillingResponse expectedCode = BillingResponse.ok; - const String debugMessage = 'dummy message'; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: expectedCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: consumeMethodName, - value: buildBillingResultMap(expectedBillingResult), - ); - final BillingResultWrapper billingResultWrapper = - await GooglePlayConnection.instance - .consumePurchase(PurchaseDetails.fromPurchase(dummyPurchase)); - - expect(billingResultWrapper, equals(expectedBillingResult)); - }); - }); - - group('complete purchase', () { - const String completeMethodName = - 'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)'; - test('complete purchase success', () async { - final BillingResponse expectedCode = BillingResponse.ok; - const String debugMessage = 'dummy message'; - final BillingResultWrapper expectedBillingResult = BillingResultWrapper( - responseCode: expectedCode, debugMessage: debugMessage); - stubPlatform.addResponse( - name: completeMethodName, - value: buildBillingResultMap(expectedBillingResult), - ); - PurchaseDetails purchaseDetails = - PurchaseDetails.fromPurchase(dummyUnacknowledgedPurchase); - Completer completer = Completer(); - purchaseDetails.status = PurchaseStatus.purchased; - if (purchaseDetails.pendingCompletePurchase) { - final BillingResultWrapper billingResultWrapper = - await GooglePlayConnection.instance.completePurchase( - purchaseDetails, - developerPayload: 'dummy payload'); - print('pending ${billingResultWrapper.responseCode}'); - print('expectedBillingResult ${expectedBillingResult.responseCode}'); - print('pending ${billingResultWrapper.debugMessage}'); - print('expectedBillingResult ${expectedBillingResult.debugMessage}'); - expect(billingResultWrapper, equals(expectedBillingResult)); - completer.complete(billingResultWrapper); - } - expect(await completer.future, equals(expectedBillingResult)); - }); - }); -} diff --git a/packages/in_app_purchase/test/store_kit_wrappers/sk_methodchannel_apis_test.dart b/packages/in_app_purchase/test/store_kit_wrappers/sk_methodchannel_apis_test.dart deleted file mode 100644 index 3a08d9e8e45d..000000000000 --- a/packages/in_app_purchase/test/store_kit_wrappers/sk_methodchannel_apis_test.dart +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:in_app_purchase/src/channel.dart'; -import 'package:in_app_purchase/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(() {}); - - 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.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}); - }); - }); - - 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('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.transactionIdentifier)); - }); - - test('should restore transaction', () async { - SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); - TestPaymentTransactionObserver observer = - TestPaymentTransactionObserver(); - queue.setTransactionObserver(observer); - await queue.restoreTransactions(applicationUserName: 'aUserID'); - expect(fakeIOSPlatform.applicationNameHasTransactionRestored, 'aUserID'); - }); - }); -} - -class FakeIOSPlatform { - FakeIOSPlatform() { - channel.setMockMethodCallHandler(onMethodCall); - getProductRequestFailTest = false; - } - // get product request - List startProductRequestParam; - bool getProductRequestFailTest; - - // refresh receipt request - int refreshReceipt = 0; - Map refreshReceiptParam; - - // payment queue - List payments = []; - List transactionsFinished = []; - String applicationNameHasTransactionRestored; - - Future onMethodCall(MethodCall call) { - switch (call.method) { - // request makers - case '-[InAppPurchasePlugin startProductRequest:result:]': - List productIDS = - List.castFrom(call.arguments); - assert(productIDS is List, 'invalid argument type'); - startProductRequestParam = call.arguments; - if (getProductRequestFailTest) { - return Future>.value(null); - } - return Future>.value( - buildProductResponseMap(dummyProductResponseWrapper)); - case '-[InAppPurchasePlugin refreshReceipt:result:]': - refreshReceipt++; - refreshReceiptParam = call.arguments; - return Future.sync(() {}); - // receipt manager - case '-[InAppPurchasePlugin retrieveReceiptData:result:]': - return Future.value('receipt data'); - // payment queue - case '-[SKPaymentQueue canMakePayments:]': - return Future.value(true); - case '-[SKPaymentQueue transactions]': - return Future>.value([buildTransactionMap(dummyTransaction)]); - case '-[InAppPurchasePlugin addPayment:result:]': - payments.add(SKPaymentWrapper.fromJson(call.arguments)); - return Future.sync(() {}); - case '-[InAppPurchasePlugin finishTransaction:result:]': - transactionsFinished.add(call.arguments); - return Future.sync(() {}); - case '-[InAppPurchasePlugin restoreTransactions:result:]': - applicationNameHasTransactionRestored = call.arguments; - return Future.sync(() {}); - } - return Future.sync(() {}); - } -} - -class TestPaymentTransactionObserver extends SKTransactionObserverWrapper { - void updatedTransactions({List transactions}) {} - - void removedTransactions({List transactions}) {} - - void restoreCompletedTransactionsFailed({SKError error}) {} - - void paymentQueueRestoreCompletedTransactionsFinished() {} - - bool shouldAddStorePayment( - {SKPaymentWrapper payment, SKProductWrapper product}) { - return true; - } -} diff --git a/packages/in_app_purchase/test/store_kit_wrappers/sk_product_test.dart b/packages/in_app_purchase/test/store_kit_wrappers/sk_product_test.dart deleted file mode 100644 index 2a9066f05c53..000000000000 --- a/packages/in_app_purchase/test/store_kit_wrappers/sk_product_test.dart +++ /dev/null @@ -1,157 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:in_app_purchase/src/in_app_purchase/purchase_details.dart'; -import 'package:test/test.dart'; -import 'package:in_app_purchase/src/store_kit_wrappers/sk_product_wrapper.dart'; -import 'package:in_app_purchase/src/in_app_purchase/in_app_purchase_connection.dart'; -import 'package:in_app_purchase/src/in_app_purchase/product_details.dart'; -import 'package:in_app_purchase/store_kit_wrappers.dart'; -import 'sk_test_stub_objects.dart'; - -void main() { - group('product related object wrapper test', () { - test( - 'SKProductSubscriptionPeriodWrapper should have property values consistent with map', - () { - final SKProductSubscriptionPeriodWrapper wrapper = - SKProductSubscriptionPeriodWrapper.fromJson( - buildSubscriptionPeriodMap(dummySubscription)); - expect(wrapper, equals(dummySubscription)); - }); - - test( - 'SKProductSubscriptionPeriodWrapper should have properties to be null if map is empty', - () { - final SKProductSubscriptionPeriodWrapper wrapper = - SKProductSubscriptionPeriodWrapper.fromJson({}); - expect(wrapper.numberOfUnits, null); - expect(wrapper.unit, null); - }); - - test( - 'SKProductDiscountWrapper should have property values consistent with map', - () { - final SKProductDiscountWrapper wrapper = - SKProductDiscountWrapper.fromJson(buildDiscountMap(dummyDiscount)); - expect(wrapper, equals(dummyDiscount)); - }); - - test( - 'SKProductDiscountWrapper should have properties to be null if map is empty', - () { - final SKProductDiscountWrapper wrapper = - SKProductDiscountWrapper.fromJson({}); - expect(wrapper.price, null); - expect(wrapper.priceLocale, null); - expect(wrapper.numberOfPeriods, null); - expect(wrapper.paymentMode, null); - expect(wrapper.subscriptionPeriod, null); - }); - - test('SKProductWrapper should have property values consistent with map', - () { - final SKProductWrapper wrapper = - SKProductWrapper.fromJson(buildProductMap(dummyProductWrapper)); - expect(wrapper, equals(dummyProductWrapper)); - }); - - test('SKProductWrapper should have properties to be null if map is empty', - () { - final SKProductWrapper wrapper = - SKProductWrapper.fromJson({}); - expect(wrapper.productIdentifier, null); - expect(wrapper.localizedTitle, null); - expect(wrapper.localizedDescription, null); - expect(wrapper.priceLocale, null); - expect(wrapper.subscriptionGroupIdentifier, null); - expect(wrapper.price, null); - expect(wrapper.subscriptionPeriod, null); - }); - - test('toProductDetails() should return correct Product object', () { - final SKProductWrapper wrapper = - SKProductWrapper.fromJson(buildProductMap(dummyProductWrapper)); - final ProductDetails product = ProductDetails.fromSKProduct(wrapper); - 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.skProduct, wrapper); - expect(product.skuDetail, null); - }); - - test('SKProductResponse wrapper should match', () { - final SkProductResponseWrapper wrapper = - SkProductResponseWrapper.fromJson( - buildProductResponseMap(dummyProductResponseWrapper)); - expect(wrapper, equals(dummyProductResponseWrapper)); - }); - test('SKProductResponse wrapper should default to empty list', () { - final Map> productResponseMapEmptyList = - >{ - 'products': >[], - 'invalidProductIdentifiers': [], - }; - final SkProductResponseWrapper wrapper = - SkProductResponseWrapper.fromJson(productResponseMapEmptyList); - expect(wrapper.products.length, 0); - expect(wrapper.invalidProductIdentifiers.length, 0); - }); - - test('LocaleWrapper should have property values consistent with map', () { - final SKPriceLocaleWrapper wrapper = - SKPriceLocaleWrapper.fromJson(buildLocaleMap(dummyLocale)); - expect(wrapper, equals(dummyLocale)); - }); - }); - - group('Payment queue related object tests', () { - test('Should construct correct SKPaymentWrapper from json', () { - SKPaymentWrapper payment = - SKPaymentWrapper.fromJson(dummyPayment.toMap()); - expect(payment, equals(dummyPayment)); - }); - - test('Should construct correct SKError from json', () { - SKError error = SKError.fromJson(buildErrorMap(dummyError)); - expect(error, equals(dummyError)); - }); - - test('Should construct correct SKTransactionWrapper from json', () { - SKPaymentTransactionWrapper transaction = - SKPaymentTransactionWrapper.fromJson( - buildTransactionMap(dummyTransaction)); - expect(transaction, equals(dummyTransaction)); - }); - - test('toPurchaseDetails() should return correct PurchaseDetail object', () { - PurchaseDetails details = - PurchaseDetails.fromSKTransaction(dummyTransaction, 'receipt data'); - expect(dummyTransaction.transactionIdentifier, details.purchaseID); - expect(dummyTransaction.payment.productIdentifier, details.productID); - expect((dummyTransaction.transactionTimeStamp * 1000).toInt().toString(), - details.transactionDate); - expect(details.verificationData.localVerificationData, 'receipt data'); - expect(details.verificationData.serverVerificationData, 'receipt data'); - expect(details.verificationData.source, IAPSource.AppStore); - expect(details.skPaymentTransaction, dummyTransaction); - expect(details.billingClientPurchase, null); - expect(details.pendingCompletePurchase, true); - }); - test('Should generate correct map of the payment object', () { - Map map = dummyPayment.toMap(); - expect(map['productIdentifier'], dummyPayment.productIdentifier); - expect(map['applicationUsername'], dummyPayment.applicationUsername); - - expect(map['requestData'], dummyPayment.requestData); - - expect(map['quantity'], dummyPayment.quantity); - - expect(map['simulatesAskToBuyInSandbox'], - dummyPayment.simulatesAskToBuyInSandbox); - }); - }); -} diff --git a/packages/in_app_purchase/test/stub_in_app_purchase_platform.dart b/packages/in_app_purchase/test/stub_in_app_purchase_platform.dart deleted file mode 100644 index 312479573a68..000000000000 --- a/packages/in_app_purchase/test/stub_in_app_purchase_platform.dart +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'package:flutter/services.dart'; - -typedef void AdditionalSteps(dynamic args); - -class StubInAppPurchasePlatform { - Map _expectedCalls = {}; - Map _additionalSteps = {}; - void addResponse( - {String name, - dynamic value, - AdditionalSteps additionalStepBeforeReturn}) { - _additionalSteps[name] = additionalStepBeforeReturn; - _expectedCalls[name] = value; - } - - List _previousCalls = []; - List get previousCalls => _previousCalls; - MethodCall previousCallMatching(String name) => _previousCalls - .firstWhere((MethodCall call) => call.method == name, orElse: () => null); - int countPreviousCalls(String name) => - _previousCalls.where((MethodCall call) => call.method == name).length; - - void reset() { - _expectedCalls.clear(); - _previousCalls.clear(); - _additionalSteps.clear(); - } - - Future fakeMethodCallHandler(MethodCall call) async { - _previousCalls.add(call); - if (_expectedCalls.containsKey(call.method)) { - if (_additionalSteps[call.method] != null) { - _additionalSteps[call.method](call.arguments); - } - return Future.sync(() => _expectedCalls[call.method]); - } else { - return Future.sync(() => null); - } - } -} diff --git a/packages/integration_test/CHANGELOG.md b/packages/integration_test/CHANGELOG.md deleted file mode 100644 index d6ef143666e6..000000000000 --- a/packages/integration_test/CHANGELOG.md +++ /dev/null @@ -1,209 +0,0 @@ -## 0.9.1 - -* Keep handling deprecated Android v1 classes for backward compatibility. - -## 0.9.0 - -* Add screenshot capability to web tests. - -## 0.8.2 - -* Add support to get timeline. - -## 0.8.1 - -* Show stack trace of widget test errors on the platform side -* Fix method channel name for iOS - -## 0.8.0 - -* Rename plugin to integration_test. - -## 0.7.0 - -* Move utilities for tracking frame performance in an e2e test to `flutter_test`. - -## 0.6.3 - -* Add customizable `flutter_driver` adaptor. -* Add utilities for tracking frame performance in an e2e test. - -## 0.6.2+1 - -* Fix incorrect test results when one test passes then another fails - -## 0.6.2 - -* Fix `setSurfaceSize` for e2e tests - -## 0.6.1 - -* Added `data` in the reported json. - -## 0.6.0 - -* **Breaking change** `E2EPlugin` exports a `Future` for `testResults`. - -## 0.5.0+1 - -* Fixed the device pixel ratio problem. - -## 0.5.0 - -* **Breaking change** by default, tests will use the device window size. - Tests can still override the window size by using the `setSurfaceSize` method. -* **Breaking change** If using Flutter 1.19.0-2.0.pre.196 or greater, the - `testTextInput` will no longer automatically register. -* **Breaking change** If using Flutter 1.19.0-2.0.pre.196 or greater, the - `HttpOverrides` will no longer be set by default. -* Minor formatting changes to Dart code. - -## 0.4.3+3 - -* Fixed code snippet in readme that referenced a non-existent `result` variable. - -## 0.4.3+2 - -* Bumps AGP to 3.6.3 -* Changes android-retrofuture dependency type to "implementation" - -## 0.4.3+1 - -* Post-v2 Android embedding cleanup. - -## 0.4.3 - -* Uses CompletableFuture from android-retrofuture allow compatibility with API < 24. - -## 0.4.2 - -* Adds support for Android E2E tests that utilize other @Rule's, like GrantPermissionRule. -* Fix CocoaPods podspec lint warnings. - -## 0.4.1 - -* Remove Android dependencies fallback. -* Require Flutter SDK 1.12.13+hotfix.5 or greater. - -## 0.4.0 - -* **Breaking change** Driver request_data call's response has changed to - encapsulate the failure details. -* Details for failure cases are added: failed method name, stack trace. - -## 0.3.0+1 - -* Replace deprecated `getFlutterEngine` call on Android. - -## 0.3.0 - -* Updates documentation to instruct developers not to launch the activity since - we are doing it for them. -* Renames `FlutterRunner` to `FlutterTestRunner` to avoid conflict with Fuchsia. - -## 0.2.4+4 - -* Fixed a hang that occurred on platforms that don't have a `MethodChannel` listener registered.. - -## 0.2.4+3 - -* Fixed code snippet in the readme under the "Using Flutter driver to run tests" section. - -## 0.2.4+2 - -* Make the pedantic dev_dependency explicit. - -## 0.2.4+1 - -* Registering web service extension for using e2e with web. - -## 0.2.4 - -* Fixed problem with XCTest in XCode 11.3 where the testing bundles were getting - opened multiple times which interfered with the singleton logic for E2EPlugin. - -## 0.2.3+1 - -* Added a driver test for failure behavior. - -## 0.2.3 - -* Updates `E2EPlugin` and add skeleton iOS test case `E2EIosTest`. -* Adds instructions to README.md about e2e testing on iOS devices. -* Adds iOS e2e testing to example. - -## 0.2.2+3 - -* 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.2.2+2 - -* Adds an android dummy project to silence warnings and removes unnecessary - .gitignore files. - -## 0.2.2+1 - -* Fix pedantic lints. Adds a missing await in the example test and some missing - documentation. - -## 0.2.2 - -* Added a stub macos implementation -* Added a macos example - -## 0.2.1+1 - -* Updated README. - -## 0.2.1 - -* Support the v2 Android embedder. -* Print a warning if the plugin is not registered. -* Updated method channel name. -* Set a Flutter minimum SDK version. - -## 0.2.0+1 - -* Updated README. - -## 0.2.0 - -* Renamed package from instrumentation_adapter to e2e. -* Refactored example app test. -* **Breaking change**. Renamed `InstrumentationAdapterFlutterBinding` to - `IntegrationTestWidgetsFlutterBinding`. -* Updated README. - -## 0.1.4 - -* Migrate example to AndroidX. -* Define clang module for iOS. - -## 0.1.3 - -* Added example app. -* Added stub iOS implementation. -* Updated README. -* No longer throws errors when running tests on the host. - -## 0.1.2 - -* Added support for running tests using Flutter driver. - -## 0.1.1 - -* Updates about using *androidx* library. - -## 0.1.0 - -* Update boilerplate test to use `@Rule` instead of `FlutterTest`. - -## 0.0.2 - -* Document current usage instructions, which require adding a Java test file. - -## 0.0.1 - -* Initial release diff --git a/packages/integration_test/LICENSE b/packages/integration_test/LICENSE deleted file mode 100644 index 507569823f1b..000000000000 --- a/packages/integration_test/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright 2019 The Chromium Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/integration_test/README.md b/packages/integration_test/README.md index be08a722bc75..802fd4cafefc 100644 --- a/packages/integration_test/README.md +++ b/packages/integration_test/README.md @@ -1,4 +1,17 @@ -# integration_test +# integration_test (deprecated) + +## DEPRECATED + +This package has been moved to the Flutter SDK. Starting with Flutter 2.0, +it should be included as: + +``` +dev_dependencies: + integration_test: + 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` @@ -14,6 +27,8 @@ Create a `integration_test/` directory for your package. In this directory, create a `_test.dart`, using the following as a starting point to make assertions. +Note: You should only use `testWidgets` to declare your tests, or errors will not be reported correctly. + ```dart import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; @@ -42,7 +57,7 @@ Future main() => integrationDriver(); You can also use different driver scripts to customize the behavior of the app under test. For example, `FlutterDriver` can also be parameterized with different [options](https://api.flutter.dev/flutter/flutter_driver/FlutterDriver/connect.html). -See the [extended driver](https://github.com/flutter/plugins/tree/master/packages/integration_test/example/test_driver/integration_test_extended_driver.dart) for an example. +See the [extended driver](https://github.com/flutter/plugins/blob/master/packages/integration_test/example/test_driver/extended_integration_test.dart) for an example. ### Package Structure diff --git a/packages/integration_test/android/.gitignore b/packages/integration_test/android/.gitignore deleted file mode 100644 index c6cbe562a427..000000000000 --- a/packages/integration_test/android/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/workspace.xml -/.idea/libraries -.DS_Store -/build -/captures diff --git a/packages/integration_test/android/build.gradle b/packages/integration_test/android/build.gradle deleted file mode 100644 index 9f5a3896be0d..000000000000 --- a/packages/integration_test/android/build.gradle +++ /dev/null @@ -1,44 +0,0 @@ -group 'com.example.integration_test' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.6.3' - } -} - -rootProject.allprojects { - repositories { - google() - jcenter() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 28 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - } - dependencies { - api 'junit:junit:4.12' - - // https://developer.android.com/jetpack/androidx/releases/test/#1.2.0 - api 'androidx.test:runner:1.2.0' - api 'androidx.test:rules:1.2.0' - api 'androidx.test.espresso:espresso-core:3.2.0' - - implementation 'com.google.guava:guava:28.1-android' - } -} diff --git a/packages/integration_test/android/gradle.properties b/packages/integration_test/android/gradle.properties deleted file mode 100644 index 2bd6f4fda009..000000000000 --- a/packages/integration_test/android/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M - diff --git a/packages/integration_test/android/settings.gradle b/packages/integration_test/android/settings.gradle deleted file mode 100644 index 1d010945f01e..000000000000 --- a/packages/integration_test/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'integrationTest' diff --git a/packages/integration_test/android/src/main/AndroidManifest.xml b/packages/integration_test/android/src/main/AndroidManifest.xml deleted file mode 100644 index 183e7fcd827c..000000000000 --- a/packages/integration_test/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/packages/integration_test/android/src/main/java/dev/flutter/plugins/integration_test/FlutterTestRunner.java b/packages/integration_test/android/src/main/java/dev/flutter/plugins/integration_test/FlutterTestRunner.java deleted file mode 100644 index fc6b99b30f64..000000000000 --- a/packages/integration_test/android/src/main/java/dev/flutter/plugins/integration_test/FlutterTestRunner.java +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package dev.flutter.plugins.integration_test; - -import android.util.Log; -import androidx.test.rule.ActivityTestRule; -import java.lang.reflect.Field; -import java.util.Map; -import java.util.concurrent.ExecutionException; -import org.junit.Rule; -import org.junit.rules.TestRule; -import org.junit.runner.Description; -import org.junit.runner.Runner; -import org.junit.runner.notification.Failure; -import org.junit.runner.notification.RunNotifier; - -public class FlutterTestRunner extends Runner { - - private static final String TAG = "FlutterTestRunner"; - - final Class testClass; - TestRule rule = null; - - public FlutterTestRunner(Class testClass) { - super(); - this.testClass = testClass; - - // Look for an `ActivityTestRule` annotated `@Rule` and invoke `launchActivity()` - Field[] fields = testClass.getDeclaredFields(); - for (Field field : fields) { - if (field.isAnnotationPresent(Rule.class)) { - try { - Object instance = testClass.newInstance(); - if (field.get(instance) instanceof ActivityTestRule) { - rule = (TestRule) field.get(instance); - break; - } - } catch (InstantiationException | IllegalAccessException e) { - // This might occur if the developer did not make the rule public. - // We could call field.setAccessible(true) but it seems better to throw. - throw new RuntimeException("Unable to access activity rule", e); - } - } - } - } - - @Override - public Description getDescription() { - return Description.createTestDescription(testClass, "Flutter Tests"); - } - - @Override - public void run(RunNotifier notifier) { - if (rule == null) { - throw new RuntimeException("Unable to run tests due to missing activity rule"); - } - try { - if (rule instanceof ActivityTestRule) { - ((ActivityTestRule) rule).launchActivity(null); - } - } catch (RuntimeException e) { - Log.v(TAG, "launchActivity failed, possibly because the activity was already running. " + e); - Log.v( - TAG, - "Try disabling auto-launch of the activity, e.g. ActivityTestRule<>(MainActivity.class, true, false);"); - } - Map results = null; - try { - results = IntegrationTestPlugin.testResults.get(); - } catch (ExecutionException | InterruptedException e) { - throw new IllegalThreadStateException("Unable to get test results"); - } - - for (String name : results.keySet()) { - Description d = Description.createTestDescription(testClass, name); - notifier.fireTestStarted(d); - String outcome = results.get(name); - if (!outcome.equals("success")) { - Exception dummyException = new Exception(outcome); - notifier.fireTestFailure(new Failure(d, dummyException)); - } - notifier.fireTestFinished(d); - } - } -} diff --git a/packages/integration_test/android/src/main/java/dev/flutter/plugins/integration_test/IntegrationTestPlugin.java b/packages/integration_test/android/src/main/java/dev/flutter/plugins/integration_test/IntegrationTestPlugin.java deleted file mode 100644 index 919eb7add9ba..000000000000 --- a/packages/integration_test/android/src/main/java/dev/flutter/plugins/integration_test/IntegrationTestPlugin.java +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package dev.flutter.plugins.integration_test; - -import android.content.Context; -import com.google.common.util.concurrent.SettableFuture; -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.Map; -import java.util.concurrent.Future; - -/** IntegrationTestPlugin */ -public class IntegrationTestPlugin implements MethodCallHandler, FlutterPlugin { - private MethodChannel methodChannel; - - private static final SettableFuture> testResultsSettable = - SettableFuture.create(); - public static final Future> testResults = testResultsSettable; - - private static final String CHANNEL = "plugins.flutter.io/integration_test"; - - /** Plugin registration. */ - @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { - final IntegrationTestPlugin instance = new IntegrationTestPlugin(); - instance.onAttachedToEngine(registrar.context(), registrar.messenger()); - } - - @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { - onAttachedToEngine(binding.getApplicationContext(), binding.getBinaryMessenger()); - } - - private void onAttachedToEngine(Context unusedApplicationContext, BinaryMessenger messenger) { - methodChannel = new MethodChannel(messenger, CHANNEL); - methodChannel.setMethodCallHandler(this); - } - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { - methodChannel.setMethodCallHandler(null); - methodChannel = null; - } - - @Override - public void onMethodCall(MethodCall call, Result result) { - if (call.method.equals("allTestsFinished")) { - Map results = call.argument("results"); - testResultsSettable.set(results); - result.success(null); - } else { - result.notImplemented(); - } - } -} diff --git a/packages/integration_test/example/.gitignore b/packages/integration_test/example/.gitignore deleted file mode 100644 index 2ddde2a5e36f..000000000000 --- a/packages/integration_test/example/.gitignore +++ /dev/null @@ -1,73 +0,0 @@ -# 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/ -.dart_tool/ -.flutter-plugins -.packages -.pub-cache/ -.pub/ -/build/ - -# Android related -**/android/**/gradle-wrapper.jar -**/android/.gradle -**/android/captures/ -**/android/gradlew -**/android/gradlew.bat -**/android/local.properties -**/android/**/GeneratedPluginRegistrant.java - -# iOS/XCode related -**/ios/**/*.mode1v3 -**/ios/**/*.mode2v3 -**/ios/**/*.moved-aside -**/ios/**/*.pbxuser -**/ios/**/*.perspectivev3 -**/ios/**/*sync/ -**/ios/**/.sconsign.dblite -**/ios/**/.tags* -**/ios/**/.vagrant/ -**/ios/**/DerivedData/ -**/ios/**/Icon? -**/ios/**/Pods/ -**/ios/**/.symlinks/ -**/ios/**/profile -**/ios/**/xcuserdata -**/ios/.generated/ -**/ios/Flutter/App.framework -**/ios/Flutter/Flutter.framework -**/ios/Flutter/Generated.xcconfig -**/ios/Flutter/app.flx -**/ios/Flutter/app.zip -**/ios/Flutter/flutter_assets/ -**/ios/Flutter/flutter_export_environment.sh -**/ios/ServiceDefinitions.json -**/ios/Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!**/ios/**/default.mode1v3 -!**/ios/**/default.mode2v3 -!**/ios/**/default.pbxuser -!**/ios/**/default.perspectivev3 -!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/packages/integration_test/example/.metadata b/packages/integration_test/example/.metadata deleted file mode 100644 index 76f3d14cd0f9..000000000000 --- a/packages/integration_test/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: 4fc11db5cc50345b7d64cb4015eb9a92229f6384 - channel: master - -project_type: app diff --git a/packages/integration_test/example/README.md b/packages/integration_test/example/README.md deleted file mode 100644 index b5cc5d77e46f..000000000000 --- a/packages/integration_test/example/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# integration_test_example - -Demonstrates how to use the `package:integration_test`. - -To run `integration_test/example_test.dart`, - -Android / iOS: - -```sh -flutter drive \ - --driver=test_driver/integration_test.dart \ - --target=integration_test/example_test.dart -``` - -Web: - -```sh -flutter drive \ - --driver=test_driver/integration_test.dart \ - --target=integration_test/example_test.dart \ - -d web-server -``` diff --git a/packages/integration_test/example/android/app/build.gradle b/packages/integration_test/example/android/app/build.gradle deleted file mode 100644 index 73bd5f4bc41e..000000000000 --- a/packages/integration_test/example/android/app/build.gradle +++ /dev/null @@ -1,61 +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 28 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.example.integration_test_example" - 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.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' -} diff --git a/packages/integration_test/example/android/app/src/androidTest/java/com/example/e2e_example/EmbedderV1ActivityTest.java b/packages/integration_test/example/android/app/src/androidTest/java/com/example/e2e_example/EmbedderV1ActivityTest.java deleted file mode 100644 index 0ce7dc14d4a5..000000000000 --- a/packages/integration_test/example/android/app/src/androidTest/java/com/example/e2e_example/EmbedderV1ActivityTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.integration_test_example; - -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 EmbedderV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbedderV1Activity.class, true, false); -} diff --git a/packages/integration_test/example/android/app/src/androidTest/java/com/example/e2e_example/FlutterActivityTest.java b/packages/integration_test/example/android/app/src/androidTest/java/com/example/e2e_example/FlutterActivityTest.java deleted file mode 100644 index 36ae1ddfc7e8..000000000000 --- a/packages/integration_test/example/android/app/src/androidTest/java/com/example/e2e_example/FlutterActivityTest.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.example.integration_test_example; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import io.flutter.embedding.android.FlutterActivity; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -public class FlutterActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(FlutterActivity.class, true, false); -} diff --git a/packages/integration_test/example/android/app/src/androidTest/java/com/example/e2e_example/FlutterActivityWithPermissionTest.java b/packages/integration_test/example/android/app/src/androidTest/java/com/example/e2e_example/FlutterActivityWithPermissionTest.java deleted file mode 100644 index c01d23466fed..000000000000 --- a/packages/integration_test/example/android/app/src/androidTest/java/com/example/e2e_example/FlutterActivityWithPermissionTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.example.integration_test_example; - -import android.Manifest.permission; -import androidx.test.rule.ActivityTestRule; -import androidx.test.rule.GrantPermissionRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import io.flutter.embedding.android.FlutterActivity; -import org.junit.Rule; -import org.junit.runner.RunWith; - -/** - * Demonstrates how an integration test on Android can be run with permissions already granted. This - * is helpful if developers want to test native App behavior that depends on certain system service - * results which are guarded with permissions. - */ -@RunWith(FlutterTestRunner.class) -public class FlutterActivityWithPermissionTest { - - @Rule - public GrantPermissionRule permissionRule = - GrantPermissionRule.grant(permission.ACCESS_COARSE_LOCATION); - - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(FlutterActivity.class, true, false); -} diff --git a/packages/integration_test/example/android/app/src/debug/AndroidManifest.xml b/packages/integration_test/example/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 04d0fdb2c153..000000000000 --- a/packages/integration_test/example/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/packages/integration_test/example/android/app/src/main/AndroidManifest.xml b/packages/integration_test/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index d789e0e15faf..000000000000 --- a/packages/integration_test/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/packages/integration_test/example/android/app/src/main/java/com/example/e2e_example/EmbedderV1Activity.java b/packages/integration_test/example/android/app/src/main/java/com/example/e2e_example/EmbedderV1Activity.java deleted file mode 100644 index 9318e457800d..000000000000 --- a/packages/integration_test/example/android/app/src/main/java/com/example/e2e_example/EmbedderV1Activity.java +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package com.example.integration_test_example; - -import android.os.Bundle; -import dev.flutter.plugins.integration_test.IntegrationTestPlugin; -import io.flutter.app.FlutterActivity; - -public class EmbedderV1Activity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - IntegrationTestPlugin.registerWith( - registrarFor("dev.flutter.plugins.integration_test.IntegrationTestPlugin")); - } -} diff --git a/packages/integration_test/example/android/app/src/profile/AndroidManifest.xml b/packages/integration_test/example/android/app/src/profile/AndroidManifest.xml deleted file mode 100644 index 04d0fdb2c153..000000000000 --- a/packages/integration_test/example/android/app/src/profile/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/packages/integration_test/example/android/build.gradle b/packages/integration_test/example/android/build.gradle deleted file mode 100644 index 11e3d0901a2e..000000000000 --- a/packages/integration_test/example/android/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.6.3' - } -} - -allprojects { - repositories { - google() - jcenter() - } -} - -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/integration_test/example/android/gradle.properties b/packages/integration_test/example/android/gradle.properties deleted file mode 100644 index 755300e3a0b5..000000000000 --- a/packages/integration_test/example/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M - -android.useAndroidX=true -android.enableJetifier=true diff --git a/packages/integration_test/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/integration_test/example/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index bc24dcf039a5..000000000000 --- a/packages/integration_test/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.4-all.zip diff --git a/packages/integration_test/example/integration_test/_example_test_io.dart b/packages/integration_test/example/integration_test/_example_test_io.dart deleted file mode 100644 index 7ed28963c32b..000000000000 --- a/packages/integration_test/example/integration_test/_example_test_io.dart +++ /dev/null @@ -1,41 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'dart:io' show Platform; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import 'package:integration_test_example/main.dart' as app; - -void main() { - final IntegrationTestWidgetsFlutterBinding binding = - IntegrationTestWidgetsFlutterBinding.ensureInitialized() - as IntegrationTestWidgetsFlutterBinding; - testWidgets('verify text', (WidgetTester tester) async { - // Build our app and trigger a frame. - app.main(); - - // Trace the timeline of the following operation. The timeline result will - // be written to `build/integration_response_data.json` with the key - // `timeline`. - await binding.traceAction(() async { - // Trigger a frame. - await tester.pumpAndSettle(); - - // Verify that platform version is retrieved. - expect( - find.byWidgetPredicate( - (Widget widget) => - widget is Text && - widget.data.startsWith('Platform: ${Platform.operatingSystem}'), - ), - findsOneWidget, - ); - }); - }); -} diff --git a/packages/integration_test/example/integration_test/_example_test_web.dart b/packages/integration_test/example/integration_test/_example_test_web.dart deleted file mode 100644 index e1141cc010c8..000000000000 --- a/packages/integration_test/example/integration_test/_example_test_web.dart +++ /dev/null @@ -1,35 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'dart:html' as html; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import 'package:integration_test_example/main.dart' as app; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - testWidgets('verify text', (WidgetTester tester) async { - // Build our app and trigger a frame. - app.main(); - - // Trigger a frame. - await tester.pumpAndSettle(); - - // Verify that platform is retrieved. - expect( - find.byWidgetPredicate( - (Widget widget) => - widget is Text && - widget.data - .startsWith('Platform: ${html.window.navigator.platform}\n'), - ), - findsOneWidget, - ); - }); -} diff --git a/packages/integration_test/example/integration_test/_extended_test_io.dart b/packages/integration_test/example/integration_test/_extended_test_io.dart deleted file mode 100644 index 56fee6f7179c..000000000000 --- a/packages/integration_test/example/integration_test/_extended_test_io.dart +++ /dev/null @@ -1,38 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'dart:io' show Platform; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import 'package:integration_test_example/main.dart' as app; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('verify text', (WidgetTester tester) async { - // Build our app and trigger a frame. - app.main(); - - // Trigger a frame. - await tester.pumpAndSettle(); - - // TODO: https://github.com/flutter/flutter/issues/51890 - // Add screenshot capability for mobile platforms. - - // Verify that platform version is retrieved. - expect( - find.byWidgetPredicate( - (Widget widget) => - widget is Text && - widget.data.startsWith('Platform: ${Platform.operatingSystem}'), - ), - findsOneWidget, - ); - }); -} diff --git a/packages/integration_test/example/integration_test/_extended_test_web.dart b/packages/integration_test/example/integration_test/_extended_test_web.dart deleted file mode 100644 index 210c2dac75ba..000000000000 --- a/packages/integration_test/example/integration_test/_extended_test_web.dart +++ /dev/null @@ -1,52 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'dart:html' as html; -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import 'package:integration_test_example/main.dart' as app; - -void main() { - final IntegrationTestWidgetsFlutterBinding binding = - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('verify text', (WidgetTester tester) async { - // Build our app and trigger a frame. - app.main(); - - // Trigger a frame. - await tester.pumpAndSettle(); - - // Take a screenshot. - await binding.takeScreenshot('platform_name'); - - // Verify that platform is retrieved. - expect( - find.byWidgetPredicate( - (Widget widget) => - widget is Text && - widget.data - .startsWith('Platform: ${html.window.navigator.platform}\n'), - ), - findsOneWidget, - ); - }); - - testWidgets('verify screenshot', (WidgetTester tester) async { - // Build our app and trigger a frame. - app.main(); - - // Trigger a frame. - await tester.pumpAndSettle(); - - // Multiple methods can take screenshots. Screenshots are taken with the - // same order the methods run. - await binding.takeScreenshot('platform_name_2'); - }); -} diff --git a/packages/integration_test/example/integration_test/example_test.dart b/packages/integration_test/example/integration_test/example_test.dart deleted file mode 100644 index 918aec8777de..000000000000 --- a/packages/integration_test/example/integration_test/example_test.dart +++ /dev/null @@ -1,16 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:integration_test/integration_test.dart'; - -import '_example_test_io.dart' if (dart.library.html) '_example_test_web.dart' - as tests; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - tests.main(); -} diff --git a/packages/integration_test/example/integration_test/extended_test.dart b/packages/integration_test/example/integration_test/extended_test.dart deleted file mode 100644 index 23d69a8f9438..000000000000 --- a/packages/integration_test/example/integration_test/extended_test.dart +++ /dev/null @@ -1,19 +0,0 @@ -// This is a Flutter widget test can take a screenshot. -// -// NOTE: Screenshots are only supported on Web for now. For Web, this needs to -// be executed with the `test_driver/integration_test_extended_driver.dart`. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:integration_test/integration_test.dart'; - -import '_extended_test_io.dart' if (dart.library.html) '_extended_test_web.dart' - as tests; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - tests.main(); -} diff --git a/packages/integration_test/example/ios/Flutter/AppFrameworkInfo.plist b/packages/integration_test/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6b4c0f78a785..000000000000 --- a/packages/integration_test/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 8.0 - - diff --git a/packages/integration_test/example/ios/Flutter/Debug.xcconfig b/packages/integration_test/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index e8efba114687..000000000000 --- a/packages/integration_test/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/integration_test/example/ios/Flutter/Release.xcconfig b/packages/integration_test/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index 399e9340e6f6..000000000000 --- a/packages/integration_test/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/integration_test/example/ios/Runner.xcodeproj/project.pbxproj b/packages/integration_test/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index b96fa2fb2618..000000000000 --- a/packages/integration_test/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,733 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 769541CB23A0351900E5C350 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 769541CA23A0351900E5C350 /* RunnerTests.m */; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - C2A5EDF11F4FDBF3ABFD7006 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 625A5A90428602E25C0DE2F6 /* libPods-Runner.a */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 769541CD23A0351900E5C350 /* 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 = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 0D6F1CB5DBBEBCC75AFAD041 /* 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 = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; - 625A5A90428602E25C0DE2F6 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 769541BF23A0337200E5C350 /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; - 769541C823A0351900E5C350 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 769541CA23A0351900E5C350 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = ""; }; - 769541CC23A0351900E5C350 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 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 = ""; }; - D69CCAD5F82E76E2E22BFA96 /* 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 = ""; }; - E23EF4D45DAE46B9DDB9B445 /* 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 = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 769541C523A0351900E5C350 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - C2A5EDF11F4FDBF3ABFD7006 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 42D734D13B733A64B01A24A9 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 769541BF23A0337200E5C350 /* XCTest.framework */, - 625A5A90428602E25C0DE2F6 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; - 769541C923A0351900E5C350 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 769541CA23A0351900E5C350 /* RunnerTests.m */, - 769541CC23A0351900E5C350 /* Info.plist */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B80C3931E831B6300D905FE /* App.framework */, - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 769541C923A0351900E5C350 /* RunnerTests */, - 97C146EF1CF9000F007C117D /* Products */, - BAB55133DD7BD81A2557E916 /* Pods */, - 42D734D13B733A64B01A24A9 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - 769541C823A0351900E5C350 /* 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 = ""; - }; - BAB55133DD7BD81A2557E916 /* Pods */ = { - isa = PBXGroup; - children = ( - D69CCAD5F82E76E2E22BFA96 /* Pods-Runner.debug.xcconfig */, - 0D6F1CB5DBBEBCC75AFAD041 /* Pods-Runner.release.xcconfig */, - E23EF4D45DAE46B9DDB9B445 /* Pods-Runner.profile.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 769541C723A0351900E5C350 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 769541CF23A0351900E5C350 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - 769541C423A0351900E5C350 /* Sources */, - 769541C523A0351900E5C350 /* Frameworks */, - 769541C623A0351900E5C350 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 769541CE23A0351900E5C350 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 769541C823A0351900E5C350 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 2882CCC16181B61F1ABC876C /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 0D321280D358770769172C49 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1020; - ORGANIZATIONNAME = "The Chromium Authors"; - TargetAttributes = { - 769541C723A0351900E5C350 = { - CreatedOnToolsVersion = 11.0; - 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 */, - 769541C723A0351900E5C350 /* RunnerTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 769541C623A0351900E5C350 /* 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 */ - 0D321280D358770769172C49 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 2882CCC16181B61F1ABC876C /* [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; - }; - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 769541C423A0351900E5C350 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 769541CB23A0351900E5C350 /* RunnerTests.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 */ - 769541CE23A0351900E5C350 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 769541CD23A0351900E5C350 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.instrumentationAdapterExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Profile; - }; - 769541D023A0351900E5C350 /* Debug */ = { - isa = XCBuildConfiguration; - 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 = 13.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.example.instrumentationAdapterExample.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; - }; - name = Debug; - }; - 769541D123A0351900E5C350 /* Release */ = { - isa = XCBuildConfiguration; - 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 = 13.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.example.instrumentationAdapterExample.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; - }; - name = Release; - }; - 769541D223A0351900E5C350 /* Profile */ = { - isa = XCBuildConfiguration; - 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 = 13.0; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.example.instrumentationAdapterExample.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; - }; - name = Profile; - }; - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.instrumentationAdapterExample; - 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 = com.example.instrumentationAdapterExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 769541CF23A0351900E5C350 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 769541D023A0351900E5C350 /* Debug */, - 769541D123A0351900E5C350 /* Release */, - 769541D223A0351900E5C350 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 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/integration_test/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/integration_test/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 72fa1469f5e1..000000000000 --- a/packages/integration_test/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/integration_test/example/ios/Runner/AppDelegate.h b/packages/integration_test/example/ios/Runner/AppDelegate.h deleted file mode 100644 index 36e21bbf9cf4..000000000000 --- a/packages/integration_test/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,6 +0,0 @@ -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/integration_test/example/ios/Runner/AppDelegate.m b/packages/integration_test/example/ios/Runner/AppDelegate.m deleted file mode 100644 index 59a72e90be12..000000000000 --- a/packages/integration_test/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,13 +0,0 @@ -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d36b1fab2d9d..000000000000 --- a/packages/integration_test/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/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index dc9ada4725e9..000000000000 Binary files a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ diff --git a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf03016f..000000000000 Binary files a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca8..000000000000 Binary files a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cde12118dda..000000000000 Binary files a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb8..000000000000 Binary files a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index dcdc2306c285..000000000000 Binary files a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee..000000000000 Binary files a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a9..000000000000 Binary files a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7..000000000000 Binary files a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 6a84f41e14e2..000000000000 Binary files a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index d0e1f5853602..000000000000 Binary files a/packages/integration_test/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/packages/integration_test/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/integration_test/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json deleted file mode 100644 index 0bedcf2fd467..000000000000 --- a/packages/integration_test/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "LaunchImage.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/integration_test/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/integration_test/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png deleted file mode 100644 index 9da19eacad3b..000000000000 Binary files a/packages/integration_test/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and /dev/null differ diff --git a/packages/integration_test/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/integration_test/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100644 index 9da19eacad3b..000000000000 Binary files a/packages/integration_test/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and /dev/null differ diff --git a/packages/integration_test/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/integration_test/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100644 index 9da19eacad3b..000000000000 Binary files a/packages/integration_test/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and /dev/null differ diff --git a/packages/integration_test/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/integration_test/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725b70f1..000000000000 --- a/packages/integration_test/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/packages/integration_test/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/integration_test/example/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f2e259c7c939..000000000000 --- a/packages/integration_test/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/integration_test/example/ios/Runner/Base.lproj/Main.storyboard b/packages/integration_test/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516fb38..000000000000 --- a/packages/integration_test/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/integration_test/example/ios/Runner/Info.plist b/packages/integration_test/example/ios/Runner/Info.plist deleted file mode 100644 index d0e099be56e7..000000000000 --- a/packages/integration_test/example/ios/Runner/Info.plist +++ /dev/null @@ -1,45 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - integration_test_example - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/integration_test/example/ios/Runner/main.m b/packages/integration_test/example/ios/Runner/main.m deleted file mode 100644 index dff6597e4513..000000000000 --- a/packages/integration_test/example/ios/Runner/main.m +++ /dev/null @@ -1,9 +0,0 @@ -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/integration_test/example/ios/RunnerTests/RunnerTests.m b/packages/integration_test/example/ios/RunnerTests/RunnerTests.m deleted file mode 100644 index ac89c60e5f06..000000000000 --- a/packages/integration_test/example/ios/RunnerTests/RunnerTests.m +++ /dev/null @@ -1,4 +0,0 @@ -#import -#import - -INTEGRATION_TEST_IOS_RUNNER(RunnerTests) diff --git a/packages/integration_test/example/lib/main.dart b/packages/integration_test/example/lib/main.dart deleted file mode 100644 index 1f33324acd01..000000000000 --- a/packages/integration_test/example/lib/main.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'my_app.dart' if (dart.library.html) 'my_web_app.dart'; - -// ignore_for_file: public_member_api_docs - -void main() => startApp(); diff --git a/packages/integration_test/example/lib/my_app.dart b/packages/integration_test/example/lib/my_app.dart deleted file mode 100644 index bfbdb860c76d..000000000000 --- a/packages/integration_test/example/lib/my_app.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'dart:io' show Platform; -import 'package:flutter/material.dart'; - -// ignore_for_file: public_member_api_docs - -void startApp() => runApp(MyApp()); - -class MyApp extends StatefulWidget { - @override - _MyAppState createState() => _MyAppState(); -} - -class _MyAppState extends State { - @override - Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Plugin example app'), - ), - body: Center( - child: Text('Platform: ${Platform.operatingSystem}\n'), - ), - ), - ); - } -} diff --git a/packages/integration_test/example/lib/my_web_app.dart b/packages/integration_test/example/lib/my_web_app.dart deleted file mode 100644 index c2ced1af97ae..000000000000 --- a/packages/integration_test/example/lib/my_web_app.dart +++ /dev/null @@ -1,28 +0,0 @@ -import 'dart:html' as html; -import 'package:flutter/material.dart'; - -// ignore_for_file: public_member_api_docs - -void startApp() => runApp(MyWebApp()); - -class MyWebApp extends StatefulWidget { - @override - _MyWebAppState createState() => _MyWebAppState(); -} - -class _MyWebAppState extends State { - @override - Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Plugin example app'), - ), - body: Center( - key: Key('mainapp'), - child: Text('Platform: ${html.window.navigator.platform}\n'), - ), - ), - ); - } -} diff --git a/packages/integration_test/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/integration_test/example/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index 785633d3a86b..000000000000 --- a/packages/integration_test/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/integration_test/example/macos/Flutter/Flutter-Release.xcconfig b/packages/integration_test/example/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index 5fba960c3af2..000000000000 --- a/packages/integration_test/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/integration_test/example/macos/Runner.xcodeproj/project.pbxproj b/packages/integration_test/example/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 718462e3b4c5..000000000000 --- a/packages/integration_test/example/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,656 +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, ); }; }; - B7C0D6D07EB453D3AC9C81F2 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A2D6D92F7F9105EA5B2C12C6 /* Pods_Runner.framework */; }; - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; }; - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 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 */ - 2A162B3576CC7562C04C8319 /* 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 /* integration_test_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = integration_test_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 = ""; }; - 710A00C7116252C03437F6D9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - 97614001DA7FEA4B30ABAB1F /* 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 = ""; }; - A2D6D92F7F9105EA5B2C12C6 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - D73912EF22F37F9E000D13A0 /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/ephemeral/App.framework; sourceTree = SOURCE_ROOT; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */, - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */, - B7C0D6D07EB453D3AC9C81F2 /* Pods_Runner.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 2B3E7A30398192ADA2835AB3 /* Pods */ = { - isa = PBXGroup; - children = ( - 710A00C7116252C03437F6D9 /* Pods-Runner.debug.xcconfig */, - 2A162B3576CC7562C04C8319 /* Pods-Runner.release.xcconfig */, - 97614001DA7FEA4B30ABAB1F /* Pods-Runner.profile.xcconfig */, - ); - path = Pods; - 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 */, - 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - 2B3E7A30398192ADA2835AB3 /* Pods */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* integration_test_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 = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - A2D6D92F7F9105EA5B2C12C6 /* 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 = ( - 10BA06117B193C37CD021555 /* [CP] Check Pods Manifest.lock */, - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - FC41FCCE1DD077B5F6ABF89F /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* integration_test_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 */ - 10BA06117B193C37CD021555 /* [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; - }; - 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"; - }; - FC41FCCE1DD077B5F6ABF89F /* [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; - }; -/* 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", - ); - MACOSX_DEPLOYMENT_TARGET = 10.11; - 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", - ); - MACOSX_DEPLOYMENT_TARGET = 10.11; - 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", - ); - MACOSX_DEPLOYMENT_TARGET = 10.11; - 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/integration_test/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/integration_test/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 464e05225e4c..000000000000 --- a/packages/integration_test/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/integration_test/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/integration_test/example/macos/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a16ed0f..000000000000 --- a/packages/integration_test/example/macos/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/packages/integration_test/example/macos/Runner/AppDelegate.swift b/packages/integration_test/example/macos/Runner/AppDelegate.swift deleted file mode 100644 index d53ef6437726..000000000000 --- a/packages/integration_test/example/macos/Runner/AppDelegate.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Cocoa -import FlutterMacOS - -@NSApplicationMain -class AppDelegate: FlutterAppDelegate { - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } -} diff --git a/packages/integration_test/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/integration_test/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index a2ec33f19f11..000000000000 --- a/packages/integration_test/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/integration_test/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/integration_test/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 3c4935a7ca84..000000000000 Binary files a/packages/integration_test/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null differ diff --git a/packages/integration_test/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/integration_test/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index ed4cc1642168..000000000000 Binary files a/packages/integration_test/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null differ diff --git a/packages/integration_test/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/integration_test/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index 483be6138973..000000000000 Binary files a/packages/integration_test/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null differ diff --git a/packages/integration_test/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/integration_test/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png deleted file mode 100644 index bcbf36df2f2a..000000000000 Binary files a/packages/integration_test/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null differ diff --git a/packages/integration_test/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/integration_test/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png deleted file mode 100644 index 9c0a65286476..000000000000 Binary files a/packages/integration_test/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null differ diff --git a/packages/integration_test/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/integration_test/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png deleted file mode 100644 index e71a726136a4..000000000000 Binary files a/packages/integration_test/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null differ diff --git a/packages/integration_test/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/integration_test/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 8a31fe2dd3f9..000000000000 Binary files a/packages/integration_test/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ diff --git a/packages/integration_test/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/integration_test/example/macos/Runner/Base.lproj/MainMenu.xib deleted file mode 100644 index 537341abf994..000000000000 --- a/packages/integration_test/example/macos/Runner/Base.lproj/MainMenu.xib +++ /dev/null @@ -1,339 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/integration_test/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/integration_test/example/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index 1d9e2f0e043c..000000000000 --- a/packages/integration_test/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 = integration_test_example - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.integrationTestExample - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2019 com.example. All rights reserved. diff --git a/packages/integration_test/example/macos/Runner/Configs/Debug.xcconfig b/packages/integration_test/example/macos/Runner/Configs/Debug.xcconfig deleted file mode 100644 index 36b0fd9464f4..000000000000 --- a/packages/integration_test/example/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/packages/integration_test/example/macos/Runner/Configs/Release.xcconfig b/packages/integration_test/example/macos/Runner/Configs/Release.xcconfig deleted file mode 100644 index dff4f49561c8..000000000000 --- a/packages/integration_test/example/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/packages/integration_test/example/macos/Runner/Configs/Warnings.xcconfig b/packages/integration_test/example/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100644 index 42bcbf4780b1..000000000000 --- a/packages/integration_test/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/integration_test/example/macos/Runner/DebugProfile.entitlements b/packages/integration_test/example/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index dddb8a30c851..000000000000 --- a/packages/integration_test/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/integration_test/example/macos/Runner/Info.plist b/packages/integration_test/example/macos/Runner/Info.plist deleted file mode 100644 index 4789daa6a443..000000000000 --- a/packages/integration_test/example/macos/Runner/Info.plist +++ /dev/null @@ -1,32 +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 - - diff --git a/packages/integration_test/example/macos/Runner/MainFlutterWindow.swift b/packages/integration_test/example/macos/Runner/MainFlutterWindow.swift deleted file mode 100644 index 2722837ec918..000000000000 --- a/packages/integration_test/example/macos/Runner/MainFlutterWindow.swift +++ /dev/null @@ -1,15 +0,0 @@ -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/integration_test/example/macos/Runner/Release.entitlements b/packages/integration_test/example/macos/Runner/Release.entitlements deleted file mode 100644 index 852fa1a4728a..000000000000 --- a/packages/integration_test/example/macos/Runner/Release.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.app-sandbox - - - diff --git a/packages/integration_test/example/pubspec.yaml b/packages/integration_test/example/pubspec.yaml deleted file mode 100644 index 9384e9763935..000000000000 --- a/packages/integration_test/example/pubspec.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: integration_test_example -description: Demonstrates how to use the integration_test plugin. -publish_to: 'none' - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.6.7 <2.0.0" - -dependencies: - flutter: - sdk: flutter - - cupertino_icons: ^0.1.2 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_driver: - sdk: flutter - integration_test: - path: ../ - integration_test_macos: - path: ../integration_test_macos - test: any - pedantic: ^1.8.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -flutter: - uses-material-design: true diff --git a/packages/integration_test/example/test_driver/extended_integration_test.dart b/packages/integration_test/example/test_driver/extended_integration_test.dart deleted file mode 100644 index 056ba4bad722..000000000000 --- a/packages/integration_test/example/test_driver/extended_integration_test.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter_driver/flutter_driver.dart'; -import 'package:integration_test/integration_test_driver_extended.dart'; - -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - await integrationDriver( - driver: driver, - onScreenshot: (String screenshotName, List screenshotBytes) async { - return true; - }, - ); -} diff --git a/packages/integration_test/example/test_driver/failure.dart b/packages/integration_test/example/test_driver/failure.dart deleted file mode 100644 index 02fc55e94a53..000000000000 --- a/packages/integration_test/example/test_driver/failure.dart +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import 'package:integration_test_example/main.dart' as app; - -/// This file is placed in `test_driver/` instead of `integration_test/`, so -/// that the CI tooling of flutter/plugins only uses this together with -/// `failure_test.dart` as the driver. It is only used for testing of -/// `package:integration_test` – do not follow the conventions here if you are a -/// user of `package:integration_test`. - -// Tests the failure behavior of the IntegrationTestWidgetsFlutterBinding -// -// This test fails intentionally! It should be run using a test runner that -// expects failure. -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('success', (WidgetTester tester) async { - expect(1 + 1, 2); // This should pass - }); - - testWidgets('failure 1', (WidgetTester tester) async { - // Build our app and trigger a frame. - app.main(); - - // Verify that platform version is retrieved. - await expectLater( - find.byWidgetPredicate( - (Widget widget) => - widget is Text && widget.data.startsWith('This should fail'), - ), - findsOneWidget, - ); - }); - - testWidgets('failure 2', (WidgetTester tester) async { - expect(1 + 1, 3); // This should fail - }); -} diff --git a/packages/integration_test/example/test_driver/failure_test.dart b/packages/integration_test/example/test_driver/failure_test.dart deleted file mode 100644 index fce6adc42c92..000000000000 --- a/packages/integration_test/example/test_driver/failure_test.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:flutter_driver/flutter_driver.dart'; -import 'package:integration_test/common.dart' as common; -import 'package:test/test.dart'; - -/// This file is only used for testing of `package:integration_test` – do not -/// follow the conventions here if you are a user of `package:integration_test`. - -Future main() async { - test('fails gracefully', () async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String jsonResult = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - common.Response response = common.Response.fromJson(jsonResult); - await driver.close(); - expect( - response.allTestsPassed, - false, - ); - }); -} diff --git a/packages/integration_test/example/test_driver/integration_test.dart b/packages/integration_test/example/test_driver/integration_test.dart deleted file mode 100644 index b38629cca97b..000000000000 --- a/packages/integration_test/example/test_driver/integration_test.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'package:integration_test/integration_test_driver.dart'; - -Future main() => integrationDriver(); diff --git a/packages/integration_test/example/web/index.html b/packages/integration_test/example/web/index.html deleted file mode 100644 index 96629657328f..000000000000 --- a/packages/integration_test/example/web/index.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - example - - - - - - - - diff --git a/packages/integration_test/example/web/manifest.json b/packages/integration_test/example/web/manifest.json deleted file mode 100644 index c63800102369..000000000000 --- a/packages/integration_test/example/web/manifest.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "example", - "short_name": "example", - "start_url": ".", - "display": "minimal-ui", - "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/integration_test/integration_test_macos/CHANGELOG.md b/packages/integration_test/integration_test_macos/CHANGELOG.md deleted file mode 100644 index ab4bac1a3f3f..000000000000 --- a/packages/integration_test/integration_test_macos/CHANGELOG.md +++ /dev/null @@ -1,11 +0,0 @@ -## 0.0.2 - -* Renames package to integration_test_macos. - -## 0.0.1+1 - -* Remove Android folder from `e2e_macos`. - -## 0.0.1 - -* Initial release diff --git a/packages/integration_test/integration_test_macos/LICENSE b/packages/integration_test/integration_test_macos/LICENSE deleted file mode 100644 index 507569823f1b..000000000000 --- a/packages/integration_test/integration_test_macos/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright 2019 The Chromium Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/integration_test/integration_test_macos/ios/integration_test_macos.podspec b/packages/integration_test/integration_test_macos/ios/integration_test_macos.podspec deleted file mode 100644 index 7294590a6479..000000000000 --- a/packages/integration_test/integration_test_macos/ios/integration_test_macos.podspec +++ /dev/null @@ -1,21 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'IntegrationTestMacOS' - s.version = '0.0.1' - s.summary = 'No-op implementation of the integration_test desktop plugin to avoid build issues on iOS' - s.description = <<-DESC - No-op implementation of integration to avoid build issues on iOS. - See https://github.com/flutter/flutter/issues/39659 - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/integration_test/integration_test_macos' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.ios.deployment_target = '8.0' -end \ No newline at end of file diff --git a/packages/integration_test/integration_test_macos/macos/Assets/.gitkeep b/packages/integration_test/integration_test_macos/macos/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/integration_test/integration_test_macos/macos/Classes/IntegrationTestPlugin.swift b/packages/integration_test/integration_test_macos/macos/Classes/IntegrationTestPlugin.swift deleted file mode 100644 index b4eb5fc410d5..000000000000 --- a/packages/integration_test/integration_test_macos/macos/Classes/IntegrationTestPlugin.swift +++ /dev/null @@ -1,22 +0,0 @@ -import FlutterMacOS - -public class IntegrationTestPlugin: NSObject, FlutterPlugin { - - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel( - name: "plugins.flutter.io/integration_test", - binaryMessenger: registrar.messenger) - - let instance = IntegrationTestPlugin() - registrar.addMethodCallDelegate(instance, channel: channel) - } - - public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - switch call.method { - case "allTestsFinished": - result(nil) - default: - result(FlutterMethodNotImplemented) - } - } -} diff --git a/packages/integration_test/integration_test_macos/macos/integration_test_macos.podspec b/packages/integration_test/integration_test_macos/macos/integration_test_macos.podspec deleted file mode 100644 index 74bf66321319..000000000000 --- a/packages/integration_test/integration_test_macos/macos/integration_test_macos.podspec +++ /dev/null @@ -1,21 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'IntegrationTestMacOS' - s.version = '0.0.1' - s.summary = 'Adapter for integration tests.' - s.description = <<-DESC -Runs tests that use the flutter_test API as integration tests on macOS. - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/integration_test/integration_test_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/integration_test' } - s.source_files = 'Classes/**/*' - s.dependency 'FlutterMacOS' - - s.platform = :osx, '10.11' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } -end - diff --git a/packages/integration_test/integration_test_macos/pubspec.yaml b/packages/integration_test/integration_test_macos/pubspec.yaml deleted file mode 100644 index 46510e32b94a..000000000000 --- a/packages/integration_test/integration_test_macos/pubspec.yaml +++ /dev/null @@ -1,21 +0,0 @@ -name: integration_test_macos -description: Desktop implementation of integration_test plugin -version: 0.0.1+1 -author: Flutter Team -homepage: https://github.com/flutter/plugins/tree/master/packages/integration_test/integration_test_macos - -flutter: - plugin: - platforms: - macos: - pluginClass: IntegrationTestPlugin - -environment: - sdk: ">=2.1.0 <3.0.0" - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - pedantic: ^1.8.0 diff --git a/packages/integration_test/ios/.gitignore b/packages/integration_test/ios/.gitignore deleted file mode 100644 index aa479fd3ce8a..000000000000 --- a/packages/integration_test/ios/.gitignore +++ /dev/null @@ -1,37 +0,0 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -.generated/ - -*.pbxuser -*.mode1v3 -*.mode2v3 -*.perspectivev3 - -!default.pbxuser -!default.mode1v3 -!default.mode2v3 -!default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/Generated.xcconfig -/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/packages/integration_test/ios/Assets/.gitkeep b/packages/integration_test/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/integration_test/ios/Classes/IntegrationTestIosTest.h b/packages/integration_test/ios/Classes/IntegrationTestIosTest.h deleted file mode 100644 index 9c53edb160e9..000000000000 --- a/packages/integration_test/ios/Classes/IntegrationTestIosTest.h +++ /dev/null @@ -1,22 +0,0 @@ -#import - -@interface IntegrationTestIosTest : NSObject - -- (BOOL)testIntegrationTest:(NSString **)testResult; - -@end - -#define INTEGRATION_TEST_IOS_RUNNER(__test_class) \ - @interface __test_class : XCTestCase \ - @end \ - \ - @implementation __test_class \ - \ - -(void)testIntegrationTest { \ - NSString *testResult; \ - IntegrationTestIosTest *integrationTestIosTest = [[IntegrationTestIosTest alloc] init]; \ - BOOL testPass = [integrationTestIosTest testIntegrationTest:&testResult]; \ - XCTAssertTrue(testPass, @"%@", testResult); \ - } \ - \ - @end diff --git a/packages/integration_test/ios/Classes/IntegrationTestIosTest.m b/packages/integration_test/ios/Classes/IntegrationTestIosTest.m deleted file mode 100644 index 1397f547e6f6..000000000000 --- a/packages/integration_test/ios/Classes/IntegrationTestIosTest.m +++ /dev/null @@ -1,43 +0,0 @@ -#import "IntegrationTestIosTest.h" -#import "IntegrationTestPlugin.h" - -@implementation IntegrationTestIosTest - -- (BOOL)testIntegrationTest:(NSString **)testResult { - IntegrationTestPlugin *integrationTestPlugin = [IntegrationTestPlugin instance]; - UIViewController *rootViewController = - [[[[UIApplication sharedApplication] delegate] window] rootViewController]; - if (![rootViewController isKindOfClass:[FlutterViewController class]]) { - NSLog(@"expected FlutterViewController as rootViewController."); - return NO; - } - FlutterViewController *flutterViewController = (FlutterViewController *)rootViewController; - [integrationTestPlugin setupChannels:flutterViewController.engine.binaryMessenger]; - while (!integrationTestPlugin.testResults) { - CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.f, NO); - } - NSDictionary *testResults = integrationTestPlugin.testResults; - NSMutableArray *passedTests = [NSMutableArray array]; - NSMutableArray *failedTests = [NSMutableArray array]; - NSLog(@"==================== Test Results ====================="); - for (NSString *test in testResults.allKeys) { - NSString *result = testResults[test]; - if ([result isEqualToString:@"success"]) { - NSLog(@"%@ passed.", test); - [passedTests addObject:test]; - } else { - NSLog(@"%@ failed: %@", test, result); - [failedTests addObject:test]; - } - } - NSLog(@"================== Test Results End ===================="); - BOOL testPass = failedTests.count == 0; - if (!testPass && testResult) { - *testResult = - [NSString stringWithFormat:@"Detected failed integration test(s) %@ among %@", - failedTests.description, testResults.allKeys.description]; - } - return testPass; -} - -@end diff --git a/packages/integration_test/ios/Classes/IntegrationTestPlugin.h b/packages/integration_test/ios/Classes/IntegrationTestPlugin.h deleted file mode 100644 index 8dd3109ffe09..000000000000 --- a/packages/integration_test/ios/Classes/IntegrationTestPlugin.h +++ /dev/null @@ -1,25 +0,0 @@ -#import - -NS_ASSUME_NONNULL_BEGIN - -/** A Flutter plugin that's responsible for communicating the test results back - * to iOS XCTest. */ -@interface IntegrationTestPlugin : NSObject - -/** - * Test results that are sent from Dart when integration test completes. Before the - * completion, it is - * @c nil. - */ -@property(nonatomic, readonly, nullable) NSDictionary *testResults; - -/** Fetches the singleton instance of the plugin. */ -+ (IntegrationTestPlugin *)instance; - -- (void)setupChannels:(id)binaryMessenger; - -- (instancetype)init NS_UNAVAILABLE; - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/integration_test/ios/Classes/IntegrationTestPlugin.m b/packages/integration_test/ios/Classes/IntegrationTestPlugin.m deleted file mode 100644 index e7e5a74c01ee..000000000000 --- a/packages/integration_test/ios/Classes/IntegrationTestPlugin.m +++ /dev/null @@ -1,54 +0,0 @@ -#import "IntegrationTestPlugin.h" - -static NSString *const kIntegrationTestPluginChannel = @"plugins.flutter.io/integration_test"; -static NSString *const kMethodTestFinished = @"allTestsFinished"; - -@interface IntegrationTestPlugin () - -@property(nonatomic, readwrite) NSDictionary *testResults; - -@end - -@implementation IntegrationTestPlugin { - NSDictionary *_testResults; -} - -+ (IntegrationTestPlugin *)instance { - static dispatch_once_t onceToken; - static IntegrationTestPlugin *sInstance; - dispatch_once(&onceToken, ^{ - sInstance = [[IntegrationTestPlugin alloc] initForRegistration]; - }); - return sInstance; -} - -- (instancetype)initForRegistration { - return [super init]; -} - -+ (void)registerWithRegistrar:(NSObject *)registrar { - // No initialization happens here because of the way XCTest loads the testing - // bundles. Setup on static variables can be disregarded when a new static - // instance of IntegrationTestPlugin is allocated when the bundle is reloaded. - // See also: https://github.com/flutter/plugins/pull/2465 -} - -- (void)setupChannels:(id)binaryMessenger { - FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:kIntegrationTestPluginChannel - binaryMessenger:binaryMessenger]; - [channel setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) { - [self handleMethodCall:call result:result]; - }]; -} - -- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if ([kMethodTestFinished isEqual:call.method]) { - self.testResults = call.arguments[@"results"]; - result(nil); - } else { - result(FlutterMethodNotImplemented); - } -} - -@end diff --git a/packages/integration_test/ios/integration_test.podspec b/packages/integration_test/ios/integration_test.podspec deleted file mode 100644 index 9fb0dfc9d790..000000000000 --- a/packages/integration_test/ios/integration_test.podspec +++ /dev/null @@ -1,21 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'integration_test' - s.version = '0.0.1' - s.summary = 'Adapter for integration tests.' - s.description = <<-DESC -Runs tests that use the flutter_test API as integration tests. - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/integration_test' - 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/integration_test' } - 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/integration_test/lib/_callback_io.dart b/packages/integration_test/lib/_callback_io.dart deleted file mode 100644 index c1a447e27cab..000000000000 --- a/packages/integration_test/lib/_callback_io.dart +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'common.dart'; - -/// The dart:io implementation of [CallbackManager]. -/// -/// See also: -/// -/// * [_callback_web.dart], which has the dart:html implementation -CallbackManager get callbackManager => _singletonCallbackManager; - -/// IOCallbackManager singleton. -final IOCallbackManager _singletonCallbackManager = IOCallbackManager(); - -/// Manages communication between `integration_tests` and the `driver_tests`. -/// -/// This is the dart:io implementation. -class IOCallbackManager implements CallbackManager { - @override - Future> callback( - Map params, IntegrationTestResults testRunner) async { - final String command = params['command']; - Map response; - switch (command) { - case 'request_data': - final bool allTestsPassed = await testRunner.allTestsPassed.future; - response = { - 'message': allTestsPassed - ? Response.allTestsPassed(data: testRunner.reportData).toJson() - : Response.someTestsFailed( - testRunner.failureMethodsDetails, - data: testRunner.reportData, - ).toJson(), - }; - break; - case 'get_health': - response = {'status': 'ok'}; - break; - default: - throw UnimplementedError('$command is not implemented'); - } - return { - 'isError': false, - 'response': response, - }; - } - - @override - void cleanup() { - // no-op. - // Add any IO platform specific Completer/Future cleanups to here if any - // comes up in the future. For example: `WebCallbackManager.cleanup`. - } - - @override - Future takeScreenshot(String screenshot) { - throw UnimplementedError( - 'Screenshots are not implemented on this platform'); - } -} diff --git a/packages/integration_test/lib/_callback_web.dart b/packages/integration_test/lib/_callback_web.dart deleted file mode 100644 index 036098148d99..000000000000 --- a/packages/integration_test/lib/_callback_web.dart +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter_test/flutter_test.dart'; - -import 'common.dart'; - -/// The dart:html implementation of [CallbackManager]. -/// -/// See also: -/// -/// * [_callback_io.dart], which has the dart:io implementation -CallbackManager get callbackManager => _singletonWebDriverCommandManager; - -/// WebDriverCommandManager singleton. -final WebCallbackManager _singletonWebDriverCommandManager = - WebCallbackManager(); - -/// Manages communication between `integration_tests` and the `driver_tests`. -/// -/// Along with responding to callbacks from the driver side this calls enables -/// usage of Web Driver commands by sending [WebDriverCommand]s to driver side. -/// -/// Tests can execute an Web Driver commands such as `screenshot` using browsers' -/// WebDriver APIs. -/// -/// See: https://www.w3.org/TR/webdriver/ -class WebCallbackManager implements CallbackManager { - /// App side tests will put the command requests from WebDriver to this pipe. - Completer _webDriverCommandPipe = - Completer(); - - /// Updated when WebDriver completes the request by the test method. - /// - /// For example, a test method will ask for a screenshot by calling - /// `takeScreenshot`. When this screenshot is taken [_driverCommandComplete] - /// will complete. - Completer _driverCommandComplete = Completer(); - - /// Takes screenshot using WebDriver screenshot command. - /// - /// Only works on Web when tests are run via `flutter driver` command. - /// - /// See: https://www.w3.org/TR/webdriver/#screen-capture. - @override - Future takeScreenshot(String screenshotName) async { - await _sendWebDriverCommand(WebDriverCommand.screenshot(screenshotName)); - } - - Future _sendWebDriverCommand(WebDriverCommand command) async { - try { - _webDriverCommandPipe.complete(Future.value(command)); - final bool awaitCommand = await _driverCommandComplete.future; - if (!awaitCommand) { - throw Exception( - 'Web Driver Command ${command.type} failed while waiting for ' - 'driver side'); - } - } catch (exception) { - throw Exception('Web Driver Command failed: ${command.type} with ' - 'exception $exception'); - } finally { - // Reset the completer. - _driverCommandComplete = Completer(); - } - } - - /// The callback function to response the driver side input. - /// - /// Provides a handshake mechanism for executing [WebDriverCommand]s on the - /// driver side. - @override - Future> callback( - Map params, IntegrationTestResults testRunner) async { - final String command = params['command']; - Map response; - switch (command) { - case 'request_data': - return params['message'] == null - ? _requestData(testRunner) - : _requestDataWithMessage(params['message'], testRunner); - break; - case 'get_health': - response = {'status': 'ok'}; - break; - default: - throw UnimplementedError('$command is not implemented'); - } - return { - 'isError': false, - 'response': response, - }; - } - - Future> _requestDataWithMessage( - String extraMessage, IntegrationTestResults testRunner) async { - Map response; - // Driver side tests' status is added as an extra message. - final DriverTestMessage message = - DriverTestMessage.fromString(extraMessage); - // If driver side tests are pending send the first command in the - // `commandPipe` to the tests. - if (message.isPending) { - final WebDriverCommand command = await _webDriverCommandPipe.future; - switch (command.type) { - case WebDriverCommandType.screenshot: - final Map data = Map.from(command.values); - data.addAll( - WebDriverCommand.typeToMap(WebDriverCommandType.screenshot)); - response = { - 'message': Response.webDriverCommand(data: data).toJson(), - }; - break; - case WebDriverCommandType.noop: - final Map data = Map(); - data.addAll(WebDriverCommand.typeToMap(WebDriverCommandType.noop)); - response = { - 'message': Response.webDriverCommand(data: data).toJson(), - }; - break; - default: - throw UnimplementedError('${command.type} is not implemented'); - } - } else { - final Map data = Map(); - data.addAll(WebDriverCommand.typeToMap(WebDriverCommandType.ack)); - response = { - 'message': Response.webDriverCommand(data: data).toJson(), - }; - _driverCommandComplete.complete(Future.value(message.isSuccess)); - _webDriverCommandPipe = Completer(); - } - return { - 'isError': false, - 'response': response, - }; - } - - Future> _requestData( - IntegrationTestResults testRunner) async { - final bool allTestsPassed = await testRunner.allTestsPassed.future; - final Map response = { - 'message': allTestsPassed - ? Response.allTestsPassed(data: testRunner.reportData).toJson() - : Response.someTestsFailed( - testRunner.failureMethodsDetails, - data: testRunner.reportData, - ).toJson(), - }; - return { - 'isError': false, - 'response': response, - }; - } - - @override - void cleanup() { - if (!_webDriverCommandPipe.isCompleted) { - _webDriverCommandPipe - .complete(Future.value(WebDriverCommand.noop())); - } - - if (!_driverCommandComplete.isCompleted) { - _driverCommandComplete.complete(Future.value(false)); - } - } -} diff --git a/packages/integration_test/lib/_extension_io.dart b/packages/integration_test/lib/_extension_io.dart deleted file mode 100644 index a8e66fca1900..000000000000 --- a/packages/integration_test/lib/_extension_io.dart +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -/// The dart:io implementation of [registerWebServiceExtension]. -/// -/// See also: -/// -/// * [_extension_web.dart], which has the dart:html implementation -void registerWebServiceExtension( - Future> Function(Map) call) { - throw UnsupportedError('Use registerServiceExtension instead'); -} diff --git a/packages/integration_test/lib/_extension_web.dart b/packages/integration_test/lib/_extension_web.dart deleted file mode 100644 index 8bf5950ef74a..000000000000 --- a/packages/integration_test/lib/_extension_web.dart +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:convert'; -import 'dart:html' as html; -import 'dart:js'; -import 'dart:js_util' as js_util; - -/// The dart:html implementation of [registerWebServiceExtension]. -/// -/// Registers Web Service Extension for Flutter Web application. -/// -/// window.$flutterDriver will be called by Flutter Web Driver to process -/// Flutter command. -/// -/// See also: -/// -/// * [_extension_io.dart], which has the dart:io implementation -void registerWebServiceExtension( - Future> Function(Map) call) { - js_util.setProperty(html.window, '\$flutterDriver', - allowInterop((dynamic message) async { - // ignore: undefined_function, undefined_identifier - final Map params = Map.from( - jsonDecode(message as String) as Map); - final Map result = - Map.from(await call(params)); - context['\$flutterDriverResult'] = json.encode(result); - })); -} diff --git a/packages/integration_test/lib/common.dart b/packages/integration_test/lib/common.dart deleted file mode 100644 index 53714a8e97ee..000000000000 --- a/packages/integration_test/lib/common.dart +++ /dev/null @@ -1,301 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:convert'; - -/// Classes shared between `integration_test.dart` and `flutter drive` based -/// adoptor (ex: `integration_test_driver.dart`). - -/// An object sent from integration_test back to the Flutter Driver in response to -/// `request_data` command. -class Response { - final List _failureDetails; - - final bool _allTestsPassed; - - /// The extra information to be added along side the test result. - Map data; - - /// Constructor to use for positive response. - Response.allTestsPassed({this.data}) - : this._allTestsPassed = true, - this._failureDetails = null; - - /// Constructor for failure response. - Response.someTestsFailed(this._failureDetails, {this.data}) - : this._allTestsPassed = false; - - /// Constructor for failure response. - Response.toolException({String ex}) - : this._allTestsPassed = false, - this._failureDetails = [Failure('ToolException', ex)]; - - /// Constructor for web driver commands response. - Response.webDriverCommand({this.data}) - : this._allTestsPassed = false, - this._failureDetails = null; - - /// Whether the test ran successfully or not. - bool get allTestsPassed => _allTestsPassed; - - /// If the result are failures get the formatted details. - String get formattedFailureDetails => - _allTestsPassed ? '' : formatFailures(_failureDetails); - - /// Failure details as a list. - List get failureDetails => _failureDetails; - - /// Serializes this message to a JSON map. - String toJson() => json.encode({ - 'result': allTestsPassed.toString(), - 'failureDetails': _failureDetailsAsString(), - if (data != null) 'data': data - }); - - /// Deserializes the result from JSON. - static Response fromJson(String source) { - final Map responseJson = json.decode(source); - if (responseJson['result'] as String == 'true') { - return Response.allTestsPassed(data: responseJson['data']); - } else { - return Response.someTestsFailed( - _failureDetailsFromJson(responseJson['failureDetails']), - data: responseJson['data'], - ); - } - } - - /// Method for formatting the test failures' details. - String formatFailures(List failureDetails) { - if (failureDetails.isEmpty) { - return ''; - } - - StringBuffer sb = StringBuffer(); - int failureCount = 1; - failureDetails.forEach((Failure f) { - sb.writeln('Failure in method: ${f.methodName}'); - sb.writeln('${f.details}'); - sb.writeln('end of failure ${failureCount.toString()}\n\n'); - failureCount++; - }); - return sb.toString(); - } - - /// Create a list of Strings from [_failureDetails]. - List _failureDetailsAsString() { - final List list = List(); - if (_failureDetails == null || _failureDetails.isEmpty) { - return list; - } - - _failureDetails.forEach((Failure f) { - list.add(f.toJson()); - }); - - return list; - } - - /// Creates a [Failure] list using a json response. - static List _failureDetailsFromJson(List list) { - final List failureList = List(); - list.forEach((s) { - final String failure = s as String; - failureList.add(Failure.fromJsonString(failure)); - }); - return failureList; - } -} - -/// Representing a failure includes the method name and the failure details. -class Failure { - /// The name of the test method which failed. - final String methodName; - - /// The details of the failure such as stack trace. - final String details; - - /// Constructor requiring all fields during initialization. - Failure(this.methodName, this.details); - - /// Serializes the object to JSON. - String toJson() { - return json.encode({ - 'methodName': methodName, - 'details': details, - }); - } - - @override - String toString() => toJson(); - - /// Decode a JSON string to create a Failure object. - static Failure fromJsonString(String jsonString) { - Map failure = json.decode(jsonString); - return Failure(failure['methodName'], failure['details']); - } -} - -/// Message used to communicate between app side tests and driver tests. -/// -/// Not all `integration_tests` use this message. They are only used when app -/// side tests are sending [WebDriverCommand]s to the driver side. -/// -/// These messages are used for the handshake since they carry information on -/// the driver side test such as: status pending or tests failed. -class DriverTestMessage { - final bool _isSuccess; - final bool _isPending; - - /// When tests are failed on the driver side. - DriverTestMessage.error() - : _isSuccess = false, - _isPending = false; - - /// When driver side is waiting on [WebDriverCommand]s to be sent from the - /// app side. - DriverTestMessage.pending() - : _isSuccess = false, - _isPending = true; - - /// When driver side successfully completed executing the [WebDriverCommand]. - DriverTestMessage.complete() - : _isSuccess = true, - _isPending = false; - - // /// Status of this message. - // /// - // /// The status will be use to notify `integration_test` of driver side's - // /// state. - // String get status => _status; - - /// Has the command completed successfully by the driver. - bool get isSuccess => _isSuccess; - - /// Is the driver waiting for a command. - bool get isPending => _isPending; - - /// Depending on the values of [isPending] and [isSuccess], returns a string - /// to represent the [DriverTestMessage]. - /// - /// Used as an alternative method to converting the object to json since - /// [RequestData] is only accepting string as `message`. - @override - String toString() { - if (isPending) { - return 'pending'; - } else if (isSuccess) { - return 'complete'; - } else { - return 'error'; - } - } - - /// Return a DriverTestMessage depending on `status`. - static DriverTestMessage fromString(String status) { - switch (status) { - case 'error': - return DriverTestMessage.error(); - case 'pending': - return DriverTestMessage.pending(); - case 'complete': - return DriverTestMessage.complete(); - default: - throw StateError('This type of status does not exist: $status'); - } - } -} - -/// Types of different WebDriver commands that can be used in web integration -/// tests. -/// -/// These commands are either commands that WebDriver can execute or used -/// for the communication between `integration_test` and the driver test. -enum WebDriverCommandType { - /// Acknowlegement for the previously sent message. - ack, - - /// No further WebDriver commands is requested by the app-side tests. - noop, - - /// Asking WebDriver to take a screenshot of the Web page. - screenshot, -} - -/// Command for WebDriver to execute. -/// -/// Only works on Web when tests are run via `flutter driver` command. -/// -/// See: https://www.w3.org/TR/webdriver/ -class WebDriverCommand { - /// Type of the [WebDriverCommand]. - /// - /// Currently the only command that triggers a WebDriver API is `screenshot`. - /// - /// There are also `ack` and `noop` commands defined to manage the handshake - /// during the communication. - final WebDriverCommandType type; - - /// Used for adding extra values to the commands such as file name for - /// `screenshot`. - final Map values; - - /// Constructor for [WebDriverCommandType.noop] command. - WebDriverCommand.noop() - : this.type = WebDriverCommandType.noop, - this.values = Map(); - - /// Constructor for [WebDriverCommandType.noop] screenshot. - WebDriverCommand.screenshot(String screenshot_name) - : this.type = WebDriverCommandType.screenshot, - this.values = {'screenshot_name': screenshot_name}; - - /// Util method for converting [WebDriverCommandType] to a map entry. - /// - /// Used for converting messages to json format. - static Map typeToMap(WebDriverCommandType type) => { - 'web_driver_command': '${type}', - }; -} - -/// Template methods each class that responses the driver side inputs must -/// implement. -/// -/// Depending on the platform the communication between `integration_tests` and -/// the `driver_tests` can be different. -/// -/// For the web implementation [WebCallbackManager]. -/// For the io implementation [IOCallbackManager]. -abstract class CallbackManager { - /// The callback function to response the driver side input. - Future> callback( - Map params, IntegrationTestResults testRunner); - - /// Request to take a screenshot of the application. - Future takeScreenshot(String screenshot); - - /// Cleanup and completers or locks used during the communication. - void cleanup(); -} - -/// Interface that surfaces test results of integration tests. -/// -/// Implemented by [IntegrationTestWidgetsFlutterBinding]s. -/// -/// Any class which needs to access the test results but do not want to create -/// a cyclic dependency [IntegrationTestWidgetsFlutterBinding]s can use this -/// interface. Example [CallbackManager]. -abstract class IntegrationTestResults { - /// Stores failure details. - /// - /// Failed test method's names used as key. - List get failureMethodsDetails; - - /// The extra data for the reported result. - Map get reportData; - - /// Whether all the test methods completed succesfully. - Completer get allTestsPassed; -} diff --git a/packages/integration_test/lib/integration_test.dart b/packages/integration_test/lib/integration_test.dart deleted file mode 100644 index 4176c1c2c5e1..000000000000 --- a/packages/integration_test/lib/integration_test.dart +++ /dev/null @@ -1,285 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; -import 'dart:developer' as developer; - -import 'package:flutter/rendering.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; -import 'package:vm_service/vm_service.dart' as vm; -import 'package:vm_service/vm_service_io.dart' as vm_io; - -import 'common.dart'; -import '_extension_io.dart' if (dart.library.html) '_extension_web.dart'; -import '_callback_io.dart' if (dart.library.html) '_callback_web.dart' - as driver_actions; - -const String _success = 'success'; - -/// A subclass of [LiveTestWidgetsFlutterBinding] that reports tests results -/// on a channel to adapt them to native instrumentation test format. -class IntegrationTestWidgetsFlutterBinding extends LiveTestWidgetsFlutterBinding - implements IntegrationTestResults { - /// Sets up a listener to report that the tests are finished when everything is - /// torn down. - IntegrationTestWidgetsFlutterBinding() { - // TODO(jackson): Report test results as they arrive - tearDownAll(() async { - try { - // For web integration tests we are not using the - // `plugins.flutter.io/integration_test`. Mark the tests as complete - // before invoking the channel. - if (kIsWeb) { - if (!_allTestsPassed.isCompleted) { - _allTestsPassed.complete(true); - } - } - callbackManager.cleanup(); - await _channel.invokeMethod( - 'allTestsFinished', - { - 'results': results.map((name, result) { - if (result is Failure) { - return MapEntry(name, result.details); - } - return MapEntry(name, result); - }) - }, - ); - } on MissingPluginException { - print('Warning: integration_test test plugin was not detected.'); - } - if (!_allTestsPassed.isCompleted) _allTestsPassed.complete(true); - }); - - // TODO(jackson): Report the results individually instead of all at once - // See https://github.com/flutter/flutter/issues/38985 - final TestExceptionReporter oldTestExceptionReporter = reportTestException; - reportTestException = - (FlutterErrorDetails details, String testDescription) { - results[testDescription] = Failure(testDescription, details.toString()); - if (!_allTestsPassed.isCompleted) { - _allTestsPassed.complete(false); - } - oldTestExceptionReporter(details, testDescription); - }; - } - - // TODO(dnfield): Remove the ignore once we bump the minimum Flutter version - // ignore: override_on_non_overriding_member - @override - bool get overrideHttpClient => false; - - // TODO(dnfield): Remove the ignore once we bump the minimum Flutter version - // ignore: override_on_non_overriding_member - @override - bool get registerTestTextInput => false; - - Size _surfaceSize; - - /// Artificially changes the surface size to `size` on the Widget binding, - /// then flushes microtasks. - /// - /// Set to null to use the default surface size. - @override - Future setSurfaceSize(Size size) { - return TestAsyncUtils.guard(() async { - assert(inTest); - if (_surfaceSize == size) { - return; - } - _surfaceSize = size; - handleMetricsChanged(); - }); - } - - @override - ViewConfiguration createViewConfiguration() { - final double devicePixelRatio = window.devicePixelRatio; - final Size size = _surfaceSize ?? window.physicalSize / devicePixelRatio; - return TestViewConfiguration( - size: size, - window: window, - ); - } - - @override - Completer get allTestsPassed => _allTestsPassed; - final Completer _allTestsPassed = Completer(); - - @override - List get failureMethodsDetails => _failures; - - /// Similar to [WidgetsFlutterBinding.ensureInitialized]. - /// - /// Returns an instance of the [IntegrationTestWidgetsFlutterBinding], creating and - /// initializing it if necessary. - static WidgetsBinding ensureInitialized() { - if (WidgetsBinding.instance == null) { - IntegrationTestWidgetsFlutterBinding(); - } - assert(WidgetsBinding.instance is IntegrationTestWidgetsFlutterBinding); - return WidgetsBinding.instance; - } - - static const MethodChannel _channel = - MethodChannel('plugins.flutter.io/integration_test'); - - /// Test results that will be populated after the tests have completed. - /// - /// Keys are the test descriptions, and values are either [_success] or - /// a [Failure]. - @visibleForTesting - Map results = {}; - - List get _failures => results.values.whereType().toList(); - - /// The extra data for the reported result. - /// - /// The values in `reportData` must be json-serializable objects or `null`. - /// If it's `null`, no extra data is attached to the result. - /// - /// The default value is `null`. - @override - Map get reportData => _reportData; - Map _reportData; - set reportData(Map data) => this._reportData = data; - - /// Manages callbacks received from driver side and commands send to driver - /// side. - final CallbackManager callbackManager = driver_actions.callbackManager; - - /// Taking a screenshot. - /// - /// Called by test methods. Implementation differs for each platform. - Future takeScreenshot(String screenshotName) async { - await callbackManager.takeScreenshot(screenshotName); - } - - /// The callback function to response the driver side input. - @visibleForTesting - Future> callback(Map params) async { - return await callbackManager.callback( - params, this /* as IntegrationTestResults */); - } - - // Emulates the Flutter driver extension, returning 'pass' or 'fail'. - @override - void initServiceExtensions() { - super.initServiceExtensions(); - - if (kIsWeb) { - registerWebServiceExtension(callback); - } - - registerServiceExtension(name: 'driver', callback: callback); - } - - @override - Future runTest( - Future testBody(), - VoidCallback invariantTester, { - String description = '', - Duration timeout, - }) async { - await super.runTest( - testBody, - invariantTester, - description: description, - timeout: timeout, - ); - results[description] ??= _success; - } - - vm.VmService _vmService; - - /// Initialize the [vm.VmService] settings for the timeline. - @visibleForTesting - Future enableTimeline({ - List streams = const ['all'], - @visibleForTesting vm.VmService vmService, - }) async { - assert(streams != null); - assert(streams.isNotEmpty); - if (vmService != null) { - _vmService = vmService; - } - if (_vmService == null) { - final developer.ServiceProtocolInfo info = - await developer.Service.getInfo(); - assert(info.serverUri != null); - _vmService = await vm_io.vmServiceConnectUri( - 'ws://localhost:${info.serverUri.port}${info.serverUri.path}ws', - ); - } - await _vmService.setVMTimelineFlags(streams); - } - - /// Runs [action] and returns a [vm.Timeline] trace for it. - /// - /// Waits for the `Future` returned by [action] to complete prior to stopping - /// the trace. - /// - /// The `streams` parameter limits the recorded timeline event streams to only - /// the ones listed. By default, all streams are recorded. - /// See `timeline_streams` in - /// [Dart-SDK/runtime/vm/timeline.cc](https://github.com/dart-lang/sdk/blob/master/runtime/vm/timeline.cc) - /// - /// If [retainPriorEvents] is true, retains events recorded prior to calling - /// [action]. Otherwise, prior events are cleared before calling [action]. By - /// default, prior events are cleared. - Future traceTimeline( - Future action(), { - List streams = const ['all'], - bool retainPriorEvents = false, - }) async { - await enableTimeline(streams: streams); - if (retainPriorEvents) { - await action(); - return await _vmService.getVMTimeline(); - } - - await _vmService.clearVMTimeline(); - final vm.Timestamp startTime = await _vmService.getVMTimelineMicros(); - await action(); - final vm.Timestamp endTime = await _vmService.getVMTimelineMicros(); - return await _vmService.getVMTimeline( - timeOriginMicros: startTime.timestamp, - timeExtentMicros: endTime.timestamp, - ); - } - - /// This is a convenience wrap of [traceTimeline] and send the result back to - /// the host for the [flutter_driver] style tests. - /// - /// This records the timeline during `action` and adds the result to - /// [reportData] with `reportKey`. [reportData] contains the extra information - /// of the test other than test success/fail. It will be passed back to the - /// host and be processed by the [ResponseDataCallback] defined in - /// [integrationDriver]. By default it will be written to - /// `build/integration_response_data.json` with the key `timeline`. - /// - /// For tests with multiple calls of this method, `reportKey` needs to be a - /// unique key, otherwise the later result will override earlier one. - /// - /// The `streams` and `retainPriorEvents` parameters are passed as-is to - /// [traceTimeline]. - Future traceAction( - Future action(), { - List streams = const ['all'], - bool retainPriorEvents = false, - String reportKey = 'timeline', - }) async { - vm.Timeline timeline = await traceTimeline( - action, - streams: streams, - retainPriorEvents: retainPriorEvents, - ); - reportData ??= {}; - reportData[reportKey] = timeline.toJson(); - } -} diff --git a/packages/integration_test/lib/integration_test_driver.dart b/packages/integration_test/lib/integration_test_driver.dart deleted file mode 100644 index 3b0e756000d0..000000000000 --- a/packages/integration_test/lib/integration_test_driver.dart +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright 2014 The Flutter Authors. All 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'; - -import 'package:integration_test/common.dart'; -import 'package:path/path.dart' as path; - -/// Flutter Driver test output directory. -/// -/// Tests should write any output files to this directory. Defaults to the path -/// set in the FLUTTER_TEST_OUTPUTS_DIR environment variable, or `build` if -/// unset. -String testOutputsDirectory = - Platform.environment['FLUTTER_TEST_OUTPUTS_DIR'] ?? 'build'; - -/// The callback type to handle [integration_test.Response.data] after the test -/// succeeds. -typedef ResponseDataCallback = FutureOr Function(Map); - -/// Writes a json-serializable json data to to -/// [testOutputsDirectory]/`testOutputFilename.json`. -/// -/// This is the default `responseDataCallback` in [integrationDriver]. -Future writeResponseData( - Map data, { - String testOutputFilename = 'integration_response_data', - String destinationDirectory, -}) async { - assert(testOutputFilename != null); - destinationDirectory ??= testOutputsDirectory; - await fs.directory(destinationDirectory).create(recursive: true); - final File file = fs.file(path.join( - destinationDirectory, - '$testOutputFilename.json', - )); - final String resultString = _encodeJson(data, true); - await file.writeAsString(resultString); -} - -/// Adaptor to run an integration test using `flutter drive`. -/// -/// `timeout` controls the longest time waited before the test ends. -/// It is not necessarily the execution time for the test app: the test may -/// finish sooner than the `timeout`. -/// -/// `responseDataCallback` is the handler for processing [integration_test.Response.data]. -/// The default value is `writeResponseData`. -/// -/// To an integration test `.dart` using `flutter drive`, put a file named -/// `_test.dart` in the app's `test_driver` directory: -/// -/// ```dart -/// import 'dart:async'; -/// -/// import 'package:integration_test/integration_test_driver.dart'; -/// -/// Future main() async => integrationDriver(); -/// -/// ``` -Future integrationDriver({ - Duration timeout = const Duration(minutes: 1), - ResponseDataCallback responseDataCallback = writeResponseData, -}) async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String jsonResult = await driver.requestData(null, timeout: timeout); - final Response response = Response.fromJson(jsonResult); - await driver.close(); - - if (response.allTestsPassed) { - print('All tests passed.'); - if (responseDataCallback != null) { - await responseDataCallback(response.data); - } - exit(0); - } else { - print('Failure Details:\n${response.formattedFailureDetails}'); - exit(1); - } -} - -const JsonEncoder _prettyEncoder = JsonEncoder.withIndent(' '); - -String _encodeJson(Map jsonObject, bool pretty) { - return pretty ? _prettyEncoder.convert(jsonObject) : json.encode(jsonObject); -} diff --git a/packages/integration_test/lib/integration_test_driver_extended.dart b/packages/integration_test/lib/integration_test_driver_extended.dart deleted file mode 100644 index bc38bb71de50..000000000000 --- a/packages/integration_test/lib/integration_test_driver_extended.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'common.dart'; - -import 'package:flutter_driver/flutter_driver.dart'; - -/// Example Integration Test which can also run WebDriver command depending on -/// the requests coming from the test methods. -Future integrationDriver( - {FlutterDriver driver, Function onScreenshot}) async { - if (driver == null) { - driver = await FlutterDriver.connect(); - } - // Test states that it's waiting on web driver commands. - // [DriverTestMessage] is converted to string since json format causes an - // error if it's used as a message for requestData. - String jsonResponse = - await driver.requestData(DriverTestMessage.pending().toString()); - - Response response = Response.fromJson(jsonResponse); - - // Until `integration_test` returns a [WebDriverCommandType.noop], keep - // executing WebDriver commands. - while (response.data != null && - response.data['web_driver_command'] != null && - response.data['web_driver_command'] != '${WebDriverCommandType.noop}') { - final String webDriverCommand = response.data['web_driver_command']; - if (webDriverCommand == '${WebDriverCommandType.screenshot}') { - // Use `driver.screenshot()` method to get a screenshot of the web page. - final List screenshotImage = await driver.screenshot(); - final String screenshotName = response.data['screenshot_name']; - - final bool screenshotSuccess = - await onScreenshot(screenshotName, screenshotImage); - if (screenshotSuccess) { - jsonResponse = - await driver.requestData(DriverTestMessage.complete().toString()); - } else { - jsonResponse = - await driver.requestData(DriverTestMessage.error().toString()); - } - - response = Response.fromJson(jsonResponse); - } else if (webDriverCommand == '${WebDriverCommandType.ack}') { - // Previous command completed ask for a new one. - jsonResponse = - await driver.requestData(DriverTestMessage.pending().toString()); - - response = Response.fromJson(jsonResponse); - } else { - break; - } - } - - // If No-op command is sent, ask for the result of all tests. - if (response.data != null && - response.data['web_driver_command'] != null && - response.data['web_driver_command'] == '${WebDriverCommandType.noop}') { - jsonResponse = await driver.requestData(null); - - response = Response.fromJson(jsonResponse); - print('result $jsonResponse'); - } - - await driver.close(); - - if (response.allTestsPassed) { - print('All tests passed.'); - exit(0); - } else { - print('Failure Details:\n${response.formattedFailureDetails}'); - exit(1); - } -} diff --git a/packages/integration_test/pubspec.yaml b/packages/integration_test/pubspec.yaml deleted file mode 100644 index 39a48d1675da..000000000000 --- a/packages/integration_test/pubspec.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: integration_test -description: Runs tests that use the flutter_test API as integration tests. -version: 0.9.1 -homepage: https://github.com/flutter/plugins/tree/master/packages/integration_test - -environment: - sdk: ">=2.2.2 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" - -dependencies: - flutter: - sdk: flutter - flutter_driver: - sdk: flutter - flutter_test: - sdk: flutter - path: ^1.6.4 - vm_service: ^4.2.0 - -dev_dependencies: - pedantic: ^1.8.0 - mockito: ^4.1.1 - -flutter: - plugin: - platforms: - android: - package: dev.flutter.plugins.integration_test - pluginClass: IntegrationTestPlugin - ios: - pluginClass: IntegrationTestPlugin diff --git a/packages/integration_test/test/binding_fail_test.dart b/packages/integration_test/test/binding_fail_test.dart deleted file mode 100644 index bb5961b18fc7..000000000000 --- a/packages/integration_test/test/binding_fail_test.dart +++ /dev/null @@ -1,79 +0,0 @@ -import 'dart:async'; -import 'dart:io'; -import 'dart:convert'; - -import 'package:flutter_test/flutter_test.dart'; - -// Assumes that the flutter command is in `$PATH`. -const String _flutterBin = 'flutter'; -const String _integrationResultsPrefix = - 'IntegrationTestWidgetsFlutterBinding test results:'; -const String _failureExcerpt = 'Expected: \\n Actual: '; - -void main() async { - group('Integration binding result', () { - test('when multiple tests pass', () async { - final Map results = - await _runTest('test/data/pass_test_script.dart'); - - expect( - results, - equals({ - 'passing test 1': 'success', - 'passing test 2': 'success', - })); - }); - - test('when multiple tests fail', () async { - final Map results = - await _runTest('test/data/fail_test_script.dart'); - - expect(results, hasLength(2)); - expect( - results, containsPair('failing test 1', contains(_failureExcerpt))); - expect( - results, containsPair('failing test 2', contains(_failureExcerpt))); - }); - - test('when one test passes, then another fails', () async { - final Map results = - await _runTest('test/data/pass_then_fail_test_script.dart'); - - expect(results, hasLength(2)); - expect(results, containsPair('passing test', equals('success'))); - expect(results, containsPair('failing test', contains(_failureExcerpt))); - }); - }); -} - -/// Runs a test script and returns the [IntegrationTestWidgetsFlutterBinding.result]. -/// -/// [scriptPath] is relative to the package root. -Future> _runTest(String scriptPath) async { - final Process process = - await Process.start(_flutterBin, ['test', '--machine', scriptPath]); - - /// In the test [tearDownAll] block, the test results are encoded into JSON and - /// are printed with the [_integrationResultsPrefix] prefix. - /// - /// See the following for the test event spec which we parse the printed lines - /// out of: https://github.com/dart-lang/test/blob/master/pkgs/test/doc/json_reporter.md - final String testResults = (await process.stdout - .transform(utf8.decoder) - .expand((String text) => text.split('\n')) - .map((String line) { - try { - return jsonDecode(line); - } on FormatException { - // Only interested in test events which are JSON. - } - }) - .where((dynamic testEvent) => - testEvent != null && testEvent['type'] == 'print') - .map((dynamic printEvent) => printEvent['message'] as String) - .firstWhere((String message) => - message.startsWith(_integrationResultsPrefix))) - .replaceAll(_integrationResultsPrefix, ''); - - return jsonDecode(testResults); -} diff --git a/packages/integration_test/test/binding_test.dart b/packages/integration_test/test/binding_test.dart deleted file mode 100644 index ef4efc59aac0..000000000000 --- a/packages/integration_test/test/binding_test.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'dart:convert'; - -import 'package:flutter/material.dart'; - -import 'package:integration_test/integration_test.dart'; -import 'package:integration_test/common.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; -import 'package:vm_service/vm_service.dart' as vm; - -vm.Timeline _ktimelines = vm.Timeline( - traceEvents: [], - timeOriginMicros: 100, - timeExtentMicros: 200, -); - -void main() async { - Future> request; - - group('Test Integration binding', () { - final WidgetsBinding binding = - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - assert(binding is IntegrationTestWidgetsFlutterBinding); - final IntegrationTestWidgetsFlutterBinding integrationBinding = - binding as IntegrationTestWidgetsFlutterBinding; - - MockVM mockVM; - List clockTimes = [100, 200]; - - setUp(() { - request = integrationBinding.callback({ - 'command': 'request_data', - }); - mockVM = MockVM(); - when(mockVM.getVMTimeline( - timeOriginMicros: anyNamed('timeOriginMicros'), - timeExtentMicros: anyNamed('timeExtentMicros'), - )).thenAnswer((_) => Future.value(_ktimelines)); - when(mockVM.getVMTimelineMicros()).thenAnswer( - (_) => Future.value(vm.Timestamp(timestamp: clockTimes.removeAt(0))), - ); - }); - - testWidgets('Run Integration app', (WidgetTester tester) async { - runApp(MaterialApp( - home: Text('Test'), - )); - expect(tester.binding, integrationBinding); - integrationBinding.reportData = {'answer': 42}; - }); - - testWidgets('setSurfaceSize works', (WidgetTester tester) async { - await tester.pumpWidget(MaterialApp(home: Center(child: Text('Test')))); - - final Size windowCenter = tester.binding.window.physicalSize / - tester.binding.window.devicePixelRatio / - 2; - final double windowCenterX = windowCenter.width; - final double windowCenterY = windowCenter.height; - - Offset widgetCenter = tester.getRect(find.byType(Text)).center; - expect(widgetCenter.dx, windowCenterX); - expect(widgetCenter.dy, windowCenterY); - - await tester.binding.setSurfaceSize(const Size(200, 300)); - await tester.pump(); - widgetCenter = tester.getRect(find.byType(Text)).center; - expect(widgetCenter.dx, 100); - expect(widgetCenter.dy, 150); - - await tester.binding.setSurfaceSize(null); - await tester.pump(); - widgetCenter = tester.getRect(find.byType(Text)).center; - expect(widgetCenter.dx, windowCenterX); - expect(widgetCenter.dy, windowCenterY); - }); - - testWidgets('Test traceAction', (WidgetTester tester) async { - await integrationBinding.enableTimeline(vmService: mockVM); - await integrationBinding.traceAction(() async {}); - expect(integrationBinding.reportData, isNotNull); - expect(integrationBinding.reportData.containsKey('timeline'), true); - expect( - json.encode(integrationBinding.reportData['timeline']), - json.encode(_ktimelines), - ); - }); - }); - - tearDownAll(() async { - // This part is outside the group so that `request` has been compeleted as - // part of the `tearDownAll` registerred in the group during - // `IntegrationTestWidgetsFlutterBinding` initialization. - final Map response = - (await request)['response'] as Map; - final String message = response['message'] as String; - Response result = Response.fromJson(message); - assert(result.data['answer'] == 42); - }); -} - -class MockVM extends Mock implements vm.VmService {} diff --git a/packages/integration_test/test/data/README.md b/packages/integration_test/test/data/README.md deleted file mode 100644 index e52aca112ce4..000000000000 --- a/packages/integration_test/test/data/README.md +++ /dev/null @@ -1,4 +0,0 @@ -Files in this directory are not invoked directly by the test command. - -They are used as inputs for the other test files outside of this directory, so -that failures can be tested. \ No newline at end of file diff --git a/packages/integration_test/test/data/fail_test_script.dart b/packages/integration_test/test/data/fail_test_script.dart deleted file mode 100644 index 05a75d7d031e..000000000000 --- a/packages/integration_test/test/data/fail_test_script.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'dart:convert'; - -import 'package:integration_test/integration_test.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() async { - final IntegrationTestWidgetsFlutterBinding binding = - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('failing test 1', (WidgetTester tester) async { - expect(true, false); - }); - - testWidgets('failing test 2', (WidgetTester tester) async { - expect(true, false); - }); - - tearDownAll(() { - print( - 'IntegrationTestWidgetsFlutterBinding test results: ${jsonEncode(binding.results)}'); - }); -} diff --git a/packages/integration_test/test/data/pass_test_script.dart b/packages/integration_test/test/data/pass_test_script.dart deleted file mode 100644 index 7e222c8e8961..000000000000 --- a/packages/integration_test/test/data/pass_test_script.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'dart:convert'; - -import 'package:integration_test/integration_test.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() async { - final IntegrationTestWidgetsFlutterBinding binding = - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('passing test 1', (WidgetTester tester) async { - expect(true, true); - }); - - testWidgets('passing test 2', (WidgetTester tester) async { - expect(true, true); - }); - - tearDownAll(() { - print( - 'IntegrationTestWidgetsFlutterBinding test results: ${jsonEncode(binding.results)}'); - }); -} diff --git a/packages/integration_test/test/data/pass_then_fail_test_script.dart b/packages/integration_test/test/data/pass_then_fail_test_script.dart deleted file mode 100644 index 324c1c21cfa6..000000000000 --- a/packages/integration_test/test/data/pass_then_fail_test_script.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'dart:convert'; - -import 'package:integration_test/integration_test.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() async { - final IntegrationTestWidgetsFlutterBinding binding = - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('passing test', (WidgetTester tester) async { - expect(true, true); - }); - - testWidgets('failing test', (WidgetTester tester) async { - expect(true, false); - }); - - tearDownAll(() { - print( - 'IntegrationTestWidgetsFlutterBinding test results: ${jsonEncode(binding.results)}'); - }); -} diff --git a/packages/integration_test/test/response_serialization_test.dart b/packages/integration_test/test/response_serialization_test.dart deleted file mode 100644 index 8b969402035d..000000000000 --- a/packages/integration_test/test/response_serialization_test.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; - -import 'package:integration_test/common.dart'; - -void main() { - test('Serialize and deserialize Failure', () { - Failure fail = Failure('what a name', 'no detail'); - Failure restored = Failure.fromJsonString(fail.toString()); - expect(restored.methodName, fail.methodName); - expect(restored.details, fail.details); - }); - - test('Serialize and deserialize Response', () { - Response response, restored; - String jsonString; - - response = Response.allTestsPassed(); - jsonString = response.toJson(); - expect(jsonString, '{"result":"true","failureDetails":[]}'); - restored = Response.fromJson(jsonString); - expect(restored.allTestsPassed, response.allTestsPassed); - expect(restored.data, null); - expect(restored.formattedFailureDetails, ''); - - final Failure fail = Failure('what a name', 'no detail'); - final Failure fail2 = Failure('what a name2', 'no detail2'); - response = Response.someTestsFailed([fail, fail2]); - jsonString = response.toJson(); - restored = Response.fromJson(jsonString); - expect(restored.allTestsPassed, response.allTestsPassed); - expect(restored.data, null); - expect(restored.formattedFailureDetails, response.formattedFailureDetails); - - Map data = {'aaa': 'bbb'}; - response = Response.allTestsPassed(data: data); - jsonString = response.toJson(); - restored = Response.fromJson(jsonString); - expect(restored.data.keys, ['aaa']); - expect(restored.data.values, ['bbb']); - - response = Response.someTestsFailed([fail, fail2], data: data); - jsonString = response.toJson(); - restored = Response.fromJson(jsonString); - expect(restored.data.keys, ['aaa']); - expect(restored.data.values, ['bbb']); - }); -} diff --git a/packages/ios_platform_images/AUTHORS b/packages/ios_platform_images/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/ios_platform_images/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/ios_platform_images/CHANGELOG.md b/packages/ios_platform_images/CHANGELOG.md index a83f22ec7571..60db21a450d8 100644 --- a/packages/ios_platform_images/CHANGELOG.md +++ b/packages/ios_platform_images/CHANGELOG.md @@ -1,3 +1,15 @@ +## NEXT + +* Add iOS unit test target. + +## 0.2.0 + +* Migrate to null safety. + +## 0.1.2+4 + +* Update Flutter SDK constraint. + ## 0.1.2+3 * Remove no-op android folder in the example app. diff --git a/packages/ios_platform_images/LICENSE b/packages/ios_platform_images/LICENSE index a6d6c0749818..c6823b81eb84 100644 --- a/packages/ios_platform_images/LICENSE +++ b/packages/ios_platform_images/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017 The Chromium Authors. All rights reserved. +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: diff --git a/packages/ios_platform_images/analysis_options.yaml b/packages/ios_platform_images/analysis_options.yaml new file mode 100644 index 000000000000..cda4f6e153e6 --- /dev/null +++ b/packages/ios_platform_images/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options_legacy.yaml diff --git a/packages/ios_platform_images/example/ios/Podfile b/packages/ios_platform_images/example/ios/Podfile new file mode 100644 index 000000000000..397864535f5d --- /dev/null +++ b/packages/ios_platform_images/example/ios/Podfile @@ -0,0 +1,44 @@ +# 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 + use_frameworks! + use_modular_headers! + + 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/ios_platform_images/example/ios/Runner.xcodeproj/project.pbxproj b/packages/ios_platform_images/example/ios/Runner.xcodeproj/project.pbxproj index 03bbe666a0ed..0ee14af546ef 100644 --- a/packages/ios_platform_images/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/ios_platform_images/example/ios/Runner.xcodeproj/project.pbxproj @@ -15,8 +15,20 @@ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; A30D9778BC0D4D09580CF4BE /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 906079E3CC5A6FAB808EAF1E /* Pods_Runner.framework */; }; + F76AC1C1266713D00040C8BC /* IosPlatformImagesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F76AC1C0266713D00040C8BC /* IosPlatformImagesTests.m */; }; + FC73B055B2CD2E32A3E50B27 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AD2C5EF0E06B6EC7EBCB922C /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + F76AC1C3266713D00040C8BC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -31,6 +43,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0B20D3254D8E1A2B01D83810 /* 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 = ""; }; 0DE21BF62447752100097E3A /* textfile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = textfile; sourceTree = ""; }; 0EF1CD9A3A3064B5289EF22E /* 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 = ""; }; @@ -40,6 +53,7 @@ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 80830F517E3E8B75B2D3AC0A /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 906079E3CC5A6FAB808EAF1E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; 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 = ""; }; @@ -48,7 +62,12 @@ 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 = ""; }; + AD2C5EF0E06B6EC7EBCB922C /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; D1A761179BC59B1BAEE63036 /* 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 = ""; }; + D36FEDC657E1CE88220062D7 /* 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 = ""; }; + F76AC1BE266713D00040C8BC /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F76AC1C0266713D00040C8BC /* IosPlatformImagesTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = IosPlatformImagesTests.m; sourceTree = ""; }; + F76AC1C2266713D00040C8BC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -60,6 +79,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F76AC1BB266713D00040C8BC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FC73B055B2CD2E32A3E50B27 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -69,6 +96,9 @@ D1A761179BC59B1BAEE63036 /* Pods-Runner.debug.xcconfig */, 0EF1CD9A3A3064B5289EF22E /* Pods-Runner.release.xcconfig */, 4B56C310C5932F84CD6C17AC /* Pods-Runner.profile.xcconfig */, + 0B20D3254D8E1A2B01D83810 /* Pods-RunnerTests.debug.xcconfig */, + D36FEDC657E1CE88220062D7 /* Pods-RunnerTests.release.xcconfig */, + 80830F517E3E8B75B2D3AC0A /* Pods-RunnerTests.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -89,6 +119,7 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, + F76AC1BF266713D00040C8BC /* RunnerTests */, 97C146EF1CF9000F007C117D /* Products */, 790F6E36EBDB3EC4A899BEF5 /* Pods */, DBEBA2309FD49D5C34798105 /* Frameworks */, @@ -99,6 +130,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + F76AC1BE266713D00040C8BC /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -111,7 +143,6 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, @@ -120,19 +151,22 @@ path = Runner; sourceTree = ""; }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { + DBEBA2309FD49D5C34798105 /* Frameworks */ = { isa = PBXGroup; children = ( + 906079E3CC5A6FAB808EAF1E /* Pods_Runner.framework */, + AD2C5EF0E06B6EC7EBCB922C /* Pods_RunnerTests.framework */, ); - name = "Supporting Files"; + name = Frameworks; sourceTree = ""; }; - DBEBA2309FD49D5C34798105 /* Frameworks */ = { + F76AC1BF266713D00040C8BC /* RunnerTests */ = { isa = PBXGroup; children = ( - 906079E3CC5A6FAB808EAF1E /* Pods_Runner.framework */, + F76AC1C0266713D00040C8BC /* IosPlatformImagesTests.m */, + F76AC1C2266713D00040C8BC /* Info.plist */, ); - name = Frameworks; + path = RunnerTests; sourceTree = ""; }; /* End PBXGroup section */ @@ -160,6 +194,25 @@ productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; + F76AC1BD266713D00040C8BC /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F76AC1C8266713D00040C8BC /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + C102F13F37851E08F0608EE5 /* [CP] Check Pods Manifest.lock */, + F76AC1BA266713D00040C8BC /* Sources */, + F76AC1BB266713D00040C8BC /* Frameworks */, + F76AC1BC266713D00040C8BC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F76AC1C4266713D00040C8BC /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = F76AC1BE266713D00040C8BC /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -167,12 +220,18 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 1020; - ORGANIZATIONNAME = "The Chromium Authors"; + ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; }; + F76AC1BD266713D00040C8BC = { + CreatedOnToolsVersion = 12.5; + DevelopmentTeam = S8QB4VV633; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; @@ -189,6 +248,7 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + F76AC1BD266713D00040C8BC /* RunnerTests */, ); }; /* End PBXProject section */ @@ -206,6 +266,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F76AC1BC266713D00040C8BC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -229,9 +296,12 @@ files = ( ); inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/ios_platform_images/ios_platform_images.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ios_platform_images.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -274,6 +344,28 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + C102F13F37851E08F0608EE5 /* [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 */ @@ -286,8 +378,24 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F76AC1BA266713D00040C8BC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F76AC1C1266713D00040C8BC /* IosPlatformImagesTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + F76AC1C4266713D00040C8BC /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = F76AC1C3266713D00040C8BC /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -310,7 +418,6 @@ /* Begin XCBuildConfiguration section */ 249021D3217E4FDB00AE95B9 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -387,7 +494,6 @@ }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -443,7 +549,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -546,6 +651,51 @@ }; name = Release; }; + F76AC1C5266713D00040C8BC /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0B20D3254D8E1A2B01D83810 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = S8QB4VV633; + 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; + }; + F76AC1C6266713D00040C8BC /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D36FEDC657E1CE88220062D7 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = S8QB4VV633; + 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; + }; + F76AC1C7266713D00040C8BC /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 80830F517E3E8B75B2D3AC0A /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = S8QB4VV633; + 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 = Profile; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -569,6 +719,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + F76AC1C8266713D00040C8BC /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F76AC1C5266713D00040C8BC /* Debug */, + F76AC1C6266713D00040C8BC /* Release */, + F76AC1C7266713D00040C8BC /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; diff --git a/packages/ios_platform_images/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/ios_platform_images/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140cfdb3f..6de5fabfee04 100644 --- a/packages/ios_platform_images/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/ios_platform_images/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -27,8 +27,6 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> - - - - + + + + + + - - + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/ios_platform_images/example/ios/Runner/AppDelegate.swift b/packages/ios_platform_images/example/ios/Runner/AppDelegate.swift index 70693e4a8c12..caf998393333 100644 --- a/packages/ios_platform_images/example/ios/Runner/AppDelegate.swift +++ b/packages/ios_platform_images/example/ios/Runner/AppDelegate.swift @@ -1,3 +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 UIKit import Flutter diff --git a/packages/ios_platform_images/example/ios/Runner/Runner-Bridging-Header.h b/packages/ios_platform_images/example/ios/Runner/Runner-Bridging-Header.h index 7335fdf9000c..eb7e8ba8052f 100644 --- a/packages/ios_platform_images/example/ios/Runner/Runner-Bridging-Header.h +++ b/packages/ios_platform_images/example/ios/Runner/Runner-Bridging-Header.h @@ -1 +1,5 @@ -#import "GeneratedPluginRegistrant.h" \ No newline at end of file +// Copyright 2013 The Flutter Authors. 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/ios_platform_images/example/ios/RunnerTests/Info.plist b/packages/ios_platform_images/example/ios/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/ios_platform_images/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/ios_platform_images/example/ios/RunnerTests/IosPlatformImagesTests.m b/packages/ios_platform_images/example/ios/RunnerTests/IosPlatformImagesTests.m new file mode 100644 index 000000000000..747719d30276 --- /dev/null +++ b/packages/ios_platform_images/example/ios/RunnerTests/IosPlatformImagesTests.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 ios_platform_images; +@import XCTest; + +@interface IosPlatformImagesTests : XCTestCase +@end + +@implementation IosPlatformImagesTests + +- (void)testPlugin { + IosPlatformImagesPlugin* plugin = [[IosPlatformImagesPlugin alloc] init]; + XCTAssertNotNil(plugin); +} + +@end diff --git a/packages/ios_platform_images/example/lib/main.dart b/packages/ios_platform_images/example/lib/main.dart index 655380f8d125..1546edca8c90 100644 --- a/packages/ios_platform_images/example/lib/main.dart +++ b/packages/ios_platform_images/example/lib/main.dart @@ -1,3 +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:flutter/material.dart'; import 'package:ios_platform_images/ios_platform_images.dart'; @@ -14,8 +18,7 @@ class _MyAppState extends State { void initState() { super.initState(); - IosPlatformImages.resolveURL("textfile", null) - .then((value) => print(value)); + IosPlatformImages.resolveURL("textfile").then((value) => print(value)); } @override @@ -26,8 +29,11 @@ class _MyAppState extends State { title: const Text('Plugin example app'), ), body: Center( - // "pug" is a resource in Assets.xcassets. - child: Image(image: IosPlatformImages.load("flutter")), + // "flutter" is a resource in Assets.xcassets. + child: Image( + 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 fa0f9eb3aac6..e45e7694cc2b 100644 --- a/packages/ios_platform_images/example/pubspec.yaml +++ b/packages/ios_platform_images/example/pubspec.yaml @@ -1,64 +1,27 @@ name: ios_platform_images_example description: Demonstrates how to use the ios_platform_images plugin. -publish_to: 'none' -homepage: https://github.com/flutter/plugins/tree/master/packages/ios_platform_images/ios_platform_images +publish_to: none environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.12.0 <3.0.0" dependencies: flutter: sdk: flutter - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^0.1.2 + cupertino_icons: ^1.0.2 dev_dependencies: flutter_test: sdk: flutter ios_platform_images: + # When depending on this package from a real application you should use: + # ios_platform_images: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ - pedantic: ^1.8.0 + pedantic: ^1.10.0 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/ios_platform_images/example/test/widget_test.dart b/packages/ios_platform_images/example/test/widget_test.dart index ae8e5a41e4be..09fa35c27a6b 100644 --- a/packages/ios_platform_images/example/test/widget_test.dart +++ b/packages/ios_platform_images/example/test/widget_test.dart @@ -1,3 +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 'dart:io'; import 'package:flutter/material.dart'; diff --git a/packages/ios_platform_images/ios/Classes/IosPlatformImagesPlugin.h b/packages/ios_platform_images/ios/Classes/IosPlatformImagesPlugin.h index d4106b598d75..f3c8efe9bd6a 100644 --- a/packages/ios_platform_images/ios/Classes/IosPlatformImagesPlugin.h +++ b/packages/ios_platform_images/ios/Classes/IosPlatformImagesPlugin.h @@ -1,3 +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 /// A plugin for Flutter that allows Flutter to load images in a platform diff --git a/packages/ios_platform_images/ios/Classes/IosPlatformImagesPlugin.m b/packages/ios_platform_images/ios/Classes/IosPlatformImagesPlugin.m index bad6f11417b9..abd331e5d0cb 100644 --- a/packages/ios_platform_images/ios/Classes/IosPlatformImagesPlugin.m +++ b/packages/ios_platform_images/ios/Classes/IosPlatformImagesPlugin.m @@ -1,3 +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 "IosPlatformImagesPlugin.h" #if !__has_feature(objc_arc) diff --git a/packages/ios_platform_images/lib/ios_platform_images.dart b/packages/ios_platform_images/lib/ios_platform_images.dart index c7c12616ec36..e9bc0b342239 100644 --- a/packages/ios_platform_images/lib/ios_platform_images.dart +++ b/packages/ios_platform_images/lib/ios_platform_images.dart @@ -1,3 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + import 'dart:async'; import 'dart:typed_data'; import 'dart:ui' as ui; @@ -9,13 +13,11 @@ import 'package:flutter/foundation.dart' show SynchronousFuture, describeIdentity; class _FutureImageStreamCompleter extends ImageStreamCompleter { - final Future futureScale; - final InformationCollector informationCollector; - - _FutureImageStreamCompleter( - {Future codec, this.futureScale, this.informationCollector}) - : assert(codec != null), - assert(futureScale != null) { + _FutureImageStreamCompleter({ + required Future codec, + required this.futureScale, + this.informationCollector, + }) { codec.then(_onCodecReady, onError: (dynamic error, StackTrace stack) { reportError( context: ErrorDescription('resolving a single-frame image stream'), @@ -27,6 +29,9 @@ class _FutureImageStreamCompleter extends ImageStreamCompleter { }); } + final Future futureScale; + final InformationCollector? informationCollector; + Future _onCodecReady(ui.Codec codec) async { try { ui.FrameInfo nextFrame = await codec.getNextFrame(); @@ -50,9 +55,7 @@ class _FutureMemoryImage extends ImageProvider<_FutureMemoryImage> { /// Constructor for FutureMemoryImage. [_futureBytes] is the bytes that will /// be loaded into an image and [_futureScale] is the scale that will be applied to /// that image to account for high-resolution images. - const _FutureMemoryImage(this._futureBytes, this._futureScale) - : assert(_futureBytes != null), - assert(_futureScale != null); + const _FutureMemoryImage(this._futureBytes, this._futureScale); final Future _futureBytes; final Future _futureScale; @@ -73,7 +76,9 @@ class _FutureMemoryImage extends ImageProvider<_FutureMemoryImage> { } Future _loadAsync( - _FutureMemoryImage key, DecoderCallback decode) async { + _FutureMemoryImage key, + DecoderCallback decode, + ) async { assert(key == this); return _futureBytes.then((Uint8List bytes) { return decode(bytes); @@ -113,10 +118,19 @@ class IosPlatformImages { /// /// See [https://developer.apple.com/documentation/uikit/uiimage/1624146-imagenamed?language=objc] static ImageProvider load(String name) { - Future loadInfo = _channel.invokeMethod('loadImage', name); + Future loadInfo = _channel.invokeMapMethod('loadImage', name); Completer bytesCompleter = Completer(); Completer scaleCompleter = Completer(); loadInfo.then((map) { + if (map == null) { + scaleCompleter.completeError( + Exception("Image couldn't be found: $name"), + ); + bytesCompleter.completeError( + Exception("Image couldn't be found: $name"), + ); + return; + } scaleCompleter.complete(map["scale"]); bytesCompleter.complete(map["data"]); }); @@ -129,7 +143,7 @@ class IosPlatformImages { /// Returns null if the resource can't be found. /// /// See [https://developer.apple.com/documentation/foundation/nsbundle/1411540-urlforresource?language=objc] - static Future resolveURL(String name, [String ext]) { - return _channel.invokeMethod('resolveURL', [name, ext]); + static Future resolveURL(String name, {String? extension}) { + return _channel.invokeMethod('resolveURL', [name, extension]); } } diff --git a/packages/ios_platform_images/pubspec.yaml b/packages/ios_platform_images/pubspec.yaml index 482097a515b8..e90937f4f0b5 100644 --- a/packages/ios_platform_images/pubspec.yaml +++ b/packages/ios_platform_images/pubspec.yaml @@ -1,62 +1,24 @@ name: ios_platform_images description: A plugin to share images between Flutter and iOS in add-to-app setups. -version: 0.1.2+3 -homepage: https://github.com/flutter/plugins/tree/master/packages/ios_platform_images/ios_platform_images +repository: https://github.com/flutter/plugins/tree/master/packages/ios_platform_images/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 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - pedantic: ^1.8.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter. flutter: - # This section identifies this Flutter project as a plugin project. - # The androidPackage and pluginClass identifiers should not ordinarily - # be modified. They are used by the tooling to maintain consistency when - # adding or updating assets for this project. plugin: platforms: ios: pluginClass: IosPlatformImagesPlugin - # To add assets to your plugin package, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. +dependencies: + flutter: + sdk: flutter - # To add custom fonts to your plugin package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/custom-fonts/#from-packages +dev_dependencies: + flutter_test: + sdk: flutter + pedantic: ^1.10.0 diff --git a/packages/ios_platform_images/test/ios_platform_images_test.dart b/packages/ios_platform_images/test/ios_platform_images_test.dart index fd87180e9ac0..a896e3d835af 100644 --- a/packages/ios_platform_images/test/ios_platform_images_test.dart +++ b/packages/ios_platform_images/test/ios_platform_images_test.dart @@ -1,3 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:ios_platform_images/ios_platform_images.dart'; diff --git a/packages/local_auth/AUTHORS b/packages/local_auth/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/local_auth/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/local_auth/CHANGELOG.md b/packages/local_auth/CHANGELOG.md index a1dc7aaa4e16..a97c4b47b288 100644 --- a/packages/local_auth/CHANGELOG.md +++ b/packages/local_auth/CHANGELOG.md @@ -1,3 +1,54 @@ +## 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. diff --git a/packages/local_auth/LICENSE b/packages/local_auth/LICENSE index a6d6c0749818..c6823b81eb84 100644 --- a/packages/local_auth/LICENSE +++ b/packages/local_auth/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017 The Chromium Authors. All rights reserved. +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: diff --git a/packages/local_auth/README.md b/packages/local_auth/README.md index ca2aa49bed23..84470c646e6b 100644 --- a/packages/local_auth/README.md +++ b/packages/local_auth/README.md @@ -23,8 +23,8 @@ bool canCheckBiometrics = Currently the following biometric types are implemented: -* BiometricType.face -* BiometricType.fingerprint +- BiometricType.face +- BiometricType.fingerprint To get a list of enrolled biometrics, call getAvailableBiometrics: @@ -44,10 +44,10 @@ if (Platform.isIOS) { We have default dialogs with an 'OK' button to show authentication error messages for the following 2 cases: -1. Passcode/PIN/Pattern Not Set. The user has not yet configured a passcode on - iOS or PIN/pattern on Android. -2. Touch ID/Fingerprint Not Enrolled. The user has not enrolled any - fingerprints on the device. +1. Passcode/PIN/Pattern Not Set. The user has not yet configured a passcode on + iOS or PIN/pattern on Android. +2. Touch ID/Fingerprint Not Enrolled. The user has not enrolled any + fingerprints on the device. Which means, if there's no fingerprint on the user's device, a dialog with instructions will pop up to let the user set up fingerprint. If the user clicks @@ -55,20 +55,33 @@ instructions will pop up to let the user set up fingerprint. If the user clicks Use the exported APIs to trigger local authentication with default dialogs: +The `authenticate()` method uses biometric authentication, but also allows +users to use pin, pattern, or passcode. + ```dart var localAuth = LocalAuthentication(); bool didAuthenticate = - await localAuth.authenticateWithBiometrics( + await localAuth.authenticate( localizedReason: 'Please authenticate to show account balance'); ``` +To authenticate using biometric authentication only, set `biometricOnly` to `true`. + +```dart +var localAuth = LocalAuthentication(); +bool didAuthenticate = + await localAuth.authenticate( + localizedReason: 'Please authenticate to show account balance', + biometricOnly: true); +``` + If you don't want to use the default dialogs, call this API with 'useErrorDialogs = false'. In this case, it will throw the error message back and you need to handle them in your dart code: ```dart bool didAuthenticate = - await localAuth.authenticateWithBiometrics( + await localAuth.authenticate( localizedReason: 'Please authenticate to show account balance', useErrorDialogs: false); ``` @@ -84,7 +97,7 @@ const iosStrings = const IOSAuthMessages( goToSettingsButton: 'settings', goToSettingsDescription: 'Please set up your Touch ID.', lockOut: 'Please reenable your Touch ID'); -await localAuth.authenticateWithBiometrics( +await localAuth.authenticate( localizedReason: 'Please authenticate to show account balance', useErrorDialogs: false, iOSAuthStrings: iosStrings); @@ -112,7 +125,7 @@ import 'package:flutter/services.dart'; import 'package:local_auth/error_codes.dart' as auth_error; try { - bool didAuthenticate = await local_auth.authenticateWithBiometrics( + bool didAuthenticate = await local_auth.authenticate( localizedReason: 'Please authenticate to show account balance'); } on PlatformException catch (e) { if (e.code == auth_error.notAvailable) { @@ -123,7 +136,7 @@ try { ## iOS Integration -Note that this plugin works with both TouchID and FaceID. However, to use the latter, +Note that this plugin works with both Touch ID and Face ID. However, to use the latter, you need to also add: ```xml @@ -132,8 +145,7 @@ you need to also add: ``` to your Info.plist file. Failure to do so results in a dialog that tells the user your -app has not been updated to use TouchID. - +app has not been updated to use Face ID. ## Android Integration @@ -142,6 +154,42 @@ opposed to Activity. This can be easily done by switching to use `FlutterFragmentActivity` as opposed to `FlutterActivity` in your manifest (or your own Activity class if you are extending the base class). +Update your MainActivity.java: + +```java +import android.os.Bundle; +import io.flutter.app.FlutterFragmentActivity; +import io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin; +import io.flutter.plugins.localauth.LocalAuthPlugin; + +public class MainActivity extends FlutterFragmentActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + FlutterAndroidLifecyclePlugin.registerWith( + registrarFor( + "io.flutter.plugins.flutter_plugin_android_lifecycle.FlutterAndroidLifecyclePlugin")); + LocalAuthPlugin.registerWith(registrarFor("io.flutter.plugins.localauth.LocalAuthPlugin")); + } +} +``` + +OR + +Update your MainActivity.kt: + +```kotlin +import io.flutter.embedding.android.FlutterFragmentActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugins.GeneratedPluginRegistrant + +class MainActivity: FlutterFragmentActivity() { + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + GeneratedPluginRegistrant.registerWith(flutterEngine) + } +} +``` + Update your project's `AndroidManifest.xml` file to include the `USE_FINGERPRINT` permissions: @@ -155,7 +203,7 @@ Update your project's `AndroidManifest.xml` file to include the On Android, you can check only for existence of fingerprint hardware prior to API 29 (Android Q). Therefore, if you would like to support other biometrics types (such as face scanning) and you want to support SDKs lower than Q, -*do not* call `getAvailableBiometrics`. Simply call `authenticateWithBiometrics`. +_do not_ call `getAvailableBiometrics`. Simply call `authenticate` with `biometricOnly: true`. This will return an error if there was no hardware available. ## Sticky Auth @@ -170,6 +218,6 @@ app resumes. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). -For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code). +For help on editing plugin code, view the [documentation](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin). diff --git a/packages/local_auth/analysis_options.yaml b/packages/local_auth/analysis_options.yaml new file mode 100644 index 000000000000..cda4f6e153e6 --- /dev/null +++ b/packages/local_auth/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options_legacy.yaml diff --git a/packages/local_auth/android/build.gradle b/packages/local_auth/android/build.gradle index 0fc603c36867..8878cfbcfc3e 100644 --- a/packages/local_auth/android/build.gradle +++ b/packages/local_auth/android/build.gradle @@ -4,18 +4,18 @@ version '1.0-SNAPSHOT' buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:4.1.1' } } rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } @@ -34,9 +34,9 @@ android { } dependencies { - api "androidx.core:core:1.1.0-beta01" - api "androidx.biometric:biometric:1.0.0-beta01" - api "androidx.fragment:fragment:1.1.0-alpha06" + api "androidx.core:core:1.3.2" + api "androidx.biometric:biometric:1.1.0" + api "androidx.fragment:fragment:1.3.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/local_auth/android/src/main/AndroidManifest.xml b/packages/local_auth/android/src/main/AndroidManifest.xml index b7da0caab6da..cb6cb985a986 100644 --- a/packages/local_auth/android/src/main/AndroidManifest.xml +++ b/packages/local_auth/android/src/main/AndroidManifest.xml @@ -1,3 +1,5 @@ + + diff --git a/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java b/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java index e907cc43f2b4..2b825c6d1f31 100644 --- a/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java +++ b/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/AuthenticationHelper.java @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // 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; @@ -29,7 +29,7 @@ import java.util.concurrent.Executor; /** - * Authenticates the user with fingerprint and sends corresponding response back to Flutter. + * Authenticates the user with biometrics and sends corresponding response back to Flutter. * *

One instance per call is generated to ensure readable separation of executable paths across * method calls. @@ -37,10 +37,8 @@ @SuppressWarnings("deprecation") class AuthenticationHelper extends BiometricPrompt.AuthenticationCallback implements Application.ActivityLifecycleCallbacks, DefaultLifecycleObserver { - /** The callback that handles the result of this authentication process. */ interface AuthCompletionHandler { - /** Called when authentication was successful. */ void onSuccess(); @@ -75,24 +73,32 @@ interface AuthCompletionHandler { Lifecycle lifecycle, FragmentActivity activity, MethodCall call, - AuthCompletionHandler completionHandler) { + AuthCompletionHandler completionHandler, + boolean allowCredentials) { this.lifecycle = lifecycle; this.activity = activity; this.completionHandler = completionHandler; this.call = call; this.isAuthSticky = call.argument("stickyAuth"); this.uiThreadExecutor = new UiThreadExecutor(); - this.promptInfo = + + BiometricPrompt.PromptInfo.Builder promptBuilder = new BiometricPrompt.PromptInfo.Builder() .setDescription((String) call.argument("localizedReason")) .setTitle((String) call.argument("signInTitle")) - .setSubtitle((String) call.argument("fingerprintHint")) - .setNegativeButtonText((String) call.argument("cancelButton")) + .setSubtitle((String) call.argument("biometricHint")) .setConfirmationRequired((Boolean) call.argument("sensitiveTransaction")) - .build(); + .setConfirmationRequired((Boolean) call.argument("sensitiveTransaction")); + + if (allowCredentials) { + promptBuilder.setDeviceCredentialAllowed(true); + } else { + promptBuilder.setNegativeButtonText((String) call.argument("cancelButton")); + } + this.promptInfo = promptBuilder.build(); } - /** Start the fingerprint listener. */ + /** Start the biometric listener. */ void authenticate() { if (lifecycle != null) { lifecycle.addObserver(this); @@ -103,7 +109,7 @@ void authenticate() { biometricPrompt.authenticate(promptInfo); } - /** Cancels the fingerprint authentication. */ + /** Cancels the biometric authentication. */ void stopAuthentication() { if (biometricPrompt != null) { biometricPrompt.cancelAuthentication(); @@ -111,7 +117,7 @@ void stopAuthentication() { } } - /** Stops the fingerprint listener. */ + /** Stops the biometric listener. */ private void stop() { if (lifecycle != null) { lifecycle.removeObserver(this); @@ -125,21 +131,28 @@ private void stop() { public void onAuthenticationError(int errorCode, CharSequence errString) { switch (errorCode) { case BiometricPrompt.ERROR_NO_DEVICE_CREDENTIAL: - completionHandler.onError( - "PasscodeNotSet", - "Phone not secured by PIN, pattern or password, or SIM is currently locked."); + if (call.argument("useErrorDialogs")) { + showGoToSettingsDialog( + (String) call.argument("deviceCredentialsRequired"), + (String) call.argument("deviceCredentialsSetupDescription")); + return; + } + completionHandler.onError("NotAvailable", "Security credentials not available."); break; case BiometricPrompt.ERROR_NO_SPACE: case BiometricPrompt.ERROR_NO_BIOMETRICS: + if (promptInfo.isDeviceCredentialAllowed()) return; if (call.argument("useErrorDialogs")) { - showGoToSettingsDialog(); + showGoToSettingsDialog( + (String) call.argument("biometricRequired"), + (String) call.argument("goToSettingDescription")); return; } completionHandler.onError("NotEnrolled", "No Biometrics enrolled on this device."); break; case BiometricPrompt.ERROR_HW_UNAVAILABLE: case BiometricPrompt.ERROR_HW_NOT_PRESENT: - completionHandler.onError("NotAvailable", "Biometrics is not available on this device."); + completionHandler.onError("NotAvailable", "Security credentials not available."); break; case BiometricPrompt.ERROR_LOCKOUT: completionHandler.onError( @@ -176,7 +189,7 @@ public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult resul public void onAuthenticationFailed() {} /** - * If the activity is paused, we keep track because fingerprint dialog simply returns "User + * If the activity is paused, we keep track because biometric dialog simply returns "User * cancelled" when the activity is paused. */ @Override @@ -215,12 +228,12 @@ public void onResume(@NonNull LifecycleOwner owner) { // Suppress inflateParams lint because dialogs do not need to attach to a parent view. @SuppressLint("InflateParams") - private void showGoToSettingsDialog() { + private void showGoToSettingsDialog(String title, String descriptionText) { View view = LayoutInflater.from(activity).inflate(R.layout.go_to_setting, null, false); TextView message = (TextView) view.findViewById(R.id.fingerprint_required); TextView description = (TextView) view.findViewById(R.id.go_to_setting_description); - message.setText((String) call.argument("fingerprintRequired")); - description.setText((String) call.argument("goToSettingDescription")); + message.setText(title); + description.setText(descriptionText); Context context = new ContextThemeWrapper(activity, R.style.AlertDialogCustom); OnClickListener goToSettingHandler = new OnClickListener() { diff --git a/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java b/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java index 0f93f40e947a..7ed9a7ea324d 100644 --- a/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java +++ b/packages/local_auth/android/src/main/java/io/flutter/plugins/localauth/LocalAuthPlugin.java @@ -1,12 +1,21 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // 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 android.app.Activity.RESULT_OK; +import static android.content.Context.KEYGUARD_SERVICE; + import android.app.Activity; +import android.app.KeyguardManager; +import android.content.Context; +import android.content.Intent; import android.content.pm.PackageManager; +import android.hardware.fingerprint.FingerprintManager; import android.os.Build; +import androidx.annotation.NonNull; +import androidx.biometric.BiometricManager; import androidx.fragment.app.FragmentActivity; import androidx.lifecycle.Lifecycle; import io.flutter.embedding.engine.plugins.FlutterPlugin; @@ -17,6 +26,8 @@ import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugin.common.PluginRegistry; +import io.flutter.plugin.common.PluginRegistry.Registrar; import io.flutter.plugins.localauth.AuthenticationHelper.AuthCompletionHandler; import java.util.ArrayList; import java.util.concurrent.atomic.AtomicBoolean; @@ -29,14 +40,33 @@ @SuppressWarnings("deprecation") public class LocalAuthPlugin implements MethodCallHandler, FlutterPlugin, ActivityAware { private static final String CHANNEL_NAME = "plugins.flutter.io/local_auth"; - + private static final int LOCK_REQUEST_CODE = 221; private Activity activity; private final AtomicBoolean authInProgress = new AtomicBoolean(false); - private AuthenticationHelper authenticationHelper; + private AuthenticationHelper authHelper; // These are null when not using v2 embedding. private MethodChannel channel; private Lifecycle lifecycle; + private BiometricManager biometricManager; + private FingerprintManager fingerprintManager; + private KeyguardManager keyguardManager; + private Result lockRequestResult; + private final PluginRegistry.ActivityResultListener resultListener = + new PluginRegistry.ActivityResultListener() { + @Override + public boolean onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == LOCK_REQUEST_CODE) { + if (resultCode == RESULT_OK && lockRequestResult != null) { + authenticateSuccess(lockRequestResult); + } else { + authenticateFail(lockRequestResult); + } + lockRequestResult = null; + } + return false; + } + }; /** * Registers a plugin with the v1 embedding api {@code io.flutter.plugin.common}. @@ -49,13 +79,12 @@ public class LocalAuthPlugin implements MethodCallHandler, FlutterPlugin, Activi * io.flutter.plugin.common.BinaryMessenger}. */ @SuppressWarnings("deprecation") - public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { + public static void registerWith(Registrar registrar) { final MethodChannel channel = new MethodChannel(registrar.messenger(), CHANNEL_NAME); - channel.setMethodCallHandler(new LocalAuthPlugin(registrar.activity())); - } - - private LocalAuthPlugin(Activity activity) { - this.activity = activity; + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + plugin.activity = registrar.activity(); + channel.setMethodCallHandler(plugin); + registrar.addActivityResultListener(plugin.resultListener); } /** @@ -66,118 +95,241 @@ private LocalAuthPlugin(Activity activity) { public LocalAuthPlugin() {} @Override - public void onMethodCall(MethodCall call, final Result result) { - if (call.method.equals("authenticateWithBiometrics")) { - if (authInProgress.get()) { - // Apps should not invoke another authentication request while one is in progress, - // so we classify this as an error condition. If we ever find a legitimate use case for - // this, we can try to cancel the ongoing auth and start a new one but for now, not worth - // the complexity. - result.error("auth_in_progress", "Authentication in progress", null); - return; - } + public void onMethodCall(MethodCall call, @NonNull final Result result) { + switch (call.method) { + case "authenticate": + authenticate(call, result); + break; + case "getAvailableBiometrics": + getAvailableBiometrics(result); + break; + case "isDeviceSupported": + isDeviceSupported(result); + break; + case "stopAuthentication": + stopAuthentication(result); + break; + default: + result.notImplemented(); + break; + } + } - if (activity == null || activity.isFinishing()) { - result.error("no_activity", "local_auth plugin requires a foreground activity", null); - return; - } + /* + * Starts authentication process + */ + private void authenticate(MethodCall call, final Result result) { + if (authInProgress.get()) { + result.error("auth_in_progress", "Authentication in progress", null); + return; + } - if (!(activity instanceof FragmentActivity)) { - result.error( - "no_fragment_activity", - "local_auth plugin requires activity to be a FragmentActivity.", - null); - return; - } - authInProgress.set(true); - authenticationHelper = - new AuthenticationHelper( - lifecycle, - (FragmentActivity) activity, - call, - new AuthCompletionHandler() { - @Override - public void onSuccess() { - if (authInProgress.compareAndSet(true, false)) { - result.success(true); - } - } - - @Override - public void onFailure() { - if (authInProgress.compareAndSet(true, false)) { - result.success(false); - } - } - - @Override - public void onError(String code, String error) { - if (authInProgress.compareAndSet(true, false)) { - result.error(code, error, null); - } - } - }); - authenticationHelper.authenticate(); - } else if (call.method.equals("getAvailableBiometrics")) { - try { - if (activity == null || activity.isFinishing()) { - result.error("no_activity", "local_auth plugin requires a foreground activity", null); - return; - } - ArrayList biometrics = new ArrayList(); - PackageManager packageManager = activity.getPackageManager(); - if (Build.VERSION.SDK_INT >= 23) { - if (packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) { - biometrics.add("fingerprint"); + if (activity == null || activity.isFinishing()) { + result.error("no_activity", "local_auth plugin requires a foreground activity", null); + return; + } + + if (!(activity instanceof FragmentActivity)) { + result.error( + "no_fragment_activity", + "local_auth plugin requires activity to be a FragmentActivity.", + null); + return; + } + + if (!isDeviceSupported()) { + authInProgress.set(false); + result.error("NotAvailable", "Required security features not enabled", null); + return; + } + + authInProgress.set(true); + AuthCompletionHandler completionHandler = + new AuthCompletionHandler() { + @Override + public void onSuccess() { + authenticateSuccess(result); } - } - if (Build.VERSION.SDK_INT >= 29) { - if (packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) { - biometrics.add("face"); + + @Override + public void onFailure() { + authenticateFail(result); } - if (packageManager.hasSystemFeature(PackageManager.FEATURE_IRIS)) { - biometrics.add("iris"); + + @Override + public void onError(String code, String error) { + if (authInProgress.compareAndSet(true, false)) { + result.error(code, error, null); + } } + }; + + // if is biometricOnly try biometric prompt - might not work + boolean isBiometricOnly = call.argument("biometricOnly"); + if (isBiometricOnly) { + if (!canAuthenticateWithBiometrics()) { + if (!hasBiometricHardware()) { + completionHandler.onError("NoHardware", "No biometric hardware found"); } - result.success(biometrics); - } catch (Exception e) { - result.error("no_biometrics_available", e.getMessage(), null); + completionHandler.onError("NotEnrolled", "No biometrics enrolled on this device."); + return; } - } else if (call.method.equals(("stopAuthentication"))) { - stopAuthentication(result); - } else { - result.notImplemented(); + authHelper = + new AuthenticationHelper( + lifecycle, (FragmentActivity) activity, call, completionHandler, false); + authHelper.authenticate(); + return; + } + + // API 29 and above + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + authHelper = + new AuthenticationHelper( + lifecycle, (FragmentActivity) activity, call, completionHandler, true); + authHelper.authenticate(); + return; + } + + // API 23 - 28 with fingerprint + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && fingerprintManager != null) { + if (fingerprintManager.hasEnrolledFingerprints()) { + authHelper = + new AuthenticationHelper( + lifecycle, (FragmentActivity) activity, call, completionHandler, false); + authHelper.authenticate(); + return; + } + } + + // API 23 or higher with device credentials + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M + && keyguardManager != null + && keyguardManager.isDeviceSecure()) { + String title = call.argument("signInTitle"); + String reason = call.argument("localizedReason"); + Intent authIntent = keyguardManager.createConfirmDeviceCredentialIntent(title, reason); + + // save result for async response + lockRequestResult = result; + activity.startActivityForResult(authIntent, LOCK_REQUEST_CODE); + return; + } + + // Unable to authenticate + result.error("NotSupported", "This device does not support required security features", null); + } + + private void authenticateSuccess(Result result) { + if (authInProgress.compareAndSet(true, false)) { + result.success(true); + } + } + + private void authenticateFail(Result result) { + if (authInProgress.compareAndSet(true, false)) { + result.success(false); } } /* - Stops the authentication if in progress. - */ + * Stops the authentication if in progress. + */ private void stopAuthentication(Result result) { try { - if (authenticationHelper != null && authInProgress.get()) { - authenticationHelper.stopAuthentication(); - authenticationHelper = null; - result.success(true); - return; + if (authHelper != null && authInProgress.get()) { + authHelper.stopAuthentication(); + authHelper = null; } - result.success(false); + authInProgress.set(false); + result.success(true); } catch (Exception e) { result.success(false); } } + /* + * Returns biometric types available on device + */ + private void getAvailableBiometrics(final Result result) { + try { + if (activity == null || activity.isFinishing()) { + result.error("no_activity", "local_auth plugin requires a foreground activity", null); + return; + } + ArrayList biometrics = getAvailableBiometrics(); + result.success(biometrics); + } catch (Exception e) { + result.error("no_biometrics_available", e.getMessage(), null); + } + } + + private ArrayList getAvailableBiometrics() { + ArrayList biometrics = new ArrayList<>(); + if (activity == null || activity.isFinishing()) { + return biometrics; + } + PackageManager packageManager = activity.getPackageManager(); + if (Build.VERSION.SDK_INT >= 23) { + if (packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) { + biometrics.add("fingerprint"); + } + } + if (Build.VERSION.SDK_INT >= 29) { + if (packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) { + biometrics.add("face"); + } + if (packageManager.hasSystemFeature(PackageManager.FEATURE_IRIS)) { + biometrics.add("iris"); + } + } + + return biometrics; + } + + private boolean isDeviceSupported() { + if (keyguardManager == null) return false; + return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && keyguardManager.isDeviceSecure()); + } + + private boolean canAuthenticateWithBiometrics() { + if (biometricManager == null) return false; + return biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS; + } + + private boolean hasBiometricHardware() { + if (biometricManager == null) return false; + return biometricManager.canAuthenticate() != BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE; + } + + private void isDeviceSupported(Result result) { + result.success(isDeviceSupported()); + } + @Override public void onAttachedToEngine(FlutterPluginBinding binding) { - channel = new MethodChannel(binding.getBinaryMessenger(), CHANNEL_NAME); + channel = new MethodChannel(binding.getFlutterEngine().getDartExecutor(), CHANNEL_NAME); + channel.setMethodCallHandler(this); } @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) {} + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {} + + private void setServicesFromActivity(Activity activity) { + if (activity == null) return; + this.activity = activity; + Context context = activity.getBaseContext(); + biometricManager = BiometricManager.from(activity); + keyguardManager = (KeyguardManager) context.getSystemService(KEYGUARD_SERVICE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + fingerprintManager = + (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE); + } + } @Override public void onAttachedToActivity(ActivityPluginBinding binding) { - activity = binding.getActivity(); + binding.addActivityResultListener(resultListener); + setServicesFromActivity(binding.getActivity()); lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding); channel.setMethodCallHandler(this); } @@ -185,18 +337,17 @@ public void onAttachedToActivity(ActivityPluginBinding binding) { @Override public void onDetachedFromActivityForConfigChanges() { lifecycle = null; - activity = null; } @Override public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { - activity = binding.getActivity(); + binding.addActivityResultListener(resultListener); + setServicesFromActivity(binding.getActivity()); lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding); } @Override public void onDetachedFromActivity() { - activity = null; lifecycle = null; channel.setMethodCallHandler(null); } diff --git a/packages/local_auth/example/README.md b/packages/local_auth/example/README.md index da6cf0ccf939..a4a6091c9ba6 100644 --- a/packages/local_auth/example/README.md +++ b/packages/local_auth/example/README.md @@ -5,4 +5,4 @@ Demonstrates how to use the local_auth plugin. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). diff --git a/packages/local_auth/example/android/app/build.gradle b/packages/local_auth/example/android/app/build.gradle index eaccbe3cd9f9..34b3fe2d69e3 100644 --- a/packages/local_auth/example/android/app/build.gradle +++ b/packages/local_auth/example/android/app/build.gradle @@ -25,7 +25,7 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 28 + compileSdkVersion 29 lintOptions { disable 'InvalidPackage' diff --git a/packages/local_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/local_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties index 9a4163a4f5ee..186b71557c50 100644 --- a/packages/local_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ b/packages/local_auth/example/android/app/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/EmbeddingV1ActivityTest.java b/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/EmbeddingV1ActivityTest.java index 30d2126f0609..696fc493c6b8 100644 --- a/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/EmbeddingV1ActivityTest.java +++ b/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/EmbeddingV1ActivityTest.java @@ -1,3 +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. + package io.flutter.plugins.localauth; import androidx.test.rule.ActivityTestRule; diff --git a/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/FlutterFragmentActivityTest.java b/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/FlutterFragmentActivityTest.java new file mode 100644 index 000000000000..e5ece3edd50d --- /dev/null +++ b/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/FlutterFragmentActivityTest.java @@ -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. + +package io.flutter.plugins.localauth; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterFragmentActivity; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@RunWith(FlutterTestRunner.class) +public class FlutterFragmentActivityTest { + @Rule + public ActivityTestRule rule = + new ActivityTestRule<>(FlutterFragmentActivity.class); +} diff --git a/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/MainActivityTest.java b/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/MainActivityTest.java deleted file mode 100644 index 3d2d55bce0fa..000000000000 --- a/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/MainActivityTest.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.flutter.plugins.localauth; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import io.flutter.embedding.android.FlutterFragmentActivity; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -public class FlutterFragmentActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(FlutterFragmentActivity.class); -} diff --git a/packages/local_auth/example/android/app/src/main/java/io/flutter/plugins/localauthexample/EmbeddingV1Activity.java b/packages/local_auth/example/android/app/src/main/java/io/flutter/plugins/localauthexample/EmbeddingV1Activity.java index 91bef9dd13d7..c3fc8d47b3a4 100644 --- a/packages/local_auth/example/android/app/src/main/java/io/flutter/plugins/localauthexample/EmbeddingV1Activity.java +++ b/packages/local_auth/example/android/app/src/main/java/io/flutter/plugins/localauthexample/EmbeddingV1Activity.java @@ -1,3 +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. + package io.flutter.plugins.localauthexample; import android.os.Bundle; diff --git a/packages/local_auth/example/android/build.gradle b/packages/local_auth/example/android/build.gradle index 541636cc492a..54c943621de5 100644 --- a/packages/local_auth/example/android/build.gradle +++ b/packages/local_auth/example/android/build.gradle @@ -1,18 +1,18 @@ buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:4.1.1' } } allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/packages/local_auth/example/android/gradle.properties b/packages/local_auth/example/android/gradle.properties index a6738207fd15..7fe61a74cee0 100644 --- a/packages/local_auth/example/android/gradle.properties +++ b/packages/local_auth/example/android/gradle.properties @@ -1,4 +1,4 @@ -org.gradle.jvmargs=-Xmx1536M +org.gradle.jvmargs=-Xmx1024m android.useAndroidX=true android.enableJetifier=true android.enableR8=true diff --git a/packages/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties index 562393332f6c..cd9fe1c68282 100644 --- a/packages/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/local_auth/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu May 30 07:21:52 NPT 2019 +#Sun Jan 03 14:07:08 CST 2021 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip diff --git a/packages/local_auth/example/android/settings_aar.gradle b/packages/local_auth/example/android/settings_aar.gradle new file mode 100644 index 000000000000..e7b4def49cb5 --- /dev/null +++ b/packages/local_auth/example/android/settings_aar.gradle @@ -0,0 +1 @@ +include ':app' diff --git a/packages/local_auth/example/integration_test/local_auth_test.dart b/packages/local_auth/example/integration_test/local_auth_test.dart new file mode 100644 index 000000000000..5e8577f2b4d3 --- /dev/null +++ b/packages/local_auth/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/local_auth.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('canCheckBiometrics', (WidgetTester tester) async { + expect( + LocalAuthentication().getAvailableBiometrics(), + completion(isList), + ); + }); +} diff --git a/packages/local_auth/example/ios/Podfile b/packages/local_auth/example/ios/Podfile new file mode 100644 index 000000000000..ef20d8e3c010 --- /dev/null +++ b/packages/local_auth/example/ios/Podfile @@ -0,0 +1,44 @@ +# 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 index 63730d4eb2e3..debbb1d06aba 100644 --- a/packages/local_auth/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/local_auth/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,18 +9,26 @@ /* 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 */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 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; @@ -28,8 +36,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -39,32 +45,44 @@ /* 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 = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; 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 = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 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 = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 0CCCD07A2CE24E13C9C1EEA4 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -72,12 +90,19 @@ /* 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 = ( - 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, @@ -88,6 +113,7 @@ 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( + 33BF11D226680B2E002967F3 /* RunnerTests */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, @@ -100,6 +126,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + 3398D2CD26163948005A052F /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -131,7 +158,10 @@ E2D5FA899A019BD3E0DB0917 /* Frameworks */ = { isa = PBXGroup; children = ( + 3398D2DF26164A03005A052F /* liblocal_auth.a */, + 3398D2DC261649CD005A052F /* liblocal_auth.a */, 9D274A3F79473B1549B2BBD5 /* libPods-Runner.a */, + 719FE2C7EAF8D9A045E09C29 /* libPods-RunnerTests.a */, ); name = Frameworks; sourceTree = ""; @@ -141,6 +171,8 @@ children = ( EB36DF6C3F25E00DF4175422 /* Pods-Runner.debug.xcconfig */, 658CDD04B21E4EA92F8EF229 /* Pods-Runner.release.xcconfig */, + 99302E79EC77497F2F274D12 /* Pods-RunnerTests.debug.xcconfig */, + FEA527BB0A821430FEAA1566 /* Pods-RunnerTests.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -148,6 +180,25 @@ /* 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" */; @@ -159,7 +210,6 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 16CF73924D0A9C13B2100A83 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -177,8 +227,13 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; + ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { + 3398D2CC26163948005A052F = { + CreatedOnToolsVersion = 12.4; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; }; @@ -198,11 +253,19 @@ 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; @@ -217,61 +280,68 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 16CF73924D0A9C13B2100A83 /* [CP] Embed Pods Frameworks */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "[CP] Embed Pods Frameworks"; + name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\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\" thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - 9740EEB61CF901F6004384FC /* Run Script */ = { + 98D96A2D1A74AF66E3DD2DBC /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); - name = "Run Script"; + name = "[CP] Check Pods Manifest.lock"; 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\" build"; + 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; }; - 98D96A2D1A74AF66E3DD2DBC /* [CP] Check Pods Manifest.lock */ = { + 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-Runner-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -281,6 +351,14 @@ /* 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; @@ -293,6 +371,14 @@ }; /* 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; @@ -313,9 +399,55 @@ /* 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; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -372,7 +504,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -437,7 +568,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.localAuthExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.localAuthExample; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -458,7 +589,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.localAuthExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.localAuthExample; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; @@ -466,6 +597,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 3398D2D426163948005A052F /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 3398D2D526163948005A052F /* Debug */, + 3398D2D626163948005A052F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/packages/local_auth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/local_auth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16ed0f..919434a6254f 100644 --- a/packages/local_auth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/packages/local_auth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> 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 index 3bb3697ef41c..58a5d07a15c8 100644 --- a/packages/local_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/local_auth/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -37,6 +37,16 @@ + + + + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/local_auth/example/ios/Runner/AppDelegate.h b/packages/local_auth/example/ios/Runner/AppDelegate.h index d9e18e990f2e..0681d288bb70 100644 --- a/packages/local_auth/example/ios/Runner/AppDelegate.h +++ b/packages/local_auth/example/ios/Runner/AppDelegate.h @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/packages/local_auth/example/ios/Runner/AppDelegate.m b/packages/local_auth/example/ios/Runner/AppDelegate.m index f08675707182..30b87969f44a 100644 --- a/packages/local_auth/example/ios/Runner/AppDelegate.m +++ b/packages/local_auth/example/ios/Runner/AppDelegate.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/packages/local_auth/example/ios/Runner/main.m b/packages/local_auth/example/ios/Runner/main.m index dff6597e4513..f97b9ef5c8a1 100644 --- a/packages/local_auth/example/ios/Runner/main.m +++ b/packages/local_auth/example/ios/Runner/main.m @@ -1,3 +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 #import #import "AppDelegate.h" diff --git a/packages/local_auth/example/ios/RunnerTests/FLTLocalAuthPluginTests.m b/packages/local_auth/example/ios/RunnerTests/FLTLocalAuthPluginTests.m new file mode 100644 index 000000000000..97e78e2f624b --- /dev/null +++ b/packages/local_auth/example/ios/RunnerTests/FLTLocalAuthPluginTests.m @@ -0,0 +1,189 @@ +// Copyright 2013 The Flutter Authors. 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/ios/RunnerTests/Info.plist b/packages/local_auth/example/ios/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/local_auth/example/ios/RunnerTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/packages/local_auth/example/lib/main.dart b/packages/local_auth/example/lib/main.dart index 06e33b9853be..b6b6f3278423 100644 --- a/packages/local_auth/example/lib/main.dart +++ b/packages/local_auth/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -21,16 +21,28 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { final LocalAuthentication auth = LocalAuthentication(); - bool _canCheckBiometrics; - List _availableBiometrics; + _SupportState _supportState = _SupportState.unknown; + bool? _canCheckBiometrics; + List? _availableBiometrics; String _authorized = 'Not Authorized'; bool _isAuthenticating = false; + @override + void initState() { + super.initState(); + auth.isDeviceSupported().then( + (isSupported) => setState(() => _supportState = isSupported + ? _SupportState.supported + : _SupportState.unsupported), + ); + } + Future _checkBiometrics() async { - bool canCheckBiometrics; + late bool canCheckBiometrics; try { canCheckBiometrics = await auth.canCheckBiometrics; } on PlatformException catch (e) { + canCheckBiometrics = false; print(e); } if (!mounted) return; @@ -41,10 +53,11 @@ class _MyAppState extends State { } Future _getAvailableBiometrics() async { - List availableBiometrics; + late List availableBiometrics; try { availableBiometrics = await auth.getAvailableBiometrics(); } on PlatformException catch (e) { + availableBiometrics = []; print(e); } if (!mounted) return; @@ -61,16 +74,51 @@ class _MyAppState extends State { _isAuthenticating = true; _authorized = 'Authenticating'; }); - authenticated = await auth.authenticateWithBiometrics( - localizedReason: 'Scan your fingerprint to authenticate', + authenticated = await auth.authenticate( + localizedReason: 'Let OS determine authentication method', useErrorDialogs: true, stickyAuth: true); + setState(() { + _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; @@ -80,39 +128,92 @@ class _MyAppState extends State { }); } - void _cancelAuthentication() { - auth.stopAuthentication(); + void _cancelAuthentication() async { + await auth.stopAuthentication(); + setState(() => _isAuthenticating = false); } @override Widget build(BuildContext context) { return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: const Text('Plugin example app'), - ), - body: ConstrainedBox( - constraints: const BoxConstraints.expand(), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: ListView( + padding: const EdgeInsets.only(top: 30), + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (_supportState == _SupportState.unknown) + CircularProgressIndicator() + else if (_supportState == _SupportState.supported) + Text("This device is supported") + else + Text("This device is not supported"), + Divider(height: 100), Text('Can check biometrics: $_canCheckBiometrics\n'), - RaisedButton( + ElevatedButton( child: const Text('Check biometrics'), onPressed: _checkBiometrics, ), + Divider(height: 100), Text('Available biometrics: $_availableBiometrics\n'), - RaisedButton( + ElevatedButton( child: const Text('Get available biometrics'), onPressed: _getAvailableBiometrics, ), + Divider(height: 100), Text('Current State: $_authorized\n'), - RaisedButton( - child: Text(_isAuthenticating ? 'Cancel' : 'Authenticate'), - onPressed: - _isAuthenticating ? _cancelAuthentication : _authenticate, - ) - ])), - )); + (_isAuthenticating) + ? ElevatedButton( + onPressed: _cancelAuthentication, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text("Cancel Authentication"), + Icon(Icons.cancel), + ], + ), + ) + : Column( + children: [ + ElevatedButton( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Authenticate'), + Icon(Icons.perm_device_information), + ], + ), + onPressed: _authenticate, + ), + ElevatedButton( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(_isAuthenticating + ? 'Cancel' + : 'Authenticate: biometrics only'), + Icon(Icons.fingerprint), + ], + ), + onPressed: _authenticateWithBiometrics, + ), + ], + ), + ], + ), + ], + ), + ), + ); } } + +enum _SupportState { + unknown, + supported, + unsupported, +} diff --git a/packages/local_auth/example/pubspec.yaml b/packages/local_auth/example/pubspec.yaml index 8d17a43efb26..ff7a27203019 100644 --- a/packages/local_auth/example/pubspec.yaml +++ b/packages/local_auth/example/pubspec.yaml @@ -1,18 +1,28 @@ name: local_auth_example description: Demonstrates how to use the local_auth plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.12.13+hotfix.5" 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: - path: ../../integration_test + sdk: flutter flutter_driver: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/local_auth/example/test_driver/integration_test.dart b/packages/local_auth/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/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/integration_test/local_auth_test.dart b/packages/local_auth/integration_test/local_auth_test.dart deleted file mode 100644 index 47e5dfa67912..000000000000 --- a/packages/local_auth/integration_test/local_auth_test.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; - -import 'package:local_auth/local_auth.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('canCheckBiometrics', (WidgetTester tester) async { - expect(LocalAuthentication().getAvailableBiometrics(), completion(isList)); - }); -} diff --git a/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.h b/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.h index 1e9e8c3a2d24..1a1446fb27bd 100644 --- a/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.h +++ b/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.h @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.m b/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.m index aa0c217ef543..a00c7eed2703 100644 --- a/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.m +++ b/packages/local_auth/ios/Classes/FLTLocalAuthPlugin.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #import @@ -6,11 +6,17 @@ #import "FLTLocalAuthPlugin.h" @interface FLTLocalAuthPlugin () -@property(copy, nullable) NSDictionary *lastCallArgs; -@property(nullable) FlutterResult lastResult; +@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 +@implementation FLTLocalAuthPlugin { + NSMutableArray *_authContextOverrides; +} + (void)registerWithRegistrar:(NSObject *)registrar { FlutterMethodChannel *channel = @@ -22,10 +28,17 @@ + (void)registerWithRegistrar:(NSObject *)registrar { } - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if ([@"authenticateWithBiometrics" isEqualToString:call.method]) { - [self authenticateWithBiometrics:call.arguments withFlutterResult:result]; + if ([@"authenticate" isEqualToString:call.method]) { + bool isBiometricOnly = [call.arguments[@"biometricOnly"] boolValue]; + if (isBiometricOnly) { + [self authenticateWithBiometrics:call.arguments withFlutterResult:result]; + } else { + [self authenticate:call.arguments withFlutterResult:result]; + } } else if ([@"getAvailableBiometrics" isEqualToString:call.method]) { [self getAvailableBiometrics:result]; + } else if ([@"isDeviceSupported" isEqualToString:call.method]) { + result(@YES); } else { result(FlutterMethodNotImplemented); } @@ -33,6 +46,19 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result #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 @@ -68,7 +94,7 @@ - (void)alertMessage:(NSString *)message } - (void)getAvailableBiometrics:(FlutterResult)result { - LAContext *context = [[LAContext alloc] init]; + LAContext *context = self.createAuthContext; NSError *authError = nil; NSMutableArray *biometrics = [[NSMutableArray alloc] init]; if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics @@ -92,7 +118,7 @@ - (void)getAvailableBiometrics:(FlutterResult)result { - (void)authenticateWithBiometrics:(NSDictionary *)arguments withFlutterResult:(FlutterResult)result { - LAContext *context = [[LAContext alloc] init]; + LAContext *context = self.createAuthContext; NSError *authError = nil; self.lastCallArgs = nil; self.lastResult = nil; @@ -103,33 +129,71 @@ - (void)authenticateWithBiometrics:(NSDictionary *)arguments [context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:arguments[@"localizedReason"] reply:^(BOOL success, NSError *error) { - if (success) { - result(@YES); - } else { - switch (error.code) { - case LAErrorPasscodeNotSet: - case LAErrorTouchIDNotAvailable: - case LAErrorTouchIDNotEnrolled: - case LAErrorTouchIDLockout: - [self handleErrors:error - flutterArguments:arguments - withFlutterResult:result]; - return; - case LAErrorSystemCancel: - if ([arguments[@"stickyAuth"] boolValue]) { - self.lastCallArgs = arguments; - self.lastResult = result; - return; - } - } - result(@NO); - } + 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 (@available(iOS 9.0, *)) { + 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]; + } + } else { + // Fallback on earlier versions + } +} + +- (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 { diff --git a/packages/local_auth/lib/auth_strings.dart b/packages/local_auth/lib/auth_strings.dart index a8f34f88723c..537340b79d4e 100644 --- a/packages/local_auth/lib/auth_strings.dart +++ b/packages/local_auth/lib/auth_strings.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -15,38 +15,46 @@ import 'package:intl/intl.dart'; /// Provides default values for all messages. class AndroidAuthMessages { const AndroidAuthMessages({ - this.fingerprintHint, - this.fingerprintNotRecognized, - this.fingerprintSuccess, + this.biometricHint, + this.biometricNotRecognized, + this.biometricRequiredTitle, + this.biometricSuccess, this.cancelButton, - this.signInTitle, - this.fingerprintRequiredTitle, + this.deviceCredentialsRequiredTitle, + this.deviceCredentialsSetupDescription, this.goToSettingsButton, this.goToSettingsDescription, + this.signInTitle, }); - final String fingerprintHint; - final String fingerprintNotRecognized; - final String fingerprintSuccess; - final String cancelButton; - final String signInTitle; - final String fingerprintRequiredTitle; - final String goToSettingsButton; - final String goToSettingsDescription; + final String? biometricHint; + final String? biometricNotRecognized; + final String? biometricRequiredTitle; + final String? biometricSuccess; + final String? cancelButton; + final String? deviceCredentialsRequiredTitle; + final String? deviceCredentialsSetupDescription; + final String? goToSettingsButton; + final String? goToSettingsDescription; + final String? signInTitle; Map get args { return { - 'fingerprintHint': fingerprintHint ?? androidFingerprintHint, - 'fingerprintNotRecognized': - fingerprintNotRecognized ?? androidFingerprintNotRecognized, - 'fingerprintSuccess': fingerprintSuccess ?? androidFingerprintSuccess, + 'biometricHint': biometricHint ?? androidBiometricHint, + 'biometricNotRecognized': + biometricNotRecognized ?? androidBiometricNotRecognized, + 'biometricSuccess': biometricSuccess ?? androidBiometricSuccess, + 'biometricRequired': + biometricRequiredTitle ?? androidBiometricRequiredTitle, 'cancelButton': cancelButton ?? androidCancelButton, - 'signInTitle': signInTitle ?? androidSignInTitle, - 'fingerprintRequired': - fingerprintRequiredTitle ?? androidFingerprintRequiredTitle, + 'deviceCredentialsRequired': deviceCredentialsRequiredTitle ?? + androidDeviceCredentialsRequiredTitle, + 'deviceCredentialsSetupDescription': deviceCredentialsSetupDescription ?? + androidDeviceCredentialsSetupDescription, 'goToSetting': goToSettingsButton ?? goToSettings, 'goToSettingDescription': goToSettingsDescription ?? androidGoToSettingsDescription, + 'signInTitle': signInTitle ?? androidSignInTitle, }; } } @@ -62,10 +70,10 @@ class IOSAuthMessages { this.cancelButton, }); - final String lockOut; - final String goToSettingsButton; - final String goToSettingsDescription; - final String cancelButton; + final String? lockOut; + final String? goToSettingsButton; + final String? goToSettingsDescription; + final String? cancelButton; Map get args { return { @@ -80,16 +88,17 @@ class IOSAuthMessages { // Strings for local_authentication plugin. Currently supports English. // Intl.message must be string literals. -String get androidFingerprintHint => Intl.message('Touch sensor', - desc: 'Hint message advising the user how to scan their fingerprint. It is ' +String get androidBiometricHint => Intl.message('Verify identity', + desc: + 'Hint message advising the user how to authenticate with biometrics. It is ' 'used on Android side. Maximum 60 characters.'); -String get androidFingerprintNotRecognized => - Intl.message('Fingerprint not recognized. Try again.', +String get androidBiometricNotRecognized => + Intl.message('Not recognized. Try again.', desc: 'Message to let the user know that authentication was failed. It ' 'is used on Android side. Maximum 60 characters.'); -String get androidFingerprintSuccess => Intl.message('Fingerprint recognized.', +String get androidBiometricSuccess => Intl.message('Success', desc: 'Message to let the user know that authentication was successful. It ' 'is used on Android side. Maximum 60 characters.'); @@ -97,17 +106,26 @@ String get androidCancelButton => Intl.message('Cancel', desc: 'Message showed on a button that the user can click to leave the ' 'current dialog. It is used on Android side. Maximum 30 characters.'); -String get androidSignInTitle => Intl.message('Fingerprint Authentication', +String get androidSignInTitle => Intl.message('Authentication required', desc: 'Message showed as a title in a dialog which indicates the user ' - 'that they need to scan fingerprint to continue. It is used on ' + 'that they need to scan biometric to continue. It is used on ' 'Android side. Maximum 60 characters.'); -String get androidFingerprintRequiredTitle { - return Intl.message('Fingerprint required', - desc: 'Message showed as a title in a dialog which indicates the user ' - 'fingerprint is not set up yet on their device. It is used on Android' - ' side. Maximum 60 characters.'); -} +String get androidBiometricRequiredTitle => Intl.message('Biometric required', + desc: 'Message showed as a title in a dialog which indicates the user ' + 'has not set up biometric authentication on their device. It is used on Android' + ' side. Maximum 60 characters.'); + +String get androidDeviceCredentialsRequiredTitle => Intl.message( + 'Device credentials required', + desc: 'Message showed as a title in a dialog which indicates the user ' + 'has not set up credentials authentication on their device. It is used on Android' + ' side. Maximum 60 characters.'); + +String get androidDeviceCredentialsSetupDescription => Intl.message( + 'Device credentials required', + desc: 'Message advising the user to go to the settings and configure ' + 'device credentials on their device. It shows in a dialog on Android side.'); String get goToSettings => Intl.message('Go to settings', desc: 'Message showed on a button that the user can click to go to ' @@ -115,10 +133,10 @@ String get goToSettings => Intl.message('Go to settings', 'and iOS side. Maximum 30 characters.'); String get androidGoToSettingsDescription => Intl.message( - 'Fingerprint is not set up on your device. Go to ' - '\'Settings > Security\' to add your fingerprint.', + 'Biometric authentication is not set up on your device. Go to ' + '\'Settings > Security\' to add biometric authentication.', desc: 'Message advising the user to go to the settings and configure ' - 'fingerprint on their device. It shows in a dialog on Android side.'); + 'biometric on their device. It shows in a dialog on Android side.'); String get iOSLockOut => Intl.message( 'Biometric authentication is disabled. Please lock and unlock your screen to ' diff --git a/packages/local_auth/lib/error_codes.dart b/packages/local_auth/lib/error_codes.dart index 3f6f298ba4f3..bcf15b7b2154 100644 --- a/packages/local_auth/lib/error_codes.dart +++ b/packages/local_auth/lib/error_codes.dart @@ -1,9 +1,9 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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 -// `authenticateWithBiometrics`. +// `authenticate`. /// Indicates that the user has not yet configured a passcode (iOS) or /// PIN/pattern/password (Android) on the device. diff --git a/packages/local_auth/lib/local_auth.dart b/packages/local_auth/lib/local_auth.dart index b2b03b920d64..0b75a83d4029 100644 --- a/packages/local_auth/lib/local_auth.dart +++ b/packages/local_auth/lib/local_auth.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -30,14 +30,36 @@ void setMockPathProviderPlatform(Platform platform) { /// A Flutter plugin for authenticating the user identity locally. class LocalAuthentication { - /// Authenticates the user with biometrics available on the device. + /// The `authenticateWithBiometrics` method has been deprecated. + /// Use `authenticate` with `biometricOnly: true` instead + @Deprecated("Use `authenticate` with `biometricOnly: true` instead") + Future authenticateWithBiometrics({ + required String localizedReason, + bool useErrorDialogs = true, + bool stickyAuth = false, + AndroidAuthMessages androidAuthStrings = const AndroidAuthMessages(), + IOSAuthMessages iOSAuthStrings = const IOSAuthMessages(), + bool sensitiveTransaction = true, + }) => + authenticate( + localizedReason: localizedReason, + useErrorDialogs: useErrorDialogs, + stickyAuth: stickyAuth, + androidAuthStrings: androidAuthStrings, + iOSAuthStrings: iOSAuthStrings, + sensitiveTransaction: sensitiveTransaction, + biometricOnly: true, + ); + + /// Authenticates the user with biometrics available on the device while also + /// allowing the user to use device authentication - pin, pattern, passcode. /// /// Returns a [Future] holding true, if the user successfully authenticated, /// false otherwise. /// /// [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.' + /// 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 @@ -62,24 +84,30 @@ class LocalAuthentication { /// dialog after the face is recognized to make sure the user meant to unlock /// their phone. /// + /// Setting [biometricOnly] to true prevents authenticates from using non-biometric + /// local authentication such as pin, passcode, and passcode. + /// /// Throws an [PlatformException] if there were technical problems with local /// authentication (e.g. lack of relevant hardware). This might throw /// [PlatformException] with error code [otherOperatingSystem] on the iOS /// simulator. - Future authenticateWithBiometrics({ - @required String localizedReason, + Future authenticate({ + required String localizedReason, bool useErrorDialogs = true, bool stickyAuth = false, AndroidAuthMessages androidAuthStrings = const AndroidAuthMessages(), IOSAuthMessages iOSAuthStrings = const IOSAuthMessages(), bool sensitiveTransaction = true, + bool biometricOnly = false, }) async { - assert(localizedReason != null); + assert(localizedReason.isNotEmpty); + final Map args = { 'localizedReason': localizedReason, 'useErrorDialogs': useErrorDialogs, 'stickyAuth': stickyAuth, 'sensitiveTransaction': sensitiveTransaction, + 'biometricOnly': biometricOnly, }; if (_platform.isIOS) { args.addAll(iOSAuthStrings.args); @@ -87,13 +115,13 @@ class LocalAuthentication { args.addAll(androidAuthStrings.args); } else { throw PlatformException( - code: otherOperatingSystem, - message: 'Local authentication does not support non-Android/iOS ' - 'operating systems.', - details: 'Your operating system is ${_platform.operatingSystem}'); + code: otherOperatingSystem, + message: 'Local authentication does not support non-Android/iOS ' + 'operating systems.', + details: 'Your operating system is ${_platform.operatingSystem}', + ); } - return await _channel.invokeMethod( - 'authenticateWithBiometrics', args); + return (await _channel.invokeMethod('authenticate', args)) ?? false; } /// Returns true if auth was cancelled successfully. @@ -101,20 +129,27 @@ class LocalAuthentication { /// Returns false if there was some error or no auth in progress. /// /// Returns [Future] bool true or false: - Future stopAuthentication() { + Future stopAuthentication() async { if (_platform.isAndroid) { - return _channel.invokeMethod('stopAuthentication'); + return await _channel.invokeMethod('stopAuthentication') ?? false; } - return Future.sync(() => true); + return true; } /// Returns true if device is capable of checking biometrics /// /// Returns a [Future] bool true or false: Future get canCheckBiometrics async => - (await _channel.invokeListMethod('getAvailableBiometrics')) + (await _channel.invokeListMethod('getAvailableBiometrics'))! .isNotEmpty; + /// Returns true if device is capable of checking biometrics or is able to + /// fail over to device credentials. + /// + /// Returns a [Future] bool true or false: + Future isDeviceSupported() async => + (await _channel.invokeMethod('isDeviceSupported')) ?? false; + /// Returns a list of enrolled biometrics /// /// Returns a [Future] List with the following possibilities: @@ -122,8 +157,10 @@ class LocalAuthentication { /// - BiometricType.fingerprint /// - BiometricType.iris (not yet implemented) Future> getAvailableBiometrics() async { - final List result = - (await _channel.invokeListMethod('getAvailableBiometrics')); + final List result = (await _channel.invokeListMethod( + 'getAvailableBiometrics', + )) ?? + []; final List biometrics = []; result.forEach((String value) { switch (value) { diff --git a/packages/local_auth/pubspec.yaml b/packages/local_auth/pubspec.yaml index db34a461e0d8..f50492381586 100644 --- a/packages/local_auth/pubspec.yaml +++ b/packages/local_auth/pubspec.yaml @@ -1,8 +1,13 @@ name: local_auth -description: Flutter plugin for Android and iOS device authentication sensors - such as Fingerprint Reader and Touch ID. -homepage: https://github.com/flutter/plugins/tree/master/packages/local_auth -version: 0.6.3+2 +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.6 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: @@ -16,20 +21,16 @@ flutter: dependencies: flutter: sdk: flutter - meta: ^1.0.5 - intl: ">=0.15.1 <0.17.0" - platform: ">=2.0.0 <4.0.0" - flutter_plugin_android_lifecycle: ^1.0.2 + flutter_plugin_android_lifecycle: ^2.0.1 + intl: ^0.17.0 + meta: ^1.3.0 + platform: ^3.0.0 dev_dependencies: - integration_test: - path: ../integration_test flutter_driver: sdk: flutter flutter_test: sdk: flutter - pedantic: ^1.8.0 - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + 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 index 205c5f785708..b24de8bd3c11 100644 --- a/packages/local_auth/test/local_auth_test.dart +++ b/packages/local_auth/test/local_auth_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -19,7 +19,7 @@ void main() { ); final List log = []; - LocalAuthentication localAuthentication; + late LocalAuthentication localAuthentication; setUp(() { channel.setMockMethodCallHandler((MethodCall methodCall) { @@ -30,61 +30,146 @@ void main() { log.clear(); }); - test('authenticate with no args on Android.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); - await localAuthentication.authenticateWithBiometrics( - localizedReason: 'Needs secure'); - expect( - log, - [ - isMethodCall('authenticateWithBiometrics', - arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - }..addAll(const AndroidAuthMessages().args)), - ], - ); - }); + group("With device auth fail over", () { + test('authenticate with no args on Android.', () async { + setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); + await localAuthentication.authenticate( + localizedReason: 'Needs secure', + biometricOnly: true, + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Needs secure', + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': true, + }..addAll(const AndroidAuthMessages().args)), + ], + ); + }); + + test('authenticate with no args on iOS.', () async { + setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios')); + await localAuthentication.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 args on iOS.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios')); - await localAuthentication.authenticateWithBiometrics( - localizedReason: 'Needs secure'); - expect( - log, - [ - isMethodCall('authenticateWithBiometrics', - arguments: { - 'localizedReason': 'Needs secure', - 'useErrorDialogs': true, - 'stickyAuth': false, - 'sensitiveTransaction': true, - }..addAll(const IOSAuthMessages().args)), - ], - ); + test('authenticate with no sensitive transaction.', () async { + setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); + await localAuthentication.authenticate( + localizedReason: 'Insecure', + sensitiveTransaction: false, + useErrorDialogs: false, + biometricOnly: true, + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Insecure', + 'useErrorDialogs': false, + 'stickyAuth': false, + 'sensitiveTransaction': false, + 'biometricOnly': true, + }..addAll(const AndroidAuthMessages().args)), + ], + ); + }); }); - test('authenticate with no sensitive transaction.', () async { - setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); - await localAuthentication.authenticateWithBiometrics( - localizedReason: 'Insecure', - sensitiveTransaction: false, - useErrorDialogs: false, - ); - expect( - log, - [ - isMethodCall('authenticateWithBiometrics', - arguments: { - 'localizedReason': 'Insecure', - 'useErrorDialogs': false, - 'stickyAuth': false, - 'sensitiveTransaction': false, - }..addAll(const AndroidAuthMessages().args)), - ], - ); + group("With biometrics only", () { + test('authenticate with no args on Android.', () async { + setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); + await localAuthentication.authenticate( + localizedReason: 'Needs secure', + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Needs secure', + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': false, + }..addAll(const AndroidAuthMessages().args)), + ], + ); + }); + + test('authenticate with no args on iOS.', () async { + setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios')); + await localAuthentication.authenticate( + localizedReason: 'Needs secure', + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Needs secure', + 'useErrorDialogs': true, + 'stickyAuth': false, + 'sensitiveTransaction': true, + 'biometricOnly': false, + }..addAll(const IOSAuthMessages().args)), + ], + ); + }); + + test('authenticate with no sensitive transaction.', () async { + setMockPathProviderPlatform(FakePlatform(operatingSystem: 'android')); + await localAuthentication.authenticate( + localizedReason: 'Insecure', + sensitiveTransaction: false, + useErrorDialogs: false, + ); + expect( + log, + [ + isMethodCall('authenticate', + arguments: { + 'localizedReason': 'Insecure', + 'useErrorDialogs': false, + 'stickyAuth': false, + 'sensitiveTransaction': false, + 'biometricOnly': false, + }..addAll(const AndroidAuthMessages().args)), + ], + ); + }); }); }); } diff --git a/packages/package_info/AUTHORS b/packages/package_info/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/package_info/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/package_info/CHANGELOG.md b/packages/package_info/CHANGELOG.md index f88c270d92a3..96697dd220e6 100644 --- a/packages/package_info/CHANGELOG.md +++ b/packages/package_info/CHANGELOG.md @@ -1,3 +1,32 @@ +## 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 diff --git a/packages/package_info/LICENSE b/packages/package_info/LICENSE index a6d6c0749818..c6823b81eb84 100644 --- a/packages/package_info/LICENSE +++ b/packages/package_info/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017 The Chromium Authors. All rights reserved. +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: diff --git a/packages/package_info/README.md b/packages/package_info/README.md index b5b2405a231a..80893880f3c2 100644 --- a/packages/package_info/README.md +++ b/packages/package_info/README.md @@ -1,14 +1,22 @@ # PackageInfo -This Flutter plugin provides an API for querying information about an -application package. +--- + +## 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. -**Please set your constraint to `package_info: '>=0.4.y+x <2.0.0'`** +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. -## Backward compatible 1.0.0 version is coming -The package_info plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.4.y+z`. -Please use `package_info: '>=0.4.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 +--- + +This Flutter plugin provides an API for querying information about an +application package. # Usage @@ -39,10 +47,10 @@ PackageInfo.fromPlatform().then((PackageInfo packageInfo) { ## Known Issue -As noted on [issue 20761](https://github.com/flutter/flutter/issues/20761#issuecomment-493434578), package_info on iOS -requires the Xcode build folder to be rebuilt after changes to the version string in `pubspec.yaml`. -Clean the Xcode build folder with: -`XCode Menu -> Product -> (Holding Option Key) Clean build folder`. +As noted on [issue 20761](https://github.com/flutter/flutter/issues/20761#issuecomment-493434578), package_info on iOS +requires the Xcode build folder to be rebuilt after changes to the version string in `pubspec.yaml`. +Clean the Xcode build folder with: +`XCode Menu -> Product -> (Holding Option Key) Clean build folder`. ## Issues and feedback diff --git a/packages/package_info/analysis_options.yaml b/packages/package_info/analysis_options.yaml new file mode 100644 index 000000000000..cda4f6e153e6 --- /dev/null +++ b/packages/package_info/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options_legacy.yaml diff --git a/packages/package_info/android/build.gradle b/packages/package_info/android/build.gradle index d05fa363d3c1..9144e6aade58 100644 --- a/packages/package_info/android/build.gradle +++ b/packages/package_info/android/build.gradle @@ -4,7 +4,7 @@ version '1.0-SNAPSHOT' buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -15,14 +15,14 @@ buildscript { rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } apply plugin: 'com.android.library' android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { minSdkVersion 16 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 index 645b8dd075c6..4611f70951f9 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/package_info/darwin/Classes/FLTPackageInfoPlugin.m b/packages/package_info/darwin/Classes/FLTPackageInfoPlugin.m index 046f15fec3ea..ab686fa08676 100644 --- a/packages/package_info/darwin/Classes/FLTPackageInfoPlugin.m +++ b/packages/package_info/darwin/Classes/FLTPackageInfoPlugin.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/package_info/example/README.md b/packages/package_info/example/README.md index 4ca79663ac53..762d04ec0532 100644 --- a/packages/package_info/example/README.md +++ b/packages/package_info/example/README.md @@ -5,4 +5,4 @@ Demonstrates how to use the package_info plugin. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). diff --git a/packages/package_info/example/android/app/build.gradle b/packages/package_info/example/android/app/build.gradle index c24eba0fe81d..5099f3213fd8 100644 --- a/packages/package_info/example/android/app/build.gradle +++ b/packages/package_info/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 28 + compileSdkVersion 29 lintOptions { disable 'InvalidPackage' diff --git a/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/packageinfoexample/EmbedderV1ActivityTest.java b/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/packageinfoexample/EmbedderV1ActivityTest.java index ab1d2d29635e..8d3b0b6c6cad 100644 --- a/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/packageinfoexample/EmbedderV1ActivityTest.java +++ b/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/packageinfoexample/EmbedderV1ActivityTest.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/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 index 91aad52d4241..cf7252ce19de 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/package_info/example/android/app/src/main/java/io/flutter/plugins/packageinfoexample/EmbedderV1Activity.java b/packages/package_info/example/android/app/src/main/java/io/flutter/plugins/packageinfoexample/EmbedderV1Activity.java index a32c50484838..ded5f348c506 100644 --- a/packages/package_info/example/android/app/src/main/java/io/flutter/plugins/packageinfoexample/EmbedderV1Activity.java +++ b/packages/package_info/example/android/app/src/main/java/io/flutter/plugins/packageinfoexample/EmbedderV1Activity.java @@ -1,10 +1,11 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // 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 android.os.Bundle; +import dev.flutter.plugins.integration_test.IntegrationTestPlugin; import io.flutter.app.FlutterActivity; import io.flutter.plugins.packageinfo.PackageInfoPlugin; @@ -14,5 +15,7 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); PackageInfoPlugin.registerWith( registrarFor("io.flutter.plugins.packageinfo.PackageInfoPlugin")); + IntegrationTestPlugin.registerWith( + registrarFor("dev.flutter.plugins.integration_test.IntegrationTestPlugin")); } } diff --git a/packages/package_info/example/android/build.gradle b/packages/package_info/example/android/build.gradle index d5e73b13a253..64450a26d537 100644 --- a/packages/package_info/example/android/build.gradle +++ b/packages/package_info/example/android/build.gradle @@ -1,7 +1,7 @@ buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -12,7 +12,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/packages/package_info/example/integration_test/package_info_test.dart b/packages/package_info/example/integration_test/package_info_test.dart index 5038509ec84f..ab8f5f38b472 100644 --- a/packages/package_info/example/integration_test/package_info_test.dart +++ b/packages/package_info/example/integration_test/package_info_test.dart @@ -1,6 +1,8 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter 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'; @@ -22,12 +24,12 @@ void main() { } else if (Platform.isIOS) { expect(info.appName, 'Package Info Example'); expect(info.buildNumber, '1'); - expect(info.packageName, 'io.flutter.plugins.packageInfoExample'); + 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, 'io.flutter.plugins.packageInfoExample'); + expect(info.packageName, 'dev.flutter.plugins.packageInfoExample'); expect(info.version, '1.0.0'); } else { throw (UnsupportedError('platform not supported')); @@ -47,13 +49,13 @@ void main() { expect(find.text('Package Info Example'), findsOneWidget); expect(find.text('1'), findsOneWidget); expect( - find.text('io.flutter.plugins.packageInfoExample'), findsOneWidget); + 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('io.flutter.plugins.packageInfoExample'), findsOneWidget); + 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/Podfile b/packages/package_info/example/ios/Podfile new file mode 100644 index 000000000000..f7d6a5e68c3a --- /dev/null +++ b/packages/package_info/example/ios/Podfile @@ -0,0 +1,38 @@ +# 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 index a3c977a0bf14..dd979713357f 100644 --- a/packages/package_info/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/package_info/example/ios/Runner.xcodeproj/project.pbxproj @@ -177,7 +177,7 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; + ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; @@ -437,7 +437,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.packageInfoExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.packageInfoExample; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -458,7 +458,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.packageInfoExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.packageInfoExample; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; diff --git a/packages/package_info/example/ios/Runner/AppDelegate.h b/packages/package_info/example/ios/Runner/AppDelegate.h index d9e18e990f2e..0681d288bb70 100644 --- a/packages/package_info/example/ios/Runner/AppDelegate.h +++ b/packages/package_info/example/ios/Runner/AppDelegate.h @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/package_info/example/ios/Runner/AppDelegate.m b/packages/package_info/example/ios/Runner/AppDelegate.m index f08675707182..30b87969f44a 100644 --- a/packages/package_info/example/ios/Runner/AppDelegate.m +++ b/packages/package_info/example/ios/Runner/AppDelegate.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/package_info/example/ios/Runner/main.m b/packages/package_info/example/ios/Runner/main.m index bec320c0bee0..f97b9ef5c8a1 100644 --- a/packages/package_info/example/ios/Runner/main.m +++ b/packages/package_info/example/ios/Runner/main.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/package_info/example/lib/main.dart b/packages/package_info/example/lib/main.dart index 91ed910ef21d..60e4a16f7817 100644 --- a/packages/package_info/example/lib/main.dart +++ b/packages/package_info/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -25,7 +25,7 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); + MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @@ -57,7 +57,7 @@ class _MyHomePageState extends State { Widget _infoTile(String title, String subtitle) { return ListTile( title: Text(title), - subtitle: Text(subtitle ?? 'Not set'), + subtitle: Text(subtitle.isNotEmpty ? subtitle : 'Not set'), ); } diff --git a/packages/package_info/example/macos/Podfile b/packages/package_info/example/macos/Podfile new file mode 100644 index 000000000000..dade8dfad0dc --- /dev/null +++ b/packages/package_info/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.11' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/package_info/example/macos/Runner.xcodeproj/project.pbxproj b/packages/package_info/example/macos/Runner.xcodeproj/project.pbxproj index 6e63b7eb69ae..3525d85d6678 100644 --- a/packages/package_info/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/package_info/example/macos/Runner.xcodeproj/project.pbxproj @@ -26,11 +26,7 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; }; - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 9A0CC0B8F23AFE5DF719BADB /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CED91D820ABAEDEBEFEBDBDA /* Pods_Runner.framework */; }; - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; }; - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -50,8 +46,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */, - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */, ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; @@ -72,7 +66,6 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FlutterMacOS.framework; path = Flutter/ephemeral/FlutterMacOS.framework; sourceTree = SOURCE_ROOT; }; 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; @@ -80,7 +73,6 @@ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; B3868D4F5169B9990BB5D1F5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; CED91D820ABAEDEBEFEBDBDA /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - D73912EF22F37F9E000D13A0 /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/ephemeral/App.framework; sourceTree = SOURCE_ROOT; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -88,8 +80,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */, - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */, 9A0CC0B8F23AFE5DF719BADB /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -145,8 +135,6 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - D73912EF22F37F9E000D13A0 /* App.framework */, - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */, ); path = Flutter; sourceTree = ""; @@ -280,7 +268,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename\n"; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; }; 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -329,10 +317,13 @@ buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/package_info/package_info.framework", ); name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/packages/package_info/example/macos/Runner/AppDelegate.swift b/packages/package_info/example/macos/Runner/AppDelegate.swift index d53ef6437726..5cec4c48f620 100644 --- a/packages/package_info/example/macos/Runner/AppDelegate.swift +++ b/packages/package_info/example/macos/Runner/AppDelegate.swift @@ -1,3 +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 Cocoa import FlutterMacOS diff --git a/packages/package_info/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/package_info/example/macos/Runner/Configs/AppInfo.xcconfig index b5fb184b14ff..4ecd23f86c29 100644 --- a/packages/package_info/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/packages/package_info/example/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = Package Info Example // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.packageInfoExample +PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.packageInfoExample // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2020 io.flutter.plugins. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2020 The Flutter Authors. All rights reserved. diff --git a/packages/package_info/example/macos/Runner/MainFlutterWindow.swift b/packages/package_info/example/macos/Runner/MainFlutterWindow.swift index 2722837ec918..32aaeedceb1f 100644 --- a/packages/package_info/example/macos/Runner/MainFlutterWindow.swift +++ b/packages/package_info/example/macos/Runner/MainFlutterWindow.swift @@ -1,3 +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 Cocoa import FlutterMacOS diff --git a/packages/package_info/example/pubspec.yaml b/packages/package_info/example/pubspec.yaml index a6dbbce44811..9c1c3cd0ad6e 100644 --- a/packages/package_info/example/pubspec.yaml +++ b/packages/package_info/example/pubspec.yaml @@ -1,19 +1,28 @@ 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: - path: ../../integration_test + sdk: flutter dev_dependencies: flutter_driver: sdk: flutter - test: any - pedantic: ^1.8.0 + 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 index f532c389a02b..6a0e6fa82dbe 100644 --- a/packages/package_info/example/test_driver/integration_test.dart +++ b/packages/package_info/example/test_driver/integration_test.dart @@ -1,19 +1,9 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. -import 'dart:convert'; -import 'dart:io'; +// @dart=2.9 -import 'package:flutter_driver/flutter_driver.dart'; +import 'package:integration_test/integration_test_driver.dart'; -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String data = await driver.requestData( - null, - timeout: const Duration(minutes: 1), - ); - await driver.close(); - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} +Future main() => integrationDriver(); diff --git a/packages/package_info/ios/Classes/FLTPackageInfoPlugin.h b/packages/package_info/ios/Classes/FLTPackageInfoPlugin.h index 5f58c82c9446..65be5f99d569 100644 --- a/packages/package_info/ios/Classes/FLTPackageInfoPlugin.h +++ b/packages/package_info/ios/Classes/FLTPackageInfoPlugin.h @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/package_info/lib/package_info.dart b/packages/package_info/lib/package_info.dart index eaf28597e56c..69246813873a 100644 --- a/packages/package_info/lib/package_info.dart +++ b/packages/package_info/lib/package_info.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -24,30 +24,31 @@ class PackageInfo { /// See [fromPlatform] for the right API to get a [PackageInfo] that's /// actually populated with real data. PackageInfo({ - this.appName, - this.packageName, - this.version, - this.buildNumber, + required this.appName, + required this.packageName, + required this.version, + required this.buildNumber, }); - static PackageInfo _fromPlatform; + static PackageInfo? _fromPlatform; /// Retrieves package information from the platform. /// The result is cached. static Future fromPlatform() async { - if (_fromPlatform != null) { - return _fromPlatform; - } + PackageInfo? packageInfo = _fromPlatform; + if (packageInfo != null) return packageInfo; final Map map = - await _kChannel.invokeMapMethod('getAll'); - _fromPlatform = PackageInfo( - appName: map["appName"], - packageName: map["packageName"], - version: map["version"], - buildNumber: map["buildNumber"], + (await _kChannel.invokeMapMethod('getAll'))!; + + packageInfo = PackageInfo( + appName: map["appName"] ?? '', + packageName: map["packageName"] ?? '', + version: map["version"] ?? '', + buildNumber: map["buildNumber"] ?? '', ); - return _fromPlatform; + _fromPlatform = packageInfo; + return packageInfo; } /// The app name. `CFBundleDisplayName` on iOS, `application/label` on Android. diff --git a/packages/package_info/macos/Classes/FLTPackageInfoPlugin.h b/packages/package_info/macos/Classes/FLTPackageInfoPlugin.h index cb165ad5e41e..590e8263d951 100644 --- a/packages/package_info/macos/Classes/FLTPackageInfoPlugin.h +++ b/packages/package_info/macos/Classes/FLTPackageInfoPlugin.h @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/package_info/macos/package_info.podspec b/packages/package_info/macos/package_info.podspec index 3c342ec6b8c5..dbe5bd9a105b 100644 --- a/packages/package_info/macos/package_info.podspec +++ b/packages/package_info/macos/package_info.podspec @@ -4,14 +4,14 @@ Pod::Spec.new do |s| s.name = 'package_info' s.version = '0.0.1' - s.summary = 'A new flutter plugin project.' + s.summary = 'Flutter plugin for querying information about the application package.' s.description = <<-DESC -A new flutter plugin project. +Flutter plugin for querying information about the application package, based on bundle data. DESC - s.homepage = 'http://example.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } - s.source = { :path => '.' } + 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' diff --git a/packages/package_info/pubspec.yaml b/packages/package_info/pubspec.yaml index 0705dd11d139..dd9de6f14808 100644 --- a/packages/package_info/pubspec.yaml +++ b/packages/package_info/pubspec.yaml @@ -1,11 +1,13 @@ name: package_info description: Flutter plugin for querying information about the application package, such as CFBundleVersion on iOS or versionCode on Android. -homepage: https://github.com/flutter/plugins/tree/master/packages/package_info -# 0.4.y+z is compatible with 1.0.0, if you land a breaking change bump -# the version to 2.0.0. -# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.4.3 +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: @@ -27,11 +29,6 @@ dev_dependencies: sdk: flutter flutter_driver: sdk: flutter - test: any integration_test: - path: ../integration_test - pedantic: ^1.8.0 - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + 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 index 47d48fde2d2d..91661de72103 100644 --- a/packages/package_info/test/package_info_test.dart +++ b/packages/package_info/test/package_info_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -11,7 +11,7 @@ void main() { const MethodChannel channel = MethodChannel('plugins.flutter.io/package_info'); - List log; + late List log; channel.setMockMethodCallHandler((MethodCall methodCall) async { log.add(methodCall); diff --git a/packages/path_provider/path_provider/AUTHORS b/packages/path_provider/path_provider/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/path_provider/path_provider/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/path_provider/path_provider/CHANGELOG.md b/packages/path_provider/path_provider/CHANGELOG.md index fe92462627df..ca05c24eedb7 100644 --- a/packages/path_provider/path_provider/CHANGELOG.md +++ b/packages/path_provider/path_provider/CHANGELOG.md @@ -1,3 +1,60 @@ +## NEXT + +* Add iOS unit test target. + +## 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. +* BREAKING CHANGE: Path accessors that return non-nullable results will throw + a `MissingPlatformDirectoryException` if the platform implementation is unable + to get the corresponding directory (except on platforms where the method is + explicitly unsupported, where they will continue to throw `UnsupportedError`). + +## 1.6.28 + +* Drop unused UUID dependency for tests. + +## 1.6.27 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 1.6.26 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 1.6.25 + +* Update Flutter SDK constraint. + +## 1.6.24 + +* Remove unused `test` dependency. +* Update Dart SDK constraint in example. + +## 1.6.23 + +* Check in windows/ directory for example/ + +## 1.6.22 + +* Switch to guava-android dependency instead of full guava. + +## 1.6.21 + +* Update android compileSdkVersion to 29. + +## 1.6.20 + +* Check in linux/ directory for example/ + ## 1.6.19 * Android implementation does path queries in the background thread rather than UI thread. diff --git a/packages/path_provider/path_provider/LICENSE b/packages/path_provider/path_provider/LICENSE index 447867e0637e..c6823b81eb84 100644 --- a/packages/path_provider/path_provider/LICENSE +++ b/packages/path_provider/path_provider/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017, the Flutter project authors. All rights reserved. +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: diff --git a/packages/path_provider/path_provider/README.md b/packages/path_provider/path_provider/README.md index e8d97e0106a3..47ae6891d294 100644 --- a/packages/path_provider/path_provider/README.md +++ b/packages/path_provider/path_provider/README.md @@ -1,13 +1,13 @@ # path_provider -[![pub package](https://img.shields.io/pub/v/path_provider.svg)](https://pub.dartlang.org/packages/path_provider) +[![pub package](https://img.shields.io/pub/v/path_provider.svg)](https://pub.dev/packages/path_provider) -A Flutter plugin for finding commonly used locations on the filesystem. Supports iOS, Android, Linux and MacOS. +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. ## Usage -To use this plugin, add `path_provider` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). +To use this plugin, add `path_provider` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). ### Example diff --git a/packages/path_provider/path_provider/android/build.gradle b/packages/path_provider/path_provider/android/build.gradle index 91bedf7f29a3..6df60f0a3a63 100644 --- a/packages/path_provider/path_provider/android/build.gradle +++ b/packages/path_provider/path_provider/android/build.gradle @@ -4,7 +4,7 @@ version '1.0-SNAPSHOT' buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -15,14 +15,14 @@ buildscript { rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } apply plugin: 'com.android.library' android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { minSdkVersion 16 @@ -41,6 +41,6 @@ android { dependencies { implementation 'androidx.annotation:annotation:1.1.0' - implementation 'com.google.guava:guava:20.0' + implementation 'com.google.guava:guava:28.1-android' testImplementation 'junit:junit:4.12' } 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 index 9a0c76232fb3..49360809e892 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter 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/src/main/java/io/flutter/plugins/pathprovider/StorageDirectoryMapper.java b/packages/path_provider/path_provider/android/src/main/java/io/flutter/plugins/pathprovider/StorageDirectoryMapper.java index 820509ba86ea..1a77560623a2 100644 --- a/packages/path_provider/path_provider/android/src/main/java/io/flutter/plugins/pathprovider/StorageDirectoryMapper.java +++ b/packages/path_provider/path_provider/android/src/main/java/io/flutter/plugins/pathprovider/StorageDirectoryMapper.java @@ -1,3 +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. + package io.flutter.plugins.pathprovider; import android.os.Build.VERSION; @@ -6,7 +10,6 @@ /** Helps to map the Dart `StorageDirectory` enum to a Android system constant. */ class StorageDirectoryMapper { - /** * Return a Android Environment constant for a Dart Index. * 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/src/test/java/io/flutter/plugins/pathprovider/StorageDirectoryMapperTest.java index 74a4e6d5169d..7469c545b817 100644 --- a/packages/path_provider/path_provider/android/src/test/java/io/flutter/plugins/pathprovider/StorageDirectoryMapperTest.java +++ b/packages/path_provider/path_provider/android/src/test/java/io/flutter/plugins/pathprovider/StorageDirectoryMapperTest.java @@ -1,3 +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. + package io.flutter.plugins.pathprovider; import static org.junit.Assert.assertEquals; @@ -8,7 +12,6 @@ import org.junit.Test; public class StorageDirectoryMapperTest { - @org.junit.Test public void testAndroidType_null() { assertNull(StorageDirectoryMapper.androidType(null)); diff --git a/packages/path_provider/path_provider/example/README.md b/packages/path_provider/path_provider/example/README.md index f1564c63c283..1f8ea7189ccd 100644 --- a/packages/path_provider/path_provider/example/README.md +++ b/packages/path_provider/path_provider/example/README.md @@ -5,4 +5,4 @@ Demonstrates how to use the path_provider plugin. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[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 0404c7203903..e7f1bfb111a2 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 28 + compileSdkVersion 29 lintOptions { disable 'InvalidPackage' diff --git a/packages/path_provider/path_provider/example/android/app/src/androidTest/java/EmbeddingV1ActivityTest.java b/packages/path_provider/path_provider/example/android/app/src/androidTest/java/EmbeddingV1ActivityTest.java index 385740db166d..b6a39a8260ce 100644 --- a/packages/path_provider/path_provider/example/android/app/src/androidTest/java/EmbeddingV1ActivityTest.java +++ b/packages/path_provider/path_provider/example/android/app/src/androidTest/java/EmbeddingV1ActivityTest.java @@ -1,3 +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. package io.flutter.plugins.pathprovider; diff --git a/packages/path_provider/path_provider/example/android/app/src/androidTest/java/MainActivityTest.java b/packages/path_provider/path_provider/example/android/app/src/androidTest/java/MainActivityTest.java index a99767c4ccf9..0380a4397ae6 100644 --- a/packages/path_provider/path_provider/example/android/app/src/androidTest/java/MainActivityTest.java +++ b/packages/path_provider/path_provider/example/android/app/src/androidTest/java/MainActivityTest.java @@ -1,3 +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. package io.flutter.plugins.pathprovider; diff --git a/packages/path_provider/path_provider/example/android/app/src/main/java/io/flutter/plugins/pathproviderexample/EmbeddingV1Activity.java b/packages/path_provider/path_provider/example/android/app/src/main/java/io/flutter/plugins/pathproviderexample/EmbeddingV1Activity.java index 2ff41b25a4fc..997cd1081a0a 100644 --- a/packages/path_provider/path_provider/example/android/app/src/main/java/io/flutter/plugins/pathproviderexample/EmbeddingV1Activity.java +++ b/packages/path_provider/path_provider/example/android/app/src/main/java/io/flutter/plugins/pathproviderexample/EmbeddingV1Activity.java @@ -1,3 +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. package io.flutter.plugins.pathproviderexample; diff --git a/packages/path_provider/path_provider/example/android/build.gradle b/packages/path_provider/path_provider/example/android/build.gradle index 541636cc492a..e101ac08df55 100644 --- a/packages/path_provider/path_provider/example/android/build.gradle +++ b/packages/path_provider/path_provider/example/android/build.gradle @@ -1,7 +1,7 @@ buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -12,7 +12,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/packages/path_provider/path_provider/example/integration_test/path_provider_test.dart b/packages/path_provider/path_provider/example/integration_test/path_provider_test.dart index 8eb8520b5b4b..9f8feee99ee2 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 @@ -1,13 +1,11 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'dart:async'; +// Copyright 2013 The Flutter Authors. All 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_provider/path_provider.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:path_provider/path_provider.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); @@ -32,34 +30,35 @@ void main() { final Directory result = await getLibraryDirectory(); _verifySampleFile(result, 'library'); } else if (Platform.isAndroid) { - final Future result = getLibraryDirectory(); + final Future result = getLibraryDirectory(); expect(result, throwsA(isInstanceOf())); } }); testWidgets('getExternalStorageDirectory', (WidgetTester tester) async { if (Platform.isIOS) { - final Future result = getExternalStorageDirectory(); + final Future result = getExternalStorageDirectory(); expect(result, throwsA(isInstanceOf())); } else if (Platform.isAndroid) { - final Directory result = await getExternalStorageDirectory(); + final Directory? result = await getExternalStorageDirectory(); _verifySampleFile(result, 'externalStorage'); } }); testWidgets('getExternalCacheDirectories', (WidgetTester tester) async { if (Platform.isIOS) { - final Future> result = getExternalCacheDirectories(); + final Future?> result = getExternalCacheDirectories(); expect(result, throwsA(isInstanceOf())); } else if (Platform.isAndroid) { - final List directories = await getExternalCacheDirectories(); - for (Directory result in directories) { + final List? directories = await getExternalCacheDirectories(); + expect(directories, isNotNull); + for (final Directory result in directories!) { _verifySampleFile(result, 'externalCache'); } } }); - final List _allDirs = [ + final List _allDirs = [ null, StorageDirectory.music, StorageDirectory.podcasts, @@ -70,16 +69,17 @@ void main() { StorageDirectory.movies, ]; - for (StorageDirectory type in _allDirs) { + for (final StorageDirectory? type in _allDirs) { test('getExternalStorageDirectories (type: $type)', () async { if (Platform.isIOS) { - final Future> result = + final Future?> result = getExternalStorageDirectories(type: null); expect(result, throwsA(isInstanceOf())); } else if (Platform.isAndroid) { - final List directories = + final List? directories = await getExternalStorageDirectories(type: type); - for (Directory result in directories) { + expect(directories, isNotNull); + for (final Directory result in directories!) { _verifySampleFile(result, '$type'); } } @@ -89,7 +89,11 @@ void main() { /// Verify a file called [name] in [directory] by recreating it with test /// contents when necessary. -void _verifySampleFile(Directory directory, String name) { +void _verifySampleFile(Directory? directory, String name) { + expect(directory, isNotNull); + if (directory == null) { + return; + } final File file = File('${directory.path}/$name'); if (file.existsSync()) { diff --git a/packages/path_provider/path_provider/example/ios/Podfile b/packages/path_provider/path_provider/example/ios/Podfile new file mode 100644 index 000000000000..3924e59aa0f9 --- /dev/null +++ b/packages/path_provider/path_provider/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/path_provider/path_provider/example/ios/Runner.xcodeproj/project.pbxproj b/packages/path_provider/path_provider/example/ios/Runner.xcodeproj/project.pbxproj index eb0222a7c9c5..09c902f41869 100644 --- a/packages/path_provider/path_provider/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/path_provider/path_provider/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,18 +9,26 @@ /* Begin PBXBuildFile section */ 2D9222481EC32A19007564B0 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D9222471EC32A19007564B0 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 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 */; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 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; @@ -28,8 +36,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -37,17 +43,18 @@ /* 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 = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 694A199F61914F41AAFD0B7F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 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 = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -56,6 +63,9 @@ 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 */ @@ -63,12 +73,18 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 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 */ @@ -77,6 +93,8 @@ children = ( 694A199F61914F41AAFD0B7F /* Pods-Runner.debug.xcconfig */, D317CA1E83064E01753D8BB5 /* Pods-Runner.release.xcconfig */, + 6EB685EA3DDA2EED39600D11 /* Pods-RunnerTests.debug.xcconfig */, + 29F2567B3AE74A9113ED3394 /* Pods-RunnerTests.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -84,9 +102,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, @@ -99,6 +115,7 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, + F76AC1DD26671E960040C8BC /* RunnerTests */, 97C146EF1CF9000F007C117D /* Products */, 840012C8B5EDBCF56B0E4AC1 /* Pods */, CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, @@ -109,6 +126,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + F76AC1DC26671E960040C8BC /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -141,10 +159,20 @@ 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 */ @@ -158,7 +186,6 @@ 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, - 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, ); buildRules = ( @@ -170,6 +197,25 @@ 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 */ @@ -177,11 +223,16 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; + 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" */; @@ -198,6 +249,7 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + F76AC1DB26671E960040C8BC /* RunnerTests */, ); }; /* End PBXProject section */ @@ -214,37 +266,51 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F76AC1DA26671E960040C8BC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 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 = ( ); - name = "Thin Binary"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + 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; }; - 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "[CP] Embed Pods Frameworks"; + name = "Thin Binary"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -291,8 +357,24 @@ ); 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; @@ -315,7 +397,6 @@ /* Begin XCBuildConfiguration section */ 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -372,7 +453,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -437,7 +517,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.pathProviderExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.pathProviderExample; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -458,8 +538,36 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.pathProviderExample; + 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; }; @@ -484,6 +592,15 @@ 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/path_provider/path_provider/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/path_provider/path_provider/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 21a3cc14c74e..919434a6254f 100644 --- a/packages/path_provider/path_provider/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/packages/path_provider/path_provider/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,9 +2,6 @@ - - + location = "self:"> diff --git a/packages/path_provider/path_provider/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/path_provider/path_provider/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 3bb3697ef41c..8501fd2bb642 100644 --- a/packages/path_provider/path_provider/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/path_provider/path_provider/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -37,6 +37,16 @@ + + + + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/path_provider/path_provider/example/ios/Runner/AppDelegate.h b/packages/path_provider/path_provider/example/ios/Runner/AppDelegate.h index d9e18e990f2e..0681d288bb70 100644 --- a/packages/path_provider/path_provider/example/ios/Runner/AppDelegate.h +++ b/packages/path_provider/path_provider/example/ios/Runner/AppDelegate.h @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/example/ios/Runner/AppDelegate.m b/packages/path_provider/path_provider/example/ios/Runner/AppDelegate.m index a4b51c88eb60..b790a0a52635 100644 --- a/packages/path_provider/path_provider/example/ios/Runner/AppDelegate.m +++ b/packages/path_provider/path_provider/example/ios/Runner/AppDelegate.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/example/ios/Runner/main.m b/packages/path_provider/path_provider/example/ios/Runner/main.m index bec320c0bee0..f97b9ef5c8a1 100644 --- a/packages/path_provider/path_provider/example/ios/Runner/main.m +++ b/packages/path_provider/path_provider/example/ios/Runner/main.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/example/ios/RunnerTests/Info.plist b/packages/path_provider/path_provider/example/ios/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/path_provider/path_provider/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/path_provider/path_provider/example/ios/RunnerTests/PathProviderTests.m b/packages/path_provider/path_provider/example/ios/RunnerTests/PathProviderTests.m new file mode 100644 index 000000000000..be48ea6b7ddf --- /dev/null +++ b/packages/path_provider/path_provider/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; +@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/example/lib/main.dart b/packages/path_provider/path_provider/example/lib/main.dart index ce496e9d4e63..c0ac126b2a00 100644 --- a/packages/path_provider/path_provider/example/lib/main.dart +++ b/packages/path_provider/path_provider/example/lib/main.dart @@ -1,10 +1,9 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter 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'; @@ -22,13 +21,13 @@ class MyApp extends StatelessWidget { theme: ThemeData( primarySwatch: Colors.blue, ), - home: MyHomePage(title: 'Path Provider'), + home: const MyHomePage(title: 'Path Provider'), ); } } class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); + const MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override @@ -36,13 +35,13 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - Future _tempDirectory; - Future _appSupportDirectory; - Future _appLibraryDirectory; - Future _appDocumentsDirectory; - Future _externalDocumentsDirectory; - Future> _externalStorageDirectories; - Future> _externalCacheDirectories; + Future? _tempDirectory; + Future? _appSupportDirectory; + Future? _appLibraryDirectory; + Future? _appDocumentsDirectory; + Future? _externalDocumentsDirectory; + Future?>? _externalStorageDirectories; + Future?>? _externalCacheDirectories; void _requestTempDirectory() { setState(() { @@ -51,13 +50,13 @@ class _MyHomePageState extends State { } Widget _buildDirectory( - BuildContext context, AsyncSnapshot snapshot) { + BuildContext context, AsyncSnapshot snapshot) { Text text = const Text(''); if (snapshot.connectionState == ConnectionState.done) { if (snapshot.hasError) { text = Text('Error: ${snapshot.error}'); } else if (snapshot.hasData) { - text = Text('path: ${snapshot.data.path}'); + text = Text('path: ${snapshot.data!.path}'); } else { text = const Text('path unavailable'); } @@ -66,14 +65,14 @@ class _MyHomePageState extends State { } Widget _buildDirectories( - BuildContext context, AsyncSnapshot> snapshot) { + BuildContext context, AsyncSnapshot?> snapshot) { Text text = const Text(''); if (snapshot.connectionState == ConnectionState.done) { if (snapshot.hasError) { text = Text('Error: ${snapshot.error}'); } else if (snapshot.hasData) { final String combined = - snapshot.data.map((Directory d) => d.path).join(', '); + snapshot.data!.map((Directory d) => d.path).join(', '); text = Text('paths: $combined'); } else { text = const Text('path unavailable'); @@ -129,57 +128,59 @@ class _MyHomePageState extends State { children: [ Padding( padding: const EdgeInsets.all(16.0), - child: RaisedButton( + child: ElevatedButton( child: const Text('Get Temporary Directory'), onPressed: _requestTempDirectory, ), ), - FutureBuilder( + FutureBuilder( future: _tempDirectory, builder: _buildDirectory), Padding( padding: const EdgeInsets.all(16.0), - child: RaisedButton( + child: ElevatedButton( child: const Text('Get Application Documents Directory'), onPressed: _requestAppDocumentsDirectory, ), ), - FutureBuilder( + FutureBuilder( future: _appDocumentsDirectory, builder: _buildDirectory), Padding( padding: const EdgeInsets.all(16.0), - child: RaisedButton( + child: ElevatedButton( child: const Text('Get Application Support Directory'), onPressed: _requestAppSupportDirectory, ), ), - FutureBuilder( + FutureBuilder( future: _appSupportDirectory, builder: _buildDirectory), Padding( padding: const EdgeInsets.all(16.0), - child: RaisedButton( + child: ElevatedButton( child: const Text('Get Application Library Directory'), onPressed: _requestAppLibraryDirectory, ), ), - FutureBuilder( + FutureBuilder( future: _appLibraryDirectory, builder: _buildDirectory), Padding( padding: const EdgeInsets.all(16.0), - child: RaisedButton( - child: Text( - '${Platform.isIOS ? "External directories are unavailable " "on iOS" : "Get External Storage Directory"}'), + child: ElevatedButton( + child: Text(Platform.isIOS + ? 'External directories are unavailable on iOS' + : 'Get External Storage Directory'), onPressed: Platform.isIOS ? null : _requestExternalStorageDirectory, ), ), - FutureBuilder( + FutureBuilder( future: _externalDocumentsDirectory, builder: _buildDirectory), Column(children: [ Padding( padding: const EdgeInsets.all(16.0), - child: RaisedButton( - child: Text( - '${Platform.isIOS ? "External directories are unavailable " "on iOS" : "Get External Storage Directories"}'), + child: ElevatedButton( + child: Text(Platform.isIOS + ? 'External directories are unavailable on iOS' + : 'Get External Storage Directories'), onPressed: Platform.isIOS ? null : () { @@ -190,21 +191,22 @@ class _MyHomePageState extends State { ), ), ]), - FutureBuilder>( + FutureBuilder?>( future: _externalStorageDirectories, builder: _buildDirectories), Column(children: [ Padding( padding: const EdgeInsets.all(16.0), - child: RaisedButton( - child: Text( - '${Platform.isIOS ? "External directories are unavailable " "on iOS" : "Get External Cache Directories"}'), + child: ElevatedButton( + child: Text(Platform.isIOS + ? 'External directories are unavailable on iOS' + : 'Get External Cache Directories'), onPressed: Platform.isIOS ? null : _requestExternalCacheDirectories, ), ), ]), - FutureBuilder>( + FutureBuilder?>( future: _externalCacheDirectories, builder: _buildDirectories), ], ), diff --git a/packages/path_provider/path_provider/example/linux/.gitignore b/packages/path_provider/path_provider/example/linux/.gitignore new file mode 100644 index 000000000000..d3896c98444f --- /dev/null +++ b/packages/path_provider/path_provider/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/packages/path_provider/path_provider/example/linux/CMakeLists.txt b/packages/path_provider/path_provider/example/linux/CMakeLists.txt new file mode 100644 index 000000000000..70e26b5d1689 --- /dev/null +++ b/packages/path_provider/path_provider/example/linux/CMakeLists.txt @@ -0,0 +1,106 @@ +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +set(BINARY_NAME "example") +set(APPLICATION_ID "dev.flutter.plugins.path_provider_example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Application build +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) +apply_standard_settings(${BINARY_NAME}) +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) +add_dependencies(${BINARY_NAME} flutter_assemble) +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/packages/path_provider/path_provider/example/linux/flutter/CMakeLists.txt b/packages/path_provider/path_provider/example/linux/flutter/CMakeLists.txt new file mode 100644 index 000000000000..4f48a7ced5f4 --- /dev/null +++ b/packages/path_provider/path_provider/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) +pkg_check_modules(BLKID REQUIRED IMPORTED_TARGET blkid) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO + PkgConfig::BLKID +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + linux-x64 ${CMAKE_BUILD_TYPE} +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) 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 new file mode 100644 index 000000000000..e71a16d23d05 --- /dev/null +++ b/packages/path_provider/path_provider/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// 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 new file mode 100644 index 000000000000..e0f0a47bc08f --- /dev/null +++ b/packages/path_provider/path_provider/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// 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/linux/flutter/generated_plugins.cmake b/packages/path_provider/path_provider/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..51436ae8c982 --- /dev/null +++ b/packages/path_provider/path_provider/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,15 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) diff --git a/packages/path_provider/path_provider/example/linux/main.cc b/packages/path_provider/path_provider/example/linux/main.cc new file mode 100644 index 000000000000..88a5fd45ce1b --- /dev/null +++ b/packages/path_provider/path_provider/example/linux/main.cc @@ -0,0 +1,15 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "my_application.h" + +int main(int argc, char** argv) { + // Only X11 is currently supported. + // Wayland support is being developed: + // https://github.com/flutter/flutter/issues/57932. + gdk_set_allowed_backends("x11"); + + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/packages/path_provider/path_provider/example/linux/my_application.cc b/packages/path_provider/path_provider/example/linux/my_application.cc new file mode 100644 index 000000000000..9cb411ba475b --- /dev/null +++ b/packages/path_provider/path_provider/example/linux/my_application.cc @@ -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. + +#include "my_application.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new( + my_application_get_type(), "application-id", APPLICATION_ID, nullptr)); +} diff --git a/packages/path_provider/path_provider/example/linux/my_application.h b/packages/path_provider/path_provider/example/linux/my_application.h new file mode 100644 index 000000000000..6e9f0c3ff665 --- /dev/null +++ b/packages/path_provider/path_provider/example/linux/my_application.h @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/packages/path_provider/path_provider/example/macos/Podfile b/packages/path_provider/path_provider/example/macos/Podfile new file mode 100644 index 000000000000..dade8dfad0dc --- /dev/null +++ b/packages/path_provider/path_provider/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.11' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/path_provider/path_provider/example/macos/Runner/AppDelegate.swift b/packages/path_provider/path_provider/example/macos/Runner/AppDelegate.swift index d53ef6437726..5cec4c48f620 100644 --- a/packages/path_provider/path_provider/example/macos/Runner/AppDelegate.swift +++ b/packages/path_provider/path_provider/example/macos/Runner/AppDelegate.swift @@ -1,3 +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 Cocoa import FlutterMacOS diff --git a/packages/path_provider/path_provider/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/path_provider/path_provider/example/macos/Runner/Configs/AppInfo.xcconfig index 477d8d3d133e..2e7fbeebb87e 100644 --- a/packages/path_provider/path_provider/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/packages/path_provider/path_provider/example/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = path_provider_example // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.pathProviderExample +PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.pathProviderExample // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2019 io.flutter.plugins. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2019 The Flutter Authors. All rights reserved. diff --git a/packages/path_provider/path_provider/example/macos/Runner/MainFlutterWindow.swift b/packages/path_provider/path_provider/example/macos/Runner/MainFlutterWindow.swift index 2722837ec918..32aaeedceb1f 100644 --- a/packages/path_provider/path_provider/example/macos/Runner/MainFlutterWindow.swift +++ b/packages/path_provider/path_provider/example/macos/Runner/MainFlutterWindow.swift @@ -1,3 +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 Cocoa import FlutterMacOS diff --git a/packages/path_provider/path_provider/example/pubspec.yaml b/packages/path_provider/path_provider/example/pubspec.yaml index 983b3b82eb53..61f50ae97262 100644 --- a/packages/path_provider/path_provider/example/pubspec.yaml +++ b/packages/path_provider/path_provider/example/pubspec.yaml @@ -1,19 +1,28 @@ name: path_provider_example description: Demonstrates how to use the path_provider plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.12.13+hotfix.5" dependencies: flutter: sdk: flutter path_provider: + # When depending on this package from a real application you should use: + # path_provider: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: - integration_test: - path: ../../../integration_test flutter_driver: sdk: flutter - test: any - pedantic: ^1.8.0 + integration_test: + sdk: flutter + pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/path_provider/path_provider/example/test_driver/integration_test.dart b/packages/path_provider/path_provider/example/test_driver/integration_test.dart index 7a2c21338786..4f10f2a522f3 100644 --- a/packages/path_provider/path_provider/example/test_driver/integration_test.dart +++ b/packages/path_provider/path_provider/example/test_driver/integration_test.dart @@ -1,17 +1,7 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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'; +import 'package:integration_test/integration_test_driver.dart'; -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String data = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - await driver.close(); - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} +Future main() => integrationDriver(); diff --git a/packages/path_provider/path_provider/example/windows/.gitignore b/packages/path_provider/path_provider/example/windows/.gitignore new file mode 100644 index 000000000000..d492d0d98c8f --- /dev/null +++ b/packages/path_provider/path_provider/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/path_provider/path_provider/example/windows/CMakeLists.txt b/packages/path_provider/path_provider/example/windows/CMakeLists.txt new file mode 100644 index 000000000000..abf90408efb4 --- /dev/null +++ b/packages/path_provider/path_provider/example/windows/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.15) +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/path_provider/path_provider/example/windows/flutter/CMakeLists.txt b/packages/path_provider/path_provider/example/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000000..c7a8c7607d81 --- /dev/null +++ b/packages/path_provider/path_provider/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,101 @@ +cmake_minimum_required(VERSION 3.15) + +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" +) +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 $ +) +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/path_provider/path_provider/example/windows/flutter/generated_plugin_registrant.cc b/packages/path_provider/path_provider/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000000..8b6d4680af38 --- /dev/null +++ b/packages/path_provider/path_provider/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// 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 new file mode 100644 index 000000000000..dc139d85a931 --- /dev/null +++ b/packages/path_provider/path_provider/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// 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/flutter/generated_plugins.cmake b/packages/path_provider/path_provider/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..4d10c2518654 --- /dev/null +++ b/packages/path_provider/path_provider/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,15 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_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) diff --git a/packages/path_provider/path_provider/example/windows/runner/CMakeLists.txt b/packages/path_provider/path_provider/example/windows/runner/CMakeLists.txt new file mode 100644 index 000000000000..977e38b5d1d2 --- /dev/null +++ b/packages/path_provider/path_provider/example/windows/runner/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.15) +project(runner LANGUAGES CXX) + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "run_loop.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/path_provider/path_provider/example/windows/runner/Runner.rc b/packages/path_provider/path_provider/example/windows/runner/Runner.rc new file mode 100644 index 000000000000..dbda44723259 --- /dev/null +++ b/packages/path_provider/path_provider/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", "Flutter Dev" "\0" + VALUE "FileDescription", "A new Flutter project." "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2020 The Flutter Authors. 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/path_provider/path_provider/example/windows/runner/flutter_window.cpp b/packages/path_provider/path_provider/example/windows/runner/flutter_window.cpp new file mode 100644 index 000000000000..8e415602cf3b --- /dev/null +++ b/packages/path_provider/path_provider/example/windows/runner/flutter_window.cpp @@ -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. + +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(RunLoop* run_loop, + const flutter::DartProject& project) + : run_loop_(run_loop), 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()); + run_loop_->RegisterFlutterInstance(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + run_loop_->UnregisterFlutterInstance(flutter_controller_->engine()); + 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 opporutunity 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/path_provider/path_provider/example/windows/runner/flutter_window.h b/packages/path_provider/path_provider/example/windows/runner/flutter_window.h new file mode 100644 index 000000000000..8e9c12bbe022 --- /dev/null +++ b/packages/path_provider/path_provider/example/windows/runner/flutter_window.h @@ -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. + +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "run_loop.h" +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow driven by the |run_loop|, hosting a + // Flutter view running |project|. + explicit FlutterWindow(RunLoop* run_loop, + 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 run loop driving events for this window. + RunLoop* run_loop_; + + // 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/path_provider/path_provider/example/windows/runner/main.cpp b/packages/path_provider/path_provider/example/windows/runner/main.cpp new file mode 100644 index 000000000000..126302b0be18 --- /dev/null +++ b/packages/path_provider/path_provider/example/windows/runner/main.cpp @@ -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. + +#include +#include +#include + +#include "flutter_window.h" +#include "run_loop.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); + + RunLoop run_loop; + + flutter::DartProject project(L"data"); + FlutterWindow window(&run_loop, project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + run_loop.Run(); + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/packages/path_provider/path_provider/example/windows/runner/resource.h b/packages/path_provider/path_provider/example/windows/runner/resource.h new file mode 100644 index 000000000000..d5d958dc4257 --- /dev/null +++ b/packages/path_provider/path_provider/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/path_provider/path_provider/example/windows/runner/resources/app_icon.ico b/packages/path_provider/path_provider/example/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000000..c04e20caf637 Binary files /dev/null and b/packages/path_provider/path_provider/example/windows/runner/resources/app_icon.ico differ diff --git a/packages/path_provider/path_provider/example/windows/runner/run_loop.cpp b/packages/path_provider/path_provider/example/windows/runner/run_loop.cpp new file mode 100644 index 000000000000..1916500e6440 --- /dev/null +++ b/packages/path_provider/path_provider/example/windows/runner/run_loop.cpp @@ -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. + +#include "run_loop.h" + +#include + +#include + +RunLoop::RunLoop() {} + +RunLoop::~RunLoop() {} + +void RunLoop::Run() { + bool keep_running = true; + TimePoint next_flutter_event_time = TimePoint::clock::now(); + while (keep_running) { + std::chrono::nanoseconds wait_duration = + std::max(std::chrono::nanoseconds(0), + next_flutter_event_time - TimePoint::clock::now()); + ::MsgWaitForMultipleObjects( + 0, nullptr, FALSE, static_cast(wait_duration.count() / 1000), + QS_ALLINPUT); + bool processed_events = false; + MSG message; + // All pending Windows messages must be processed; MsgWaitForMultipleObjects + // won't return again for items left in the queue after PeekMessage. + while (::PeekMessage(&message, nullptr, 0, 0, PM_REMOVE)) { + processed_events = true; + if (message.message == WM_QUIT) { + keep_running = false; + break; + } + ::TranslateMessage(&message); + ::DispatchMessage(&message); + // Allow Flutter to process messages each time a Windows message is + // processed, to prevent starvation. + next_flutter_event_time = + std::min(next_flutter_event_time, ProcessFlutterMessages()); + } + // If the PeekMessage loop didn't run, process Flutter messages. + if (!processed_events) { + next_flutter_event_time = + std::min(next_flutter_event_time, ProcessFlutterMessages()); + } + } +} + +void RunLoop::RegisterFlutterInstance( + flutter::FlutterEngine* flutter_instance) { + flutter_instances_.insert(flutter_instance); +} + +void RunLoop::UnregisterFlutterInstance( + flutter::FlutterEngine* flutter_instance) { + flutter_instances_.erase(flutter_instance); +} + +RunLoop::TimePoint RunLoop::ProcessFlutterMessages() { + TimePoint next_event_time = TimePoint::max(); + for (auto instance : flutter_instances_) { + std::chrono::nanoseconds wait_duration = instance->ProcessMessages(); + if (wait_duration != std::chrono::nanoseconds::max()) { + next_event_time = + std::min(next_event_time, TimePoint::clock::now() + wait_duration); + } + } + return next_event_time; +} diff --git a/packages/path_provider/path_provider/example/windows/runner/run_loop.h b/packages/path_provider/path_provider/example/windows/runner/run_loop.h new file mode 100644 index 000000000000..819ed3ed4995 --- /dev/null +++ b/packages/path_provider/path_provider/example/windows/runner/run_loop.h @@ -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. + +#ifndef RUNNER_RUN_LOOP_H_ +#define RUNNER_RUN_LOOP_H_ + +#include + +#include +#include + +// A runloop that will service events for Flutter instances as well +// as native messages. +class RunLoop { + public: + RunLoop(); + ~RunLoop(); + + // Prevent copying + RunLoop(RunLoop const&) = delete; + RunLoop& operator=(RunLoop const&) = delete; + + // Runs the run loop until the application quits. + void Run(); + + // Registers the given Flutter instance for event servicing. + void RegisterFlutterInstance(flutter::FlutterEngine* flutter_instance); + + // Unregisters the given Flutter instance from event servicing. + void UnregisterFlutterInstance(flutter::FlutterEngine* flutter_instance); + + private: + using TimePoint = std::chrono::steady_clock::time_point; + + // Processes all currently pending messages for registered Flutter instances. + TimePoint ProcessFlutterMessages(); + + std::set flutter_instances_; +}; + +#endif // RUNNER_RUN_LOOP_H_ diff --git a/packages/path_provider/path_provider/example/windows/runner/runner.exe.manifest b/packages/path_provider/path_provider/example/windows/runner/runner.exe.manifest new file mode 100644 index 000000000000..c977c4a42589 --- /dev/null +++ b/packages/path_provider/path_provider/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/packages/path_provider/path_provider/example/windows/runner/utils.cpp b/packages/path_provider/path_provider/example/windows/runner/utils.cpp new file mode 100644 index 000000000000..537728149601 --- /dev/null +++ b/packages/path_provider/path_provider/example/windows/runner/utils.cpp @@ -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. + +#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(); + } +} diff --git a/packages/path_provider/path_provider/example/windows/runner/utils.h b/packages/path_provider/path_provider/example/windows/runner/utils.h new file mode 100644 index 000000000000..16b3f0794597 --- /dev/null +++ b/packages/path_provider/path_provider/example/windows/runner/utils.h @@ -0,0 +1,12 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/path_provider/path_provider/example/windows/runner/win32_window.cpp b/packages/path_provider/path_provider/example/windows/runner/win32_window.cpp new file mode 100644 index 000000000000..a609a2002bb3 --- /dev/null +++ b/packages/path_provider/path_provider/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/path_provider/path_provider/example/windows/runner/win32_window.h b/packages/path_provider/path_provider/example/windows/runner/win32_window.h new file mode 100644 index 000000000000..d2a730052223 --- /dev/null +++ b/packages/path_provider/path_provider/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/path_provider/path_provider/integration_test/path_provider_test.dart b/packages/path_provider/path_provider/integration_test/path_provider_test.dart deleted file mode 100644 index 18570aeca57e..000000000000 --- a/packages/path_provider/path_provider/integration_test/path_provider_test.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:integration_test/integration_test.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('Can get temporary directory', (WidgetTester tester) async { - final String tempPath = (await getTemporaryDirectory()).path; - expect(tempPath, isNotEmpty); - }); -} diff --git a/packages/path_provider/path_provider/ios/Classes/FLTPathProviderPlugin.h b/packages/path_provider/path_provider/ios/Classes/FLTPathProviderPlugin.h index 394f8c070632..8f9fd4597f9d 100644 --- a/packages/path_provider/path_provider/ios/Classes/FLTPathProviderPlugin.h +++ b/packages/path_provider/path_provider/ios/Classes/FLTPathProviderPlugin.h @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter 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/Classes/FLTPathProviderPlugin.m b/packages/path_provider/path_provider/ios/Classes/FLTPathProviderPlugin.m index 705b371b4586..4d3dfeb6e6e6 100644 --- a/packages/path_provider/path_provider/ios/Classes/FLTPathProviderPlugin.m +++ b/packages/path_provider/path_provider/ios/Classes/FLTPathProviderPlugin.m @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter 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/lib/path_provider.dart b/packages/path_provider/path_provider/lib/path_provider.dart index 0fbab57700be..e690b7f92960 100644 --- a/packages/path_provider/path_provider/lib/path_provider.dart +++ b/packages/path_provider/path_provider/lib/path_provider.dart @@ -1,15 +1,15 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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' show Directory, Platform; import 'package:flutter/foundation.dart' show kIsWeb, visibleForTesting; import 'package:path_provider_linux/path_provider_linux.dart'; -import 'package:path_provider_windows/path_provider_windows.dart'; 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; @@ -20,10 +20,30 @@ set disablePathProviderPlatformOverride(bool override) {} bool _manualDartRegistrationNeeded = true; +/// An exception thrown when a directory that should always be available on +/// the current platform cannot be obtained. +class MissingPlatformDirectoryException implements Exception { + /// Creates a new exception + MissingPlatformDirectoryException(this.message, {this.details}); + + /// The explanation of the exception. + final String message; + + /// Added details, if any. + /// + /// E.g., an error object from the platform implementation. + final Object? details; + + @override + String toString() { + final String detailsAddition = details == null ? '' : ': $details'; + return 'MissingPlatformDirectoryException($message)$detailsAddition'; + } +} + PathProviderPlatform get _platform { - // This is to manually endorse Dart implementations until automatic - // registration of Dart plugins is implemented. For details see - // https://github.com/flutter/flutter/issues/52267. + // 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. @@ -51,10 +71,14 @@ PathProviderPlatform get _platform { /// On iOS, this uses the `NSCachesDirectory` API. /// /// On Android, this uses the `getCacheDir` API on the context. +/// +/// Throws a `MissingPlatformDirectoryException` if the system is unable to +/// provide the directory. Future getTemporaryDirectory() async { - final String path = await _platform.getTemporaryPath(); + final String? path = await _platform.getTemporaryPath(); if (path == null) { - return null; + throw MissingPlatformDirectoryException( + 'Unable to get temporary directory'); } return Directory(path); } @@ -69,10 +93,14 @@ Future getTemporaryDirectory() async { /// If this directory does not exist, it is created automatically. /// /// On Android, this function uses the `getFilesDir` API on the context. +/// +/// Throws a `MissingPlatformDirectoryException` if the system is unable to +/// provide the directory. Future getApplicationSupportDirectory() async { - final String path = await _platform.getApplicationSupportPath(); + final String? path = await _platform.getApplicationSupportPath(); if (path == null) { - return null; + throw MissingPlatformDirectoryException( + 'Unable to get application support directory'); } return Directory(path); @@ -83,10 +111,13 @@ Future getApplicationSupportDirectory() async { /// /// On Android, this function throws an [UnsupportedError] as no equivalent /// path exists. +/// +/// Throws a `MissingPlatformDirectoryException` if the system is unable to +/// provide the directory on a supported platform. Future getLibraryDirectory() async { - final String path = await _platform.getLibraryPath(); + final String? path = await _platform.getLibraryPath(); if (path == null) { - return null; + throw MissingPlatformDirectoryException('Unable to get library directory'); } return Directory(path); } @@ -100,10 +131,14 @@ Future getLibraryDirectory() async { /// On Android, this uses the `getDataDirectory` API on the context. Consider /// using [getExternalStorageDirectory] instead if data is intended to be visible /// to the user. +/// +/// Throws a `MissingPlatformDirectoryException` if the system is unable to +/// provide the directory. Future getApplicationDocumentsDirectory() async { - final String path = await _platform.getApplicationDocumentsPath(); + final String? path = await _platform.getApplicationDocumentsPath(); if (path == null) { - return null; + throw MissingPlatformDirectoryException( + 'Unable to get application documents directory'); } return Directory(path); } @@ -116,8 +151,8 @@ Future getApplicationDocumentsDirectory() async { /// to access outside the app's sandbox. /// /// On Android this uses the `getExternalFilesDir(null)`. -Future getExternalStorageDirectory() async { - final String path = await _platform.getExternalStoragePath(); +Future getExternalStorageDirectory() async { + final String? path = await _platform.getExternalStoragePath(); if (path == null) { return null; } @@ -137,8 +172,11 @@ Future getExternalStorageDirectory() async { /// /// On Android this returns Context.getExternalCacheDirs() or /// Context.getExternalCacheDir() on API levels below 19. -Future> getExternalCacheDirectories() async { - final List paths = await _platform.getExternalCachePaths(); +Future?> getExternalCacheDirectories() async { + final List? paths = await _platform.getExternalCachePaths(); + if (paths == null) { + return null; + } return paths.map((String path) => Directory(path)).toList(); } @@ -155,13 +193,16 @@ Future> getExternalCacheDirectories() async { /// /// On Android this returns Context.getExternalFilesDirs(String type) or /// Context.getExternalFilesDir(String type) on API levels below 19. -Future> getExternalStorageDirectories({ +Future?> getExternalStorageDirectories({ /// Optional parameter. See [StorageDirectory] for more informations on /// how this type translates to Android storage directories. - StorageDirectory type, + StorageDirectory? type, }) async { - final List paths = + final List? paths = await _platform.getExternalStoragePaths(type: type); + if (paths == null) { + return null; + } return paths.map((String path) => Directory(path)).toList(); } @@ -171,8 +212,8 @@ Future> getExternalStorageDirectories({ /// /// On Android and on iOS, this function throws an [UnsupportedError] as no equivalent /// path exists. -Future getDownloadsDirectory() async { - final String path = await _platform.getDownloadsPath(); +Future getDownloadsDirectory() async { + final String? path = await _platform.getDownloadsPath(); if (path == null) { return null; } diff --git a/packages/path_provider/path_provider/macos/path_provider.podspec b/packages/path_provider/path_provider/macos/path_provider.podspec deleted file mode 100644 index 9f3f01f2f858..000000000000 --- a/packages/path_provider/path_provider/macos/path_provider.podspec +++ /dev/null @@ -1,22 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'path_provider' - s.version = '0.0.1' - s.summary = 'No-op implementation of the macos path_provider to avoid build issues on macos' - s.description = <<-DESC - No-op implementation of the path_provider plugin to avoid build issues on macos. - https://github.com/flutter/flutter/issues/46618 - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/path_provider' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - - s.platform = :osx - s.osx.deployment_target = '10.11' -end - diff --git a/packages/path_provider/path_provider/pubspec.yaml b/packages/path_provider/path_provider/pubspec.yaml index 0eadc40637b0..a61c15f1e417 100644 --- a/packages/path_provider/path_provider/pubspec.yaml +++ b/packages/path_provider/path_provider/pubspec.yaml @@ -1,7 +1,12 @@ name: path_provider description: Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories. -homepage: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider -version: 1.6.19 +repository: https://github.com/flutter/plugins/tree/master/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.2 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: @@ -21,24 +26,18 @@ flutter: dependencies: flutter: sdk: flutter - path_provider_platform_interface: ^1.0.1 - path_provider_macos: ^0.0.4 - path_provider_linux: ^0.0.1 - path_provider_windows: ^0.0.4 + path_provider_linux: ^2.0.0 + path_provider_macos: ^2.0.0 + path_provider_platform_interface: ^2.0.0 + path_provider_windows: ^2.0.0 dev_dependencies: - integration_test: - path: ../../integration_test + flutter_driver: + sdk: flutter flutter_test: sdk: flutter - flutter_driver: + integration_test: sdk: flutter - test: any - uuid: "^1.0.0" - pedantic: ^1.8.0 - mockito: ^4.1.1 - plugin_platform_interface: ^1.0.0 - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + 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 eb17178b9975..218861606209 100644 --- a/packages/path_provider/path_provider/test/path_provider_test.dart +++ b/packages/path_provider/path_provider/test/path_provider_test.dart @@ -1,15 +1,14 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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' show Directory; -import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:test/fake.dart'; const String kTemporaryPath = 'temporaryPath'; const String kApplicationSupportPath = 'applicationSupportPath'; @@ -20,91 +19,190 @@ const String kExternalCachePath = 'externalCachePath'; const String kExternalStoragePath = 'externalStoragePath'; void main() { - group('PathProvider', () { - TestWidgetsFlutterBinding.ensureInitialized(); - + TestWidgetsFlutterBinding.ensureInitialized(); + group('PathProvider full implementation', () { setUp(() async { - PathProviderPlatform.instance = MockPathProviderPlatform(); + PathProviderPlatform.instance = FakePathProviderPlatform(); }); test('getTemporaryDirectory', () async { - Directory result = await getTemporaryDirectory(); + final Directory result = await getTemporaryDirectory(); expect(result.path, kTemporaryPath); }); test('getApplicationSupportDirectory', () async { - Directory result = await getApplicationSupportDirectory(); + final Directory result = await getApplicationSupportDirectory(); expect(result.path, kApplicationSupportPath); }); test('getLibraryDirectory', () async { - Directory result = await getLibraryDirectory(); + final Directory result = await getLibraryDirectory(); expect(result.path, kLibraryPath); }); test('getApplicationDocumentsDirectory', () async { - Directory result = await getApplicationDocumentsDirectory(); + final Directory result = await getApplicationDocumentsDirectory(); expect(result.path, kApplicationDocumentsPath); }); test('getExternalStorageDirectory', () async { - Directory result = await getExternalStorageDirectory(); - expect(result.path, kExternalStoragePath); + final Directory? result = await getExternalStorageDirectory(); + expect(result?.path, kExternalStoragePath); }); test('getExternalCacheDirectories', () async { - List result = await getExternalCacheDirectories(); - expect(result.length, 1); - expect(result.first.path, kExternalCachePath); + final List? result = await getExternalCacheDirectories(); + expect(result?.length, 1); + expect(result?.first.path, kExternalCachePath); }); test('getExternalStorageDirectories', () async { - List result = await getExternalStorageDirectories(); - expect(result.length, 1); - expect(result.first.path, kExternalStoragePath); + final List? result = await getExternalStorageDirectories(); + expect(result?.length, 1); + expect(result?.first.path, kExternalStoragePath); }); test('getDownloadsDirectory', () async { - Directory result = await getDownloadsDirectory(); - expect(result.path, kDownloadsPath); + final Directory? result = await getDownloadsDirectory(); + expect(result?.path, kDownloadsPath); + }); + }); + + group('PathProvider null implementation', () { + setUp(() async { + PathProviderPlatform.instance = AllNullFakePathProviderPlatform(); + }); + + test('getTemporaryDirectory throws on null', () async { + expect(getTemporaryDirectory(), + throwsA(isA())); + }); + + test('getApplicationSupportDirectory throws on null', () async { + expect(getApplicationSupportDirectory(), + throwsA(isA())); + }); + + test('getLibraryDirectory throws on null', () async { + expect(getLibraryDirectory(), + throwsA(isA())); + }); + + test('getApplicationDocumentsDirectory throws on null', () async { + expect(getApplicationDocumentsDirectory(), + throwsA(isA())); + }); + + test('getExternalStorageDirectory passes null through', () async { + final Directory? result = await getExternalStorageDirectory(); + expect(result, isNull); + }); + + test('getExternalCacheDirectories passes null through', () async { + final List? result = await getExternalCacheDirectories(); + expect(result, isNull); + }); + + test('getExternalStorageDirectories passes null through', () async { + final List? result = await getExternalStorageDirectories(); + expect(result, isNull); + }); + + test('getDownloadsDirectory passses null through', () async { + final Directory? result = await getDownloadsDirectory(); + expect(result, isNull); }); }); } -class MockPathProviderPlatform extends Mock +class FakePathProviderPlatform extends Fake with MockPlatformInterfaceMixin implements PathProviderPlatform { - Future getTemporaryPath() async { + @override + Future getTemporaryPath() async { return kTemporaryPath; } - Future getApplicationSupportPath() async { + @override + Future getApplicationSupportPath() async { return kApplicationSupportPath; } - Future getLibraryPath() async { + @override + Future getLibraryPath() async { return kLibraryPath; } - Future getApplicationDocumentsPath() async { + @override + Future getApplicationDocumentsPath() async { return kApplicationDocumentsPath; } - Future getExternalStoragePath() async { + @override + Future getExternalStoragePath() async { return kExternalStoragePath; } - Future> getExternalCachePaths() async { + @override + Future?> getExternalCachePaths() async { return [kExternalCachePath]; } - Future> getExternalStoragePaths({ - StorageDirectory type, + @override + Future?> getExternalStoragePaths({ + StorageDirectory? type, }) async { return [kExternalStoragePath]; } - Future getDownloadsPath() async { + @override + Future getDownloadsPath() async { return kDownloadsPath; } } + +class AllNullFakePathProviderPlatform extends Fake + with MockPlatformInterfaceMixin + implements PathProviderPlatform { + @override + Future getTemporaryPath() async { + return null; + } + + @override + Future getApplicationSupportPath() async { + return null; + } + + @override + Future getLibraryPath() async { + return null; + } + + @override + Future getApplicationDocumentsPath() async { + return null; + } + + @override + Future getExternalStoragePath() async { + return null; + } + + @override + Future?> getExternalCachePaths() async { + return null; + } + + @override + Future?> getExternalStoragePaths({ + StorageDirectory? type, + }) async { + return null; + } + + @override + Future getDownloadsPath() async { + return null; + } +} diff --git a/packages/path_provider/path_provider_linux/AUTHORS b/packages/path_provider/path_provider_linux/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/path_provider/path_provider_linux/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/path_provider/path_provider_linux/CHANGELOG.md b/packages/path_provider/path_provider_linux/CHANGELOG.md index baec603b426f..9383181d6a76 100644 --- a/packages/path_provider/path_provider_linux/CHANGELOG.md +++ b/packages/path_provider/path_provider_linux/CHANGELOG.md @@ -1,3 +1,24 @@ +## 2.0.1 + +* Add `implements` to pubspec.yaml. +* Add `registerWith` method to the main Dart class. + +## 2.0.0 + +* Migrate to null safety. + +## 0.1.1+3 + +* Update Flutter SDK constraint. + +## 0.1.1+2 + +* Log errors in the example when calls to the `path_provider` fail. + +## 0.1.1+1 + +* Check in linux/ directory for example/ + ## 0.1.1 - NOT PUBLISHED * Reverts changes on 0.1.0, which broke the tree. @@ -15,4 +36,3 @@ ## 0.0.1 * The initial implementation of path_provider for Linux * Implements getApplicationSupportPath, getApplicationDocumentsPath, getDownloadsPath, and getTemporaryPath - diff --git a/packages/path_provider/path_provider_linux/LICENSE b/packages/path_provider/path_provider_linux/LICENSE index d7412e0a1e0c..c6823b81eb84 100644 --- a/packages/path_provider/path_provider_linux/LICENSE +++ b/packages/path_provider/path_provider_linux/LICENSE @@ -1,4 +1,4 @@ -Copyright 2020 The Chromium Authors. All rights reserved. +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: diff --git a/packages/path_provider/path_provider_linux/README.md b/packages/path_provider/path_provider_linux/README.md index 373925d2d96d..ef9e0e855c86 100644 --- a/packages/path_provider/path_provider_linux/README.md +++ b/packages/path_provider/path_provider_linux/README.md @@ -2,13 +2,6 @@ The linux implementation of [`path_provider`]. -**Please set your constraint to `path_provider: '>=0.0.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The `path_provider` plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.0.y+z`. -Please use `path_provider: '>=0.0.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ## Usage This package is already included as part of the `path_provider` package dependency, and will diff --git a/packages/path_provider/path_provider_linux/example/integration_test/path_provider_test.dart b/packages/path_provider/path_provider_linux/example/integration_test/path_provider_test.dart index 18ac49debbd4..3bd644f69763 100644 --- a/packages/path_provider/path_provider_linux/example/integration_test/path_provider_test.dart +++ b/packages/path_provider/path_provider_linux/example/integration_test/path_provider_test.dart @@ -1,17 +1,18 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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_provider/path_provider.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:path_provider_linux/path_provider_linux.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('getTemporaryDirectory', (WidgetTester tester) async { - final Directory result = await getTemporaryDirectory(); + final PathProviderLinux provider = PathProviderLinux(); + final String? result = await provider.getTemporaryPath(); _verifySampleFile(result, 'temporaryDirectory'); }); @@ -19,25 +20,33 @@ void main() { if (!Platform.isLinux) { return; } - final Directory result = await getDownloadsDirectory(); + final PathProviderLinux provider = PathProviderLinux(); + final String? result = await provider.getDownloadsPath(); _verifySampleFile(result, 'downloadDirectory'); }); testWidgets('getApplicationDocumentsDirectory', (WidgetTester tester) async { - final Directory result = await getApplicationDocumentsDirectory(); + final PathProviderLinux provider = PathProviderLinux(); + final String? result = await provider.getApplicationDocumentsPath(); _verifySampleFile(result, 'applicationDocuments'); }); testWidgets('getApplicationSupportDirectory', (WidgetTester tester) async { - final Directory result = await getApplicationSupportDirectory(); + final PathProviderLinux provider = PathProviderLinux(); + final String? result = await provider.getApplicationSupportPath(); _verifySampleFile(result, 'applicationSupport'); }); } -/// Verify a file called [name] in [directory] by recreating it with test +/// Verify a file called [name] in [directoryPath] by recreating it with test /// contents when necessary. -void _verifySampleFile(Directory directory, String name) { - final File file = File('${directory.path}/$name'); +void _verifySampleFile(String? directoryPath, String name) { + 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(); diff --git a/packages/path_provider/path_provider_linux/example/lib/main.dart b/packages/path_provider/path_provider_linux/example/lib/main.dart index edba63482ad5..d365e6bdeab4 100644 --- a/packages/path_provider/path_provider_linux/example/lib/main.dart +++ b/packages/path_provider/path_provider_linux/example/lib/main.dart @@ -1,10 +1,12 @@ -import 'package:flutter/material.dart'; -import 'dart:async'; +// Copyright 2013 The Flutter Authors. All 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/services.dart'; -import 'package:path_provider/path_provider.dart'; +import 'package:path_provider_linux/path_provider_linux.dart'; -void main() async { +void main() { runApp(MyApp()); } @@ -15,10 +17,11 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - String _tempDirectory = 'Unknown'; - String _downloadsDirectory = 'Unknown'; - String _appSupportDirectory = 'Unknown'; - String _documentsDirectory = 'Unknown'; + String? _tempDirectory = 'Unknown'; + String? _downloadsDirectory = 'Unknown'; + String? _appSupportDirectory = 'Unknown'; + String? _documentsDirectory = 'Unknown'; + final PathProviderLinux _provider = PathProviderLinux(); @override void initState() { @@ -28,37 +31,43 @@ class _MyAppState extends State { // Platform messages are asynchronous, so we initialize in an async method. Future initDirectories() async { - String tempDirectory; - String downloadsDirectory; - String appSupportDirectory; - String documentsDirectory; + String? tempDirectory; + String? downloadsDirectory; + String? appSupportDirectory; + String? documentsDirectory; // Platform messages may fail, so we use a try/catch PlatformException. try { - tempDirectory = (await getTemporaryDirectory()).path; - } on PlatformException { + tempDirectory = await _provider.getTemporaryPath(); + } on PlatformException catch (e, stackTrace) { tempDirectory = 'Failed to get temp directory.'; + print('$tempDirectory $e $stackTrace'); } try { - downloadsDirectory = (await getDownloadsDirectory()).path; - } on PlatformException { + downloadsDirectory = await _provider.getDownloadsPath(); + } on PlatformException catch (e, stackTrace) { downloadsDirectory = 'Failed to get downloads directory.'; + print('$downloadsDirectory $e $stackTrace'); } try { - documentsDirectory = (await getApplicationDocumentsDirectory()).path; - } on PlatformException { + documentsDirectory = await _provider.getApplicationDocumentsPath(); + } on PlatformException catch (e, stackTrace) { documentsDirectory = 'Failed to get documents directory.'; + print('$documentsDirectory $e $stackTrace'); } try { - appSupportDirectory = (await getApplicationSupportDirectory()).path; - } on PlatformException { + appSupportDirectory = await _provider.getApplicationSupportPath(); + } on PlatformException catch (e, stackTrace) { appSupportDirectory = 'Failed to get documents directory.'; + print('$appSupportDirectory $e $stackTrace'); } // If the widget was removed from the tree while the asynchronous platform // message was in flight, we want to discard the reply rather than calling // setState to update our non-existent appearance. - if (!mounted) return; + if (!mounted) { + return; + } setState(() { _tempDirectory = tempDirectory; @@ -77,7 +86,7 @@ class _MyAppState extends State { ), body: Center( child: Column( - children: [ + children: [ Text('Temp Directory: $_tempDirectory\n'), Text('Documents Directory: $_documentsDirectory\n'), Text('Downloads Directory: $_downloadsDirectory\n'), diff --git a/packages/path_provider/path_provider_linux/example/linux/.gitignore b/packages/path_provider/path_provider_linux/example/linux/.gitignore new file mode 100644 index 000000000000..d3896c98444f --- /dev/null +++ b/packages/path_provider/path_provider_linux/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/packages/path_provider/path_provider_linux/example/linux/CMakeLists.txt b/packages/path_provider/path_provider_linux/example/linux/CMakeLists.txt new file mode 100644 index 000000000000..4c422c777e94 --- /dev/null +++ b/packages/path_provider/path_provider_linux/example/linux/CMakeLists.txt @@ -0,0 +1,106 @@ +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +set(BINARY_NAME "example") +set(APPLICATION_ID "dev.flutter.plugins.path_provider_linux_example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Application build +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) +apply_standard_settings(${BINARY_NAME}) +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) +add_dependencies(${BINARY_NAME} flutter_assemble) +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/packages/path_provider/path_provider_linux/example/linux/flutter/CMakeLists.txt b/packages/path_provider/path_provider_linux/example/linux/flutter/CMakeLists.txt new file mode 100644 index 000000000000..4f48a7ced5f4 --- /dev/null +++ b/packages/path_provider/path_provider_linux/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) +pkg_check_modules(BLKID REQUIRED IMPORTED_TARGET blkid) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO + PkgConfig::BLKID +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + linux-x64 ${CMAKE_BUILD_TYPE} +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) 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 new file mode 100644 index 000000000000..e71a16d23d05 --- /dev/null +++ b/packages/path_provider/path_provider_linux/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// 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 new file mode 100644 index 000000000000..e0f0a47bc08f --- /dev/null +++ b/packages/path_provider/path_provider_linux/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// 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/linux/flutter/generated_plugins.cmake b/packages/path_provider/path_provider_linux/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..51436ae8c982 --- /dev/null +++ b/packages/path_provider/path_provider_linux/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,15 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) diff --git a/packages/path_provider/path_provider_linux/example/linux/main.cc b/packages/path_provider/path_provider_linux/example/linux/main.cc new file mode 100644 index 000000000000..88a5fd45ce1b --- /dev/null +++ b/packages/path_provider/path_provider_linux/example/linux/main.cc @@ -0,0 +1,15 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "my_application.h" + +int main(int argc, char** argv) { + // Only X11 is currently supported. + // Wayland support is being developed: + // https://github.com/flutter/flutter/issues/57932. + gdk_set_allowed_backends("x11"); + + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/packages/path_provider/path_provider_linux/example/linux/my_application.cc b/packages/path_provider/path_provider_linux/example/linux/my_application.cc new file mode 100644 index 000000000000..9cb411ba475b --- /dev/null +++ b/packages/path_provider/path_provider_linux/example/linux/my_application.cc @@ -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. + +#include "my_application.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new( + my_application_get_type(), "application-id", APPLICATION_ID, nullptr)); +} diff --git a/packages/path_provider/path_provider_linux/example/linux/my_application.h b/packages/path_provider/path_provider_linux/example/linux/my_application.h new file mode 100644 index 000000000000..6e9f0c3ff665 --- /dev/null +++ b/packages/path_provider/path_provider_linux/example/linux/my_application.h @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/packages/path_provider/path_provider_linux/example/pubspec.yaml b/packages/path_provider/path_provider_linux/example/pubspec.yaml index 85dbb24bbb29..252f3510a789 100644 --- a/packages/path_provider/path_provider_linux/example/pubspec.yaml +++ b/packages/path_provider/path_provider_linux/example/pubspec.yaml @@ -3,63 +3,28 @@ description: Demonstrates how to use the path_provider_linux plugin. publish_to: "none" environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: sdk: flutter - path_provider: ^1.6.10 - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^0.1.3 - -dependency_overrides: path_provider_linux: + # When depending on this package from a real application you should use: + # path_provider_linux: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: - flutter_test: - sdk: flutter flutter_driver: sdk: flutter + flutter_test: + sdk: flutter integration_test: - path: ../../../integration_test - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec + sdk: flutter -# The following section is specific to Flutter. flutter: - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware. - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/path_provider/path_provider_linux/example/test/widget_test.dart b/packages/path_provider/path_provider_linux/example/test/widget_test.dart deleted file mode 100644 index 8ebda3b77176..000000000000 --- a/packages/path_provider/path_provider_linux/example/test/widget_test.dart +++ /dev/null @@ -1,95 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility that Flutter provides. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:pathproviderexample/main.dart'; - -void main() { - group('Test linux path provider example', () { - setUpAll(() async { - await WidgetsFlutterBinding.ensureInitialized(); - }); - - testWidgets('Finds tmp directory', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.runAsync(() async { - await tester.pumpWidget(MyApp()); - await Future.delayed(Duration(milliseconds: 20)); - await tester.pump(); - - // Verify that temporary directory is retrieved. - expect( - find.byWidgetPredicate( - (Widget widget) => - widget is Text && - widget.data.startsWith('Temp Directory: /tmp'), - ), - findsOneWidget, - ); - }); - }); - testWidgets('Finds documents directory', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.runAsync(() async { - await tester.pumpWidget(MyApp()); - await Future.delayed(Duration(milliseconds: 20)); - await tester.pump(); - - // Verify that documents directory is retrieved. - expect( - find.byWidgetPredicate( - (Widget widget) => - widget is Text && - widget.data.startsWith('Documents Directory: /'), - ), - findsOneWidget, - ); - }); - }); - testWidgets('Finds downloads directory', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.runAsync(() async { - await tester.pumpWidget(MyApp()); - await Future.delayed(Duration(milliseconds: 20)); - await tester.pump(); - - // Verify that downloads directory is retrieved. - expect( - find.byWidgetPredicate( - (Widget widget) => - widget is Text && - widget.data.startsWith('Downloads Directory: /'), - ), - findsOneWidget, - ); - }); - }); - testWidgets('Finds application support directory', - (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.runAsync(() async { - await tester.pumpWidget(MyApp()); - await Future.delayed(Duration(milliseconds: 20)); - await tester.pump(); - - // Verify that Application Support Directory is retrieved. - expect( - find.byWidgetPredicate( - (Widget widget) => - widget is Text && - widget.data.startsWith('Application Support Directory: /'), - ), - findsOneWidget, - ); - }); - }); - }); -} diff --git a/packages/path_provider/path_provider_linux/example/test_driver/integration_test.dart b/packages/path_provider/path_provider_linux/example/test_driver/integration_test.dart index 7a2c21338786..4f10f2a522f3 100644 --- a/packages/path_provider/path_provider_linux/example/test_driver/integration_test.dart +++ b/packages/path_provider/path_provider_linux/example/test_driver/integration_test.dart @@ -1,17 +1,7 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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'; +import 'package:integration_test/integration_test_driver.dart'; -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String data = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - await driver.close(); - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} +Future main() => integrationDriver(); diff --git a/packages/path_provider/path_provider_linux/ios/.gitignore b/packages/path_provider/path_provider_linux/ios/.gitignore deleted file mode 100644 index aa479fd3ce8a..000000000000 --- a/packages/path_provider/path_provider_linux/ios/.gitignore +++ /dev/null @@ -1,37 +0,0 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -.generated/ - -*.pbxuser -*.mode1v3 -*.mode2v3 -*.perspectivev3 - -!default.pbxuser -!default.mode1v3 -!default.mode2v3 -!default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/Generated.xcconfig -/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/packages/path_provider/path_provider_linux/ios/path_provider_linux.podspec b/packages/path_provider/path_provider_linux/ios/path_provider_linux.podspec deleted file mode 100644 index 3649a30e67ef..000000000000 --- a/packages/path_provider/path_provider_linux/ios/path_provider_linux.podspec +++ /dev/null @@ -1,19 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'path_provider_linux' - s.version = '0.0.1' - s.summary = 'No-op implementation of path_provider linux plugin to avoid build issues on iOS' - s.description = <<-DESC - No-op implementation of path_provider linux plugin - See https://github.com/flutter/flutter/issues/39659 - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_linux' } - s.documentation_url = 'https://pub.dev/packages/path_provider' - s.dependency 'Flutter' - s.platform = :ios, '8.0' -end \ No newline at end of file diff --git a/packages/path_provider/path_provider_linux/lib/path_provider_linux.dart b/packages/path_provider/path_provider_linux/lib/path_provider_linux.dart index 09d2447c0a6c..38800bee1f2e 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 @@ -1,46 +1,47 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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:async'; -import 'package:xdg_directories/xdg_directories.dart' as xdg; 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 { /// Registers this class as the default instance of [PathProviderPlatform] - static void register() { + static void registerWith() { PathProviderPlatform.instance = PathProviderLinux(); } @override - Future getTemporaryPath() { - return Future.value("/tmp"); + Future getTemporaryPath() { + return Future.value('/tmp'); } @override - Future getApplicationSupportPath() async { - final processName = path.basenameWithoutExtension( + Future getApplicationSupportPath() async { + final String processName = path.basenameWithoutExtension( await File('/proc/self/exe').resolveSymbolicLinks()); - final directory = Directory(path.join(xdg.dataHome.path, processName)); + 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 (!await directory.exists()) { + if (!directory.existsSync()) { await directory.create(recursive: true); } return directory.path; } @override - Future getApplicationDocumentsPath() { - return Future.value(xdg.getUserDirectory('DOCUMENTS').path); + Future getApplicationDocumentsPath() { + return Future.value(xdg.getUserDirectory('DOCUMENTS')?.path); } @override - Future getDownloadsPath() { - return Future.value(xdg.getUserDirectory('DOWNLOAD').path); + Future getDownloadsPath() { + return Future.value(xdg.getUserDirectory('DOWNLOAD')?.path); } } diff --git a/packages/path_provider/path_provider_linux/pubspec.yaml b/packages/path_provider/path_provider_linux/pubspec.yaml index b0188fd2a4c7..7e015dca06db 100644 --- a/packages/path_provider/path_provider_linux/pubspec.yaml +++ b/packages/path_provider/path_provider_linux/pubspec.yaml @@ -1,27 +1,29 @@ name: path_provider_linux -description: linux implementation of the path_provider plugin -version: 0.1.1 -homepage: https://github.com/flutter/plugins/tree/master/packages/path_provider/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 +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 +version: 2.0.1 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: + implements: path_provider platforms: linux: dartPluginClass: PathProviderLinux pluginClass: none -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" - dependencies: - path: ^1.6.4 - xdg_directories: ^0.1.0 - path_provider_platform_interface: ^1.0.1 flutter: sdk: flutter + path: ^1.8.0 + path_provider_platform_interface: ^2.0.0 + xdg_directories: ^0.2.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 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 be831b92211f..e058d0d56039 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 @@ -1,4 +1,4 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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'; @@ -7,14 +7,29 @@ import 'package:path_provider_platform_interface/path_provider_platform_interfac void main() { TestWidgetsFlutterBinding.ensureInitialized(); - PathProviderLinux.register(); + PathProviderLinux.registerWith(); - setUp(() {}); - - tearDown(() {}); + test('registered instance', () { + expect(PathProviderPlatform.instance, isA()); + }); test('getTemporaryPath', () async { - final plugin = PathProviderPlatform.instance; + final PathProviderPlatform plugin = PathProviderPlatform.instance; expect(await plugin.getTemporaryPath(), '/tmp'); }); + + test('getApplicationSupportPath', () async { + final PathProviderPlatform plugin = PathProviderPlatform.instance; + expect(await plugin.getApplicationSupportPath(), startsWith('/')); + }); + + test('getApplicationDocumentsPath', () async { + final PathProviderPlatform plugin = PathProviderPlatform.instance; + expect(await plugin.getApplicationDocumentsPath(), startsWith('/')); + }); + + test('getDownloadsPath', () async { + final PathProviderPlatform plugin = PathProviderPlatform.instance; + expect(await plugin.getDownloadsPath(), startsWith('/')); + }); } diff --git a/packages/path_provider/path_provider_macos/AUTHORS b/packages/path_provider/path_provider_macos/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/path_provider/path_provider_macos/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/path_provider/path_provider_macos/CHANGELOG.md b/packages/path_provider/path_provider_macos/CHANGELOG.md index b2ca95fbd10c..d5f9ce860b6f 100644 --- a/packages/path_provider/path_provider_macos/CHANGELOG.md +++ b/packages/path_provider/path_provider_macos/CHANGELOG.md @@ -1,3 +1,33 @@ +## NEXT + +* Add Swift language version to podspec. +* Add native unit tests. + +## 2.0.1 + +* Add `implements` to pubspec.yaml. + +## 2.0.0 + +* Update Dart SDK constraint for null safety compatibility. + +## 0.0.4+9 + +* Remove placeholder Dart file. + +## 0.0.4+8 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 0.0.4+7 + +* Update Flutter SDK constraint. + +## 0.0.4+6 + +* Remove unused `test` dependency. +* Update Dart SDK constraint in example. + ## 0.0.4+5 * Update license header. diff --git a/packages/path_provider/path_provider_macos/LICENSE b/packages/path_provider/path_provider_macos/LICENSE index a6d6c0749818..c6823b81eb84 100644 --- a/packages/path_provider/path_provider_macos/LICENSE +++ b/packages/path_provider/path_provider_macos/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017 The Chromium Authors. All rights reserved. +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: diff --git a/packages/path_provider/path_provider_macos/README.md b/packages/path_provider/path_provider_macos/README.md index b97d9e81b7db..23727fe7f370 100644 --- a/packages/path_provider/path_provider_macos/README.md +++ b/packages/path_provider/path_provider_macos/README.md @@ -2,13 +2,6 @@ The macos implementation of [`path_provider`]. -**Please set your constraint to `path_provider_macos: '>=0.0.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.0.y+z`. -Please use `path_provider_macos: '>=0.0.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ## Usage ### Import the package diff --git a/packages/path_provider/path_provider_macos/example/README.md b/packages/path_provider/path_provider_macos/example/README.md index c81750248f02..4f413873b346 100644 --- a/packages/path_provider/path_provider_macos/example/README.md +++ b/packages/path_provider/path_provider_macos/example/README.md @@ -5,4 +5,4 @@ Demonstrates how to use the path_provider_macos plugin. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). diff --git a/packages/path_provider/path_provider_macos/example/integration_test/path_provider_test.dart b/packages/path_provider/path_provider_macos/example/integration_test/path_provider_test.dart index 58a4d1805cb0..09ed8e69be24 100644 --- a/packages/path_provider/path_provider_macos/example/integration_test/path_provider_test.dart +++ b/packages/path_provider/path_provider_macos/example/integration_test/path_provider_test.dart @@ -1,43 +1,59 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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_provider/path_provider.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 Directory result = await getTemporaryDirectory(); + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getTemporaryPath(); _verifySampleFile(result, 'temporaryDirectory'); }); testWidgets('getApplicationDocumentsDirectory', (WidgetTester tester) async { - final Directory result = await getApplicationDocumentsDirectory(); + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getApplicationDocumentsPath(); _verifySampleFile(result, 'applicationDocuments'); }); testWidgets('getApplicationSupportDirectory', (WidgetTester tester) async { - final Directory result = await getApplicationSupportDirectory(); + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getApplicationSupportPath(); _verifySampleFile(result, 'applicationSupport'); }); testWidgets('getLibraryDirectory', (WidgetTester tester) async { - if (!Platform.isMacOS) { - return; - } - final Directory result = await getLibraryDirectory(); + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getLibraryPath(); _verifySampleFile(result, 'library'); }); + + testWidgets('getDownloadsDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getDownloadsPath(); + // _verifySampleFile causes hangs in driver for some reason, so just + // validate that a non-empty path was returned. + expect(result, isNotEmpty); + }); } -/// Verify a file called [name] in [directory] by recreating it with test +/// Verify a file called [name] in [directoryPath] by recreating it with test /// contents when necessary. -void _verifySampleFile(Directory directory, String name) { - final File file = File('${directory.path}/$name'); +/// +/// If [createDirectory] is true, the directory will be created if missing. +void _verifySampleFile(String? directoryPath, String name) { + 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(); diff --git a/packages/path_provider/path_provider_macos/example/ios/Flutter/AppFrameworkInfo.plist b/packages/path_provider/path_provider_macos/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6c2de8086bcd..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - UIRequiredDeviceCapabilities - - arm64 - - MinimumOSVersion - 8.0 - - diff --git a/packages/path_provider/path_provider_macos/example/ios/Flutter/Debug.xcconfig b/packages/path_provider/path_provider_macos/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index 9803018ca79d..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Generated.xcconfig" -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" diff --git a/packages/path_provider/path_provider_macos/example/ios/Flutter/Release.xcconfig b/packages/path_provider/path_provider_macos/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index a4a8c604e13d..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Generated.xcconfig" -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner.xcodeproj/project.pbxproj b/packages/path_provider/path_provider_macos/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index eb0222a7c9c5..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,490 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 2D9222481EC32A19007564B0 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D9222471EC32A19007564B0 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 85DDFCF6BBDEE02B9D9F8138 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = C0EE60090AA5F3AAAF2175B6 /* libPods-Runner.a */; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 2D9222461EC32A19007564B0 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 2D9222471EC32A19007564B0 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; - 694A199F61914F41AAFD0B7F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - C0EE60090AA5F3AAAF2175B6 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - D317CA1E83064E01753D8BB5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - 85DDFCF6BBDEE02B9D9F8138 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 840012C8B5EDBCF56B0E4AC1 /* Pods */ = { - isa = PBXGroup; - children = ( - 694A199F61914F41AAFD0B7F /* Pods-Runner.debug.xcconfig */, - D317CA1E83064E01753D8BB5 /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B80C3931E831B6300D905FE /* App.framework */, - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 840012C8B5EDBCF56B0E4AC1 /* Pods */, - CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 2D9222461EC32A19007564B0 /* GeneratedPluginRegistrant.h */, - 2D9222471EC32A19007564B0 /* GeneratedPluginRegistrant.m */, - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - CF3B75C9A7D2FA2A4C99F110 /* Frameworks */ = { - isa = PBXGroup; - children = ( - C0EE60090AA5F3AAAF2175B6 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; - }; - 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; - AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 2D9222481EC32A19007564B0 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.pathProviderExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.pathProviderExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/path_provider/path_provider_macos/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14c74e..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/path_provider/path_provider_macos/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 3bb3697ef41c..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/path_provider/path_provider_macos/example/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14c74e..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/AppDelegate.h b/packages/path_provider/path_provider_macos/example/ios/Runner/AppDelegate.h deleted file mode 100644 index d9e18e990f2e..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/AppDelegate.m b/packages/path_provider/path_provider_macos/example/ios/Runner/AppDelegate.m deleted file mode 100644 index a4b51c88eb60..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d22f10b2ab63..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf03016f..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca8..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cde12118dda..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb8..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index dcdc2306c285..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a9..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 6a84f41e14e2..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index d0e1f5853602..000000000000 Binary files a/packages/path_provider/path_provider_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/path_provider/path_provider_macos/example/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index ebf48f603974..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Base.lproj/Main.storyboard b/packages/path_provider/path_provider_macos/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516fb38..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/Info.plist b/packages/path_provider/path_provider_macos/example/ios/Runner/Info.plist deleted file mode 100644 index 342db6a5dcaf..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - path_provider_example - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner/main.m b/packages/path_provider/path_provider_macos/example/ios/Runner/main.m deleted file mode 100644 index bec320c0bee0..000000000000 --- a/packages/path_provider/path_provider_macos/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/path_provider/path_provider_macos/example/lib/main.dart b/packages/path_provider/path_provider_macos/example/lib/main.dart index 473a989914f6..67a0eb32eeda 100644 --- a/packages/path_provider/path_provider_macos/example/lib/main.dart +++ b/packages/path_provider/path_provider_macos/example/lib/main.dart @@ -1,147 +1,89 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter 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' show Directory; - import 'package:flutter/material.dart'; -import 'package:path_provider/path_provider.dart'; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; void main() { runApp(MyApp()); } -class MyApp extends StatelessWidget { +/// Sample app +class MyApp extends StatefulWidget { @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Path Provider', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: MyHomePage(title: 'Path Provider'), - ); - } + _MyAppState createState() => _MyAppState(); } -class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); - final String title; +class _MyAppState extends State { + String? _tempDirectory = 'Unknown'; + String? _downloadsDirectory = 'Unknown'; + String? _appSupportDirectory = 'Unknown'; + String? _documentsDirectory = 'Unknown'; @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - Future _tempDirectory; - Future _appSupportDirectory; - Future _appDocumentsDirectory; - Future _appLibraryDirectory; - Future _downloadsDirectory; - - void _requestTempDirectory() { - setState(() { - _tempDirectory = getTemporaryDirectory(); - }); + void initState() { + super.initState(); + initDirectories(); } - Widget _buildDirectory( - BuildContext context, AsyncSnapshot snapshot) { - Text text = const Text(''); - if (snapshot.connectionState == ConnectionState.done) { - if (snapshot.hasError) { - text = Text('Error: ${snapshot.error}'); - } else if (snapshot.hasData) { - text = Text('path: ${snapshot.data.path}'); - } else { - text = const Text('path unavailable'); - } - } - return Padding(padding: const EdgeInsets.all(16.0), child: text); - } + // Platform messages are asynchronous, so we initialize in an async method. + Future initDirectories() async { + String? tempDirectory; + String? downloadsDirectory; + String? appSupportDirectory; + String? documentsDirectory; + final PathProviderPlatform provider = PathProviderPlatform.instance; - void _requestAppDocumentsDirectory() { - setState(() { - _appDocumentsDirectory = getApplicationDocumentsDirectory(); - }); - } + try { + tempDirectory = await provider.getTemporaryPath(); + } catch (exception) { + tempDirectory = 'Failed to get temp directory: $exception'; + } + try { + downloadsDirectory = await provider.getDownloadsPath(); + } catch (exception) { + downloadsDirectory = 'Failed to get downloads directory: $exception'; + } - void _requestAppSupportDirectory() { - setState(() { - _appSupportDirectory = getApplicationSupportDirectory(); - }); - } + try { + documentsDirectory = await provider.getApplicationDocumentsPath(); + } catch (exception) { + documentsDirectory = 'Failed to get documents directory: $exception'; + } - void _requestAppLibraryDirectory() { - setState(() { - _appLibraryDirectory = getLibraryDirectory(); - }); - } + try { + appSupportDirectory = await provider.getApplicationSupportPath(); + } catch (exception) { + appSupportDirectory = 'Failed to get app support directory: $exception'; + } - void _requestDownloadsDirectory() { setState(() { - _downloadsDirectory = getDownloadsDirectory(); + _tempDirectory = tempDirectory; + _downloadsDirectory = downloadsDirectory; + _appSupportDirectory = appSupportDirectory; + _documentsDirectory = documentsDirectory; }); } @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text(widget.title), - ), - body: Center( - child: ListView( - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: RaisedButton( - child: const Text('Get Temporary Directory'), - onPressed: _requestTempDirectory, - ), - ), - FutureBuilder( - future: _tempDirectory, builder: _buildDirectory), - Padding( - padding: const EdgeInsets.all(16.0), - child: RaisedButton( - child: const Text('Get Application Documents Directory'), - onPressed: _requestAppDocumentsDirectory, - ), - ), - FutureBuilder( - future: _appDocumentsDirectory, builder: _buildDirectory), - Padding( - padding: const EdgeInsets.all(16.0), - child: RaisedButton( - child: const Text('Get Application Support Directory'), - onPressed: _requestAppSupportDirectory, - ), - ), - FutureBuilder( - future: _appSupportDirectory, builder: _buildDirectory), - Padding( - padding: const EdgeInsets.all(16.0), - child: RaisedButton( - child: const Text('Get Application Library Directory'), - onPressed: _requestAppLibraryDirectory, - ), - ), - FutureBuilder( - future: _appLibraryDirectory, builder: _buildDirectory), - Padding( - padding: const EdgeInsets.all(16.0), - child: RaisedButton( - child: const Text('Get Downlads Directory'), - onPressed: _requestDownloadsDirectory, - ), - ), - FutureBuilder( - future: _downloadsDirectory, builder: _buildDirectory), - ], + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Path Provider example app'), + ), + body: Center( + child: Column( + children: [ + Text('Temp Directory: $_tempDirectory\n'), + Text('Documents Directory: $_documentsDirectory\n'), + Text('Downloads Directory: $_downloadsDirectory\n'), + Text('Application Support Directory: $_appSupportDirectory\n'), + ], + ), ), ), ); diff --git a/packages/path_provider/path_provider_macos/example/macos/Podfile b/packages/path_provider/path_provider_macos/example/macos/Podfile new file mode 100644 index 000000000000..e8da8332969a --- /dev/null +++ b/packages/path_provider/path_provider_macos/example/macos/Podfile @@ -0,0 +1,44 @@ +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__)) + + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner.xcodeproj/project.pbxproj b/packages/path_provider/path_provider_macos/example/macos/Runner.xcodeproj/project.pbxproj index 1e39683e1446..a63463993c6e 100644 --- a/packages/path_provider/path_provider_macos/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/path_provider/path_provider_macos/example/macos/Runner.xcodeproj/project.pbxproj @@ -27,10 +27,8 @@ 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, ); }; }; + 33EBD3AA26728EA70013E557 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33EBD3A926728EA70013E557 /* RunnerTests.swift */; }; + FEE1C654F5DF2F210CC17B17 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BA0C143378C83246316BE4F7 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -41,6 +39,13 @@ remoteGlobalIDString = 33CC111A2044C6BA0003C045; remoteInfo = FLX; }; + 33EBD3AC26728EA70013E557 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -50,8 +55,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */, - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */, ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; @@ -60,7 +63,10 @@ /* Begin PBXFileReference section */ 0A1A53CF00FD04D6ED0A8E4A /* 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 = ""; }; + 0B41979101786837FC1ABC29 /* 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 = ""; }; + 0B43E5DCF2F998ABCD395373 /* 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 = ""; }; 1523F64D34B952AB303BFFA8 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1C62AF358280E9A8FA10B127 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 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 /* path_provider_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = path_provider_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -72,14 +78,16 @@ 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 = ""; }; + 33EBD3A726728EA70013E557 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 33EBD3A926728EA70013E557 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 33EBD3AB26728EA70013E557 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 46139048DB9F59D473B61B5E /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - D73912EF22F37F9E000D13A0 /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/ephemeral/App.framework; sourceTree = SOURCE_ROOT; }; + BA0C143378C83246316BE4F7 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F4586DA69948E3A954A2FC9C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -88,12 +96,18 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */, - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */, 23F6FAA3AF82DFCF2B7DD79A /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; + 33EBD3A426728EA70013E557 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FEE1C654F5DF2F210CC17B17 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -103,8 +117,10 @@ 46139048DB9F59D473B61B5E /* Pods-Runner.debug.xcconfig */, F4586DA69948E3A954A2FC9C /* Pods-Runner.release.xcconfig */, 0A1A53CF00FD04D6ED0A8E4A /* Pods-Runner.profile.xcconfig */, + 0B43E5DCF2F998ABCD395373 /* Pods-RunnerTests.debug.xcconfig */, + 0B41979101786837FC1ABC29 /* Pods-RunnerTests.release.xcconfig */, + 1C62AF358280E9A8FA10B127 /* Pods-RunnerTests.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -124,6 +140,7 @@ children = ( 33FAB671232836740065AC1E /* Runner */, 33CEB47122A05771004F2AC0 /* Flutter */, + 33EBD3A826728EA70013E557 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, 30697CBF35C100C7DD4B4699 /* Pods */, @@ -134,6 +151,7 @@ isa = PBXGroup; children = ( 33CC10ED2044A3C60003C045 /* path_provider_example.app */, + 33EBD3A726728EA70013E557 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -156,12 +174,19 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - D73912EF22F37F9E000D13A0 /* App.framework */, - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */, ); path = Flutter; sourceTree = ""; }; + 33EBD3A826728EA70013E557 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 33EBD3A926728EA70013E557 /* RunnerTests.swift */, + 33EBD3AB26728EA70013E557 /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; 33FAB671232836740065AC1E /* Runner */ = { isa = PBXGroup; children = ( @@ -179,6 +204,7 @@ isa = PBXGroup; children = ( 1523F64D34B952AB303BFFA8 /* Pods_Runner.framework */, + BA0C143378C83246316BE4F7 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -208,13 +234,32 @@ productReference = 33CC10ED2044A3C60003C045 /* path_provider_example.app */; productType = "com.apple.product-type.application"; }; + 33EBD3A626728EA70013E557 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33EBD3B126728EA70013E557 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 74960BD2BEA7516F537D0F92 /* [CP] Check Pods Manifest.lock */, + 33EBD3A326728EA70013E557 /* Sources */, + 33EBD3A426728EA70013E557 /* Frameworks */, + 33EBD3A526728EA70013E557 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 33EBD3AD26728EA70013E557 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 33EBD3A726728EA70013E557 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 33CC10E52044A3C60003C045 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 0920; + LastSwiftUpdateCheck = 1250; LastUpgradeCheck = 0930; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { @@ -232,6 +277,10 @@ CreatedOnToolsVersion = 9.2; ProvisioningStyle = Manual; }; + 33EBD3A626728EA70013E557 = { + CreatedOnToolsVersion = 12.5; + TestTargetID = 33CC10EC2044A3C60003C045; + }; }; }; buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; @@ -249,6 +298,7 @@ targets = ( 33CC10EC2044A3C60003C045 /* Runner */, 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + 33EBD3A626728EA70013E557 /* RunnerTests */, ); }; /* End PBXProject section */ @@ -263,6 +313,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 33EBD3A526728EA70013E557 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -281,7 +338,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename\n"; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; }; 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -308,16 +365,41 @@ buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/path_provider_macos/path_provider_macos.framework", ); name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_macos.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; + 74960BD2BEA7516F537D0F92 /* [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; + }; 82C3ED26F2C350499338A54B /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -353,6 +435,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 33EBD3A326728EA70013E557 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33EBD3AA26728EA70013E557 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -361,6 +451,11 @@ target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; }; + 33EBD3AD26728EA70013E557 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 33EBD3AC26728EA70013E557 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -615,6 +710,63 @@ }; name = Release; }; + 33EBD3AE26728EA70013E557 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0B43E5DCF2F998ABCD395373 /* Pods-RunnerTests.debug.xcconfig */; + 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)/path_provider_example.app/Contents/MacOS/path_provider_example"; + }; + name = Debug; + }; + 33EBD3AF26728EA70013E557 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 0B41979101786837FC1ABC29 /* Pods-RunnerTests.release.xcconfig */; + 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)/path_provider_example.app/Contents/MacOS/path_provider_example"; + }; + name = Release; + }; + 33EBD3B026728EA70013E557 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1C62AF358280E9A8FA10B127 /* Pods-RunnerTests.profile.xcconfig */; + 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)/path_provider_example.app/Contents/MacOS/path_provider_example"; + }; + name = Profile; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -648,6 +800,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 33EBD3B126728EA70013E557 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33EBD3AE26728EA70013E557 /* Debug */, + 33EBD3AF26728EA70013E557 /* Release */, + 33EBD3B026728EA70013E557 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/path_provider/path_provider_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 1552901c04e0..a0f91afed8ea 100644 --- a/packages/path_provider/path_provider_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/path_provider/path_provider_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -27,6 +27,15 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + @@ -38,18 +47,17 @@ ReferencedContainer = "container:Runner.xcodeproj"> + + + + - - - - - - - - com.apple.security.network.server + com.apple.security.files.downloads.read-write + diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner/MainFlutterWindow.swift b/packages/path_provider/path_provider_macos/example/macos/Runner/MainFlutterWindow.swift index 2722837ec918..32aaeedceb1f 100644 --- a/packages/path_provider/path_provider_macos/example/macos/Runner/MainFlutterWindow.swift +++ b/packages/path_provider/path_provider_macos/example/macos/Runner/MainFlutterWindow.swift @@ -1,3 +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 Cocoa import FlutterMacOS diff --git a/packages/path_provider/path_provider_macos/example/macos/Runner/Release.entitlements b/packages/path_provider/path_provider_macos/example/macos/Runner/Release.entitlements index 852fa1a4728a..2f9659c917fb 100644 --- a/packages/path_provider/path_provider_macos/example/macos/Runner/Release.entitlements +++ b/packages/path_provider/path_provider_macos/example/macos/Runner/Release.entitlements @@ -4,5 +4,7 @@ com.apple.security.app-sandbox + com.apple.security.files.downloads.read-write + diff --git a/packages/path_provider/path_provider_macos/example/macos/RunnerTests/Info.plist b/packages/path_provider/path_provider_macos/example/macos/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/path_provider/path_provider_macos/example/macos/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/path_provider/path_provider_macos/example/macos/RunnerTests/RunnerTests.swift b/packages/path_provider/path_provider_macos/example/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 000000000000..35704cdb06d8 --- /dev/null +++ b/packages/path_provider/path_provider_macos/example/macos/RunnerTests/RunnerTests.swift @@ -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 FlutterMacOS +import XCTest +import path_provider_macos + +class RunnerTests: XCTestCase { + func testGetTemporaryDirectory() throws { + let plugin = PathProviderPlugin() + var path: String? + plugin.handle( + FlutterMethodCall(methodName: "getTemporaryDirectory", arguments: nil), + result: { (result: Any?) -> Void in + path = result as? String + + }) + XCTAssertEqual( + path, + NSSearchPathForDirectoriesInDomains( + FileManager.SearchPathDirectory.cachesDirectory, + FileManager.SearchPathDomainMask.userDomainMask, + true + ).first) + } + + func testGetApplicationDocumentsDirectory() throws { + let plugin = PathProviderPlugin() + var path: String? + plugin.handle( + FlutterMethodCall(methodName: "getApplicationDocumentsDirectory", arguments: nil), + result: { (result: Any?) -> Void in + path = result as? String + + }) + XCTAssertEqual( + path, + NSSearchPathForDirectoriesInDomains( + FileManager.SearchPathDirectory.documentDirectory, + FileManager.SearchPathDomainMask.userDomainMask, + true + ).first) + } + + func testGetApplicationSupportDirectory() throws { + let plugin = PathProviderPlugin() + var path: String? + plugin.handle( + FlutterMethodCall(methodName: "getApplicationSupportDirectory", arguments: nil), + result: { (result: Any?) -> Void in + path = result as? String + + }) + // The application support directory path should be the system application support + // path with an added subdirectory based on the app name. + XCTAssert( + path!.hasPrefix( + NSSearchPathForDirectoriesInDomains( + FileManager.SearchPathDirectory.applicationSupportDirectory, + FileManager.SearchPathDomainMask.userDomainMask, + true + ).first!)) + XCTAssert(path!.hasSuffix("Example")) + } + + func testGetLibraryDirectory() throws { + let plugin = PathProviderPlugin() + var path: String? + plugin.handle( + FlutterMethodCall(methodName: "getLibraryDirectory", arguments: nil), + result: { (result: Any?) -> Void in + path = result as? String + + }) + XCTAssertEqual( + path, + NSSearchPathForDirectoriesInDomains( + FileManager.SearchPathDirectory.libraryDirectory, + FileManager.SearchPathDomainMask.userDomainMask, + true + ).first) + } + + func testGetDownloadsDirectory() throws { + let plugin = PathProviderPlugin() + var path: String? + plugin.handle( + FlutterMethodCall(methodName: "getDownloadsDirectory", arguments: nil), + result: { (result: Any?) -> Void in + path = result as? String + + }) + XCTAssertEqual( + path, + NSSearchPathForDirectoriesInDomains( + FileManager.SearchPathDirectory.downloadsDirectory, + FileManager.SearchPathDomainMask.userDomainMask, + true + ).first) + } +} diff --git a/packages/path_provider/path_provider_macos/example/pubspec.yaml b/packages/path_provider/path_provider_macos/example/pubspec.yaml index aaa6842651b5..d8b93545ed53 100644 --- a/packages/path_provider/path_provider_macos/example/pubspec.yaml +++ b/packages/path_provider/path_provider_macos/example/pubspec.yaml @@ -1,23 +1,28 @@ name: path_provider_example description: Demonstrates how to use the path_provider plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: sdk: flutter - path_provider: ^1.6.14 - -# path_provider_macos is endorsed, so we need to add it to dependency_overrides -# to depend on it from path: -dependency_overrides: path_provider_macos: + # When depending on this package from a real application you should use: + # path_provider_macos: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ + path_provider_platform_interface: ^2.0.0 dev_dependencies: - integration_test: - path: ../../../integration_test flutter_driver: sdk: flutter - test: any + integration_test: + sdk: flutter pedantic: ^1.8.0 flutter: diff --git a/packages/path_provider/path_provider_macos/example/test_driver/integration_test.dart b/packages/path_provider/path_provider_macos/example/test_driver/integration_test.dart index 7a2c21338786..4f10f2a522f3 100644 --- a/packages/path_provider/path_provider_macos/example/test_driver/integration_test.dart +++ b/packages/path_provider/path_provider_macos/example/test_driver/integration_test.dart @@ -1,17 +1,7 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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'; +import 'package:integration_test/integration_test_driver.dart'; -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String data = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - await driver.close(); - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} +Future main() => integrationDriver(); diff --git a/packages/path_provider/path_provider_macos/ios/path_provider_macos.podspec b/packages/path_provider/path_provider_macos/ios/path_provider_macos.podspec deleted file mode 100644 index 9f822c58c45c..000000000000 --- a/packages/path_provider/path_provider_macos/ios/path_provider_macos.podspec +++ /dev/null @@ -1,22 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'path_provider_macos' - s.version = '0.0.1' - s.summary = 'No-op implementation of path_provider macOS plugin to avoid build issues on iOS' - s.description = <<-DESC - No-op implementation of path_provider macOS plugin - See https://github.com/flutter/flutter/issues/39659 - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_macos' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.ios.deployment_target = '8.0' -end - diff --git a/packages/path_provider/path_provider_macos/lib/path_provider_macos.dart b/packages/path_provider/path_provider_macos/lib/path_provider_macos.dart deleted file mode 100644 index cf440b2858af..000000000000 --- a/packages/path_provider/path_provider_macos/lib/path_provider_macos.dart +++ /dev/null @@ -1,3 +0,0 @@ -// Analyze will fail if there is no main.dart file. This file should -// be removed once an example app has been added to path_provider_macos. -// https://github.com/flutter/flutter/issues/51007 diff --git a/packages/path_provider/path_provider_macos/macos/Classes/PathProviderPlugin.swift b/packages/path_provider/path_provider_macos/macos/Classes/PathProviderPlugin.swift index a1528822893f..b308793be355 100644 --- a/packages/path_provider/path_provider_macos/macos/Classes/PathProviderPlugin.swift +++ b/packages/path_provider/path_provider_macos/macos/Classes/PathProviderPlugin.swift @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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_macos/macos/path_provider_macos.podspec b/packages/path_provider/path_provider_macos/macos/path_provider_macos.podspec index be47ddb18b16..66b5872c9ac9 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 @@ -17,5 +17,6 @@ Pod::Spec.new do |s| s.platform = :osx s.osx.deployment_target = '10.11' + s.swift_version = '5.0' end diff --git a/packages/path_provider/path_provider_macos/pubspec.yaml b/packages/path_provider/path_provider_macos/pubspec.yaml index 970c8c59fe7a..329bffa61c10 100644 --- a/packages/path_provider/path_provider_macos/pubspec.yaml +++ b/packages/path_provider/path_provider_macos/pubspec.yaml @@ -1,24 +1,23 @@ name: path_provider_macos description: macOS implementation of the path_provider plugin -# 0.0.y+z is compatible with 1.0.0, if you land a breaking change bump -# the version to 2.0.0. -# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.0.4+5 -homepage: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_macos +repository: https://github.com/flutter/plugins/tree/master/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.1 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: + implements: path_provider platforms: macos: pluginClass: PathProviderPlugin -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" - dependencies: flutter: sdk: flutter dev_dependencies: - pedantic: ^1.8.0 + pedantic: ^1.10.0 diff --git a/packages/path_provider/path_provider_platform_interface/AUTHORS b/packages/path_provider/path_provider_platform_interface/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/path_provider/path_provider_platform_interface/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/path_provider/path_provider_platform_interface/CHANGELOG.md b/packages/path_provider/path_provider_platform_interface/CHANGELOG.md index 23f71c99a678..eec0fe3866b5 100644 --- a/packages/path_provider/path_provider_platform_interface/CHANGELOG.md +++ b/packages/path_provider/path_provider_platform_interface/CHANGELOG.md @@ -1,3 +1,19 @@ +## 2.0.1 + +* Update platform_plugin_interface version requirement. + +## 2.0.0 + +* Migrate to null safety. + +## 1.0.5 + +* Update Flutter SDK constraint. + +## 1.0.4 + +* Remove unused `test` dependency. + ## 1.0.3 * Increase upper range of `package:platform` constraint to allow 3.X versions. diff --git a/packages/path_provider/path_provider_platform_interface/LICENSE b/packages/path_provider/path_provider_platform_interface/LICENSE index d7412e0a1e0c..c6823b81eb84 100644 --- a/packages/path_provider/path_provider_platform_interface/LICENSE +++ b/packages/path_provider/path_provider_platform_interface/LICENSE @@ -1,4 +1,4 @@ -Copyright 2020 The Chromium Authors. All rights reserved. +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: diff --git a/packages/path_provider/path_provider_platform_interface/lib/path_provider_platform_interface.dart b/packages/path_provider/path_provider_platform_interface/lib/path_provider_platform_interface.dart index 4f796aaeec33..99e600d05263 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 @@ -1,14 +1,12 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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_path_provider.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - export 'src/enums.dart'; /// The interface that implementations of path_provider must implement. @@ -40,26 +38,26 @@ abstract class PathProviderPlatform extends PlatformInterface { /// Path to the temporary directory on the device that is not backed up and is /// suitable for storing caches of downloaded files. - Future getTemporaryPath() { + Future getTemporaryPath() { throw UnimplementedError('getTemporaryPath() has not been implemented.'); } /// Path to a directory where the application may place application support /// files. - Future getApplicationSupportPath() { + Future getApplicationSupportPath() { throw UnimplementedError( 'getApplicationSupportPath() has not been implemented.'); } /// Path to the directory where application can store files that are persistent, /// backed up, and not visible to the user, such as sqlite.db. - Future getLibraryPath() { + Future getLibraryPath() { throw UnimplementedError('getLibraryPath() has not been implemented.'); } /// Path to a directory where the application may place data that is /// user-generated, or that cannot otherwise be recreated by your application. - Future getApplicationDocumentsPath() { + Future getApplicationDocumentsPath() { throw UnimplementedError( 'getApplicationDocumentsPath() has not been implemented.'); } @@ -67,7 +65,7 @@ abstract class PathProviderPlatform extends PlatformInterface { /// Path to a directory where the application may access top level storage. /// The current operating system should be determined before issuing this /// function call, as this functionality is only available on Android. - Future getExternalStoragePath() { + Future getExternalStoragePath() { throw UnimplementedError( 'getExternalStoragePath() has not been implemented.'); } @@ -76,7 +74,7 @@ abstract class PathProviderPlatform extends PlatformInterface { /// stored. These paths typically reside on external storage like separate /// partitions or SD cards. Phones may have multiple storage directories /// available. - Future> getExternalCachePaths() { + Future?> getExternalCachePaths() { throw UnimplementedError( 'getExternalCachePaths() has not been implemented.'); } @@ -84,10 +82,10 @@ abstract class PathProviderPlatform extends PlatformInterface { /// Paths to directories where application specific data can be stored. /// These paths typically reside on external storage like separate partitions /// or SD cards. Phones may have multiple storage directories available. - Future> getExternalStoragePaths({ + Future?> getExternalStoragePaths({ /// Optional parameter. See [StorageDirectory] for more informations on /// how this type translates to Android storage directories. - StorageDirectory type, + StorageDirectory? type, }) { throw UnimplementedError( 'getExternalStoragePaths() has not been implemented.'); @@ -95,7 +93,7 @@ abstract class PathProviderPlatform extends PlatformInterface { /// Path to the directory where downloaded files can be stored. /// This is typically only relevant on desktop operating systems. - Future getDownloadsPath() { + Future getDownloadsPath() { throw UnimplementedError('getDownloadsPath() has not been implemented.'); } } diff --git a/packages/path_provider/path_provider_platform_interface/lib/src/enums.dart b/packages/path_provider/path_provider_platform_interface/lib/src/enums.dart index c97ef5d2b0f5..e355d7d1a5be 100644 --- a/packages/path_provider/path_provider_platform_interface/lib/src/enums.dart +++ b/packages/path_provider/path_provider_platform_interface/lib/src/enums.dart @@ -1,3 +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. + /// Corresponds to constants defined in Androids `android.os.Environment` class. /// /// https://developer.android.com/reference/android/os/Environment.html#fields_1 diff --git a/packages/path_provider/path_provider_platform_interface/lib/src/method_channel_path_provider.dart b/packages/path_provider/path_provider_platform_interface/lib/src/method_channel_path_provider.dart index 7826fa4365be..007787444adb 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 @@ -1,22 +1,20 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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 'enums.dart'; - 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. @visibleForTesting MethodChannel methodChannel = - MethodChannel('plugins.flutter.io/path_provider'); + const MethodChannel('plugins.flutter.io/path_provider'); // Ideally, this property shouldn't exist, and each platform should // just implement the supported methods. Once all the platforms are @@ -30,34 +28,40 @@ class MethodChannelPathProvider extends PathProviderPlatform { _platform = platform; } - Future getTemporaryPath() { + @override + Future getTemporaryPath() { return methodChannel.invokeMethod('getTemporaryDirectory'); } - Future getApplicationSupportPath() { + @override + Future getApplicationSupportPath() { return methodChannel.invokeMethod('getApplicationSupportDirectory'); } - Future getLibraryPath() { + @override + Future getLibraryPath() { if (!_platform.isIOS && !_platform.isMacOS) { throw UnsupportedError('Functionality only available on iOS/macOS'); } return methodChannel.invokeMethod('getLibraryDirectory'); } - Future getApplicationDocumentsPath() { + @override + Future getApplicationDocumentsPath() { return methodChannel .invokeMethod('getApplicationDocumentsDirectory'); } - Future getExternalStoragePath() { + @override + Future getExternalStoragePath() { if (!_platform.isAndroid) { throw UnsupportedError('Functionality only available on Android'); } return methodChannel.invokeMethod('getStorageDirectory'); } - Future> getExternalCachePaths() { + @override + Future?> getExternalCachePaths() { if (!_platform.isAndroid) { throw UnsupportedError('Functionality only available on Android'); } @@ -65,8 +69,9 @@ class MethodChannelPathProvider extends PathProviderPlatform { .invokeListMethod('getExternalCacheDirectories'); } - Future> getExternalStoragePaths({ - StorageDirectory type, + @override + Future?> getExternalStoragePaths({ + StorageDirectory? type, }) async { if (!_platform.isAndroid) { throw UnsupportedError('Functionality only available on Android'); @@ -77,7 +82,8 @@ class MethodChannelPathProvider extends PathProviderPlatform { ); } - Future getDownloadsPath() { + @override + Future getDownloadsPath() { if (!_platform.isMacOS) { throw UnsupportedError('Functionality only available on macOS'); } diff --git a/packages/path_provider/path_provider_platform_interface/pubspec.yaml b/packages/path_provider/path_provider_platform_interface/pubspec.yaml index 9f1293883826..7fe5e8dfc232 100644 --- a/packages/path_provider/path_provider_platform_interface/pubspec.yaml +++ b/packages/path_provider/path_provider_platform_interface/pubspec.yaml @@ -1,23 +1,23 @@ name: path_provider_platform_interface description: A common platform interface for the path_provider plugin. -homepage: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_platform_interface +repository: https://github.com/flutter/plugins/tree/master/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: 1.0.3 +version: 2.0.1 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" dependencies: flutter: sdk: flutter - meta: ^1.0.5 - platform: ">=2.0.0 <4.0.0" - plugin_platform_interface: ^1.0.1 + meta: ^1.3.0 + platform: ^3.0.0 + plugin_platform_interface: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 - test: any - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + pedantic: ^1.10.0 diff --git a/packages/path_provider/path_provider_platform_interface/test/method_channel_path_provider_test.dart b/packages/path_provider/path_provider_platform_interface/test/method_channel_path_provider_test.dart index 99c9349f9ae5..69c9b2b01f19 100644 --- a/packages/path_provider/path_provider_platform_interface/test/method_channel_path_provider_test.dart +++ b/packages/path_provider/path_provider_platform_interface/test/method_channel_path_provider_test.dart @@ -1,4 +1,4 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -19,7 +19,7 @@ void main() { const String kDownloadsPath = 'downloadsPath'; group('$MethodChannelPathProvider', () { - MethodChannelPathProvider methodChannelPathProvider; + late MethodChannelPathProvider methodChannelPathProvider; final List log = []; setUp(() async { @@ -59,7 +59,7 @@ void main() { }); test('getTemporaryPath', () async { - final String path = await methodChannelPathProvider.getTemporaryPath(); + final String? path = await methodChannelPathProvider.getTemporaryPath(); expect( log, [isMethodCall('getTemporaryDirectory', arguments: null)], @@ -68,7 +68,7 @@ void main() { }); test('getApplicationSupportPath', () async { - final String path = + final String? path = await methodChannelPathProvider.getApplicationSupportPath(); expect( log, @@ -92,7 +92,7 @@ void main() { methodChannelPathProvider .setMockPathProviderPlatform(FakePlatform(operatingSystem: 'ios')); - final String path = await methodChannelPathProvider.getLibraryPath(); + final String? path = await methodChannelPathProvider.getLibraryPath(); expect( log, [isMethodCall('getLibraryDirectory', arguments: null)], @@ -104,7 +104,7 @@ void main() { methodChannelPathProvider .setMockPathProviderPlatform(FakePlatform(operatingSystem: 'macos')); - final String path = await methodChannelPathProvider.getLibraryPath(); + final String? path = await methodChannelPathProvider.getLibraryPath(); expect( log, [isMethodCall('getLibraryDirectory', arguments: null)], @@ -113,7 +113,7 @@ void main() { }); test('getApplicationDocumentsPath', () async { - final String path = + final String? path = await methodChannelPathProvider.getApplicationDocumentsPath(); expect( log, @@ -125,13 +125,13 @@ void main() { }); test('getExternalCachePaths android succeeds', () async { - final List result = + final List? result = await methodChannelPathProvider.getExternalCachePaths(); expect( log, [isMethodCall('getExternalCacheDirectories', arguments: null)], ); - expect(result.length, 1); + expect(result!.length, 1); expect(result.first, kExternalCachePaths); }); @@ -147,10 +147,12 @@ void main() { } }); - for (StorageDirectory type - in StorageDirectory.values + [null]) { + for (final StorageDirectory? type in [ + null, + ...StorageDirectory.values + ]) { test('getExternalStoragePaths (type: $type) android succeeds', () async { - final List result = + final List? result = await methodChannelPathProvider.getExternalStoragePaths(type: type); expect( log, @@ -162,7 +164,7 @@ void main() { ], ); - expect(result.length, 1); + expect(result!.length, 1); expect(result.first, kExternalStoragePaths); }); @@ -182,7 +184,7 @@ void main() { test('getDownloadsPath macos succeeds', () async { methodChannelPathProvider .setMockPathProviderPlatform(FakePlatform(operatingSystem: 'macos')); - final String result = await methodChannelPathProvider.getDownloadsPath(); + final String? result = await methodChannelPathProvider.getDownloadsPath(); expect( log, [isMethodCall('getDownloadsDirectory', arguments: null)], diff --git a/packages/path_provider/path_provider_windows/AUTHORS b/packages/path_provider/path_provider_windows/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/path_provider/path_provider_windows/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/path_provider/path_provider_windows/CHANGELOG.md b/packages/path_provider/path_provider_windows/CHANGELOG.md index b7bc07e82bc7..2e4da0e1f353 100644 --- a/packages/path_provider/path_provider_windows/CHANGELOG.md +++ b/packages/path_provider/path_provider_windows/CHANGELOG.md @@ -1,3 +1,29 @@ +## 2.0.2 + +* Add `implements` to pubspec.yaml. +* Add `registerWith()` to the Dart main class. + +## 2.0.1 + +* Fix a crash when a known folder can't be located. + +## 2.0.0 + +* Migrate to null safety + +## 0.0.4+4 + +* Update Flutter SDK constraint. + +## 0.0.4+3 + +* Remove unused `test` dependency. +* Update Dart SDK constraint in example. + +## 0.0.4+2 + +* Check in windows/ directory for example/ + ## 0.0.4+1 * Add getPath to the stub, so that the analyzer won't complain about diff --git a/packages/path_provider/path_provider_windows/LICENSE b/packages/path_provider/path_provider_windows/LICENSE index a6d6c0749818..c6823b81eb84 100644 --- a/packages/path_provider/path_provider_windows/LICENSE +++ b/packages/path_provider/path_provider_windows/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017 The Chromium Authors. All rights reserved. +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: diff --git a/packages/path_provider/path_provider_windows/README.md b/packages/path_provider/path_provider_windows/README.md index 66a05f9e7347..6d452e770469 100644 --- a/packages/path_provider/path_provider_windows/README.md +++ b/packages/path_provider/path_provider_windows/README.md @@ -2,14 +2,6 @@ The Windows implementation of [`path_provider`][1]. -**Please set your constraint to `path_provider_windows: '>=0.0.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming - -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.0.y+z`. -Please use `path_provider_windows: '>=0.0.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ## Usage ### Import the package diff --git a/packages/path_provider/path_provider_windows/example/README.md b/packages/path_provider/path_provider_windows/example/README.md index f3ca03ff37c3..32f66a86d11d 100644 --- a/packages/path_provider/path_provider_windows/example/README.md +++ b/packages/path_provider/path_provider_windows/example/README.md @@ -5,4 +5,4 @@ Demonstrates how to use the path_provider_windows plugin. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). diff --git a/packages/path_provider/path_provider_windows/example/integration_test/path_provider_test.dart b/packages/path_provider/path_provider_windows/example/integration_test/path_provider_test.dart index ee9427686026..a8285963adb6 100644 --- a/packages/path_provider/path_provider_windows/example/integration_test/path_provider_test.dart +++ b/packages/path_provider/path_provider_windows/example/integration_test/path_provider_test.dart @@ -1,43 +1,47 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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_windows/path_provider_windows.dart'; -import 'package:e2e/e2e.dart'; void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('getTemporaryDirectory', (WidgetTester tester) async { final PathProviderWindows provider = PathProviderWindows(); - final String result = await provider.getTemporaryPath(); + final String? result = await provider.getTemporaryPath(); _verifySampleFile(result, 'temporaryDirectory'); }); testWidgets('getApplicationDocumentsDirectory', (WidgetTester tester) async { final PathProviderWindows provider = PathProviderWindows(); - final String result = await provider.getApplicationDocumentsPath(); + final String? result = await provider.getApplicationDocumentsPath(); _verifySampleFile(result, 'applicationDocuments'); }); testWidgets('getApplicationSupportDirectory', (WidgetTester tester) async { final PathProviderWindows provider = PathProviderWindows(); - final String result = await provider.getApplicationSupportPath(); + final String? result = await provider.getApplicationSupportPath(); _verifySampleFile(result, 'applicationSupport'); }); testWidgets('getDownloadsDirectory', (WidgetTester tester) async { final PathProviderWindows provider = PathProviderWindows(); - final String result = await provider.getDownloadsPath(); + final String? result = await provider.getDownloadsPath(); _verifySampleFile(result, 'downloads'); }); } /// Verify a file called [name] in [directoryPath] by recreating it with test /// contents when necessary. -void _verifySampleFile(String directoryPath, String name) { +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'); diff --git a/packages/path_provider/path_provider_windows/example/lib/main.dart b/packages/path_provider/path_provider_windows/example/lib/main.dart index 4fbb1decf2f4..509292bf7405 100644 --- a/packages/path_provider/path_provider_windows/example/lib/main.dart +++ b/packages/path_provider/path_provider_windows/example/lib/main.dart @@ -1,15 +1,13 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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:path_provider_windows/path_provider_windows.dart'; -void main() async { +void main() { runApp(MyApp()); } @@ -20,10 +18,10 @@ class MyApp extends StatefulWidget { } class _MyAppState extends State { - String _tempDirectory = 'Unknown'; - String _downloadsDirectory = 'Unknown'; - String _appSupportDirectory = 'Unknown'; - String _documentsDirectory = 'Unknown'; + String? _tempDirectory = 'Unknown'; + String? _downloadsDirectory = 'Unknown'; + String? _appSupportDirectory = 'Unknown'; + String? _documentsDirectory = 'Unknown'; @override void initState() { @@ -33,10 +31,10 @@ class _MyAppState extends State { // Platform messages are asynchronous, so we initialize in an async method. Future initDirectories() async { - String tempDirectory; - String downloadsDirectory; - String appSupportDirectory; - String documentsDirectory; + String? tempDirectory; + String? downloadsDirectory; + String? appSupportDirectory; + String? documentsDirectory; final PathProviderWindows provider = PathProviderWindows(); try { @@ -79,7 +77,7 @@ class _MyAppState extends State { ), body: Center( child: Column( - children: [ + children: [ Text('Temp Directory: $_tempDirectory\n'), Text('Documents Directory: $_documentsDirectory\n'), Text('Downloads Directory: $_downloadsDirectory\n'), diff --git a/packages/path_provider/path_provider_windows/example/pubspec.yaml b/packages/path_provider/path_provider_windows/example/pubspec.yaml index 8dbe6e020906..26a796fca90c 100644 --- a/packages/path_provider/path_provider_windows/example/pubspec.yaml +++ b/packages/path_provider/path_provider_windows/example/pubspec.yaml @@ -1,21 +1,28 @@ name: path_provider_example description: Demonstrates how to use the path_provider plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: sdk: flutter - path_provider_windows: any - -dependency_overrides: path_provider_windows: + # When depending on this package from a real application you should use: + # path_provider_windows: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: - e2e: ^0.2.1 flutter_driver: sdk: flutter - test: any - pedantic: ^1.8.0 + integration_test: + sdk: flutter + pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/path_provider/path_provider_windows/example/test_driver/integration_test.dart b/packages/path_provider/path_provider_windows/example/test_driver/integration_test.dart index f3aa9e218d82..4f10f2a522f3 100644 --- a/packages/path_provider/path_provider_windows/example/test_driver/integration_test.dart +++ b/packages/path_provider/path_provider_windows/example/test_driver/integration_test.dart @@ -1,15 +1,7 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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_driver/flutter_driver.dart'; +import 'package:integration_test/integration_test_driver.dart'; -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String result = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - await driver.close(); - exit(result == 'pass' ? 0 : 1); -} +Future main() => integrationDriver(); diff --git a/packages/path_provider/path_provider_windows/example/windows/.gitignore b/packages/path_provider/path_provider_windows/example/windows/.gitignore new file mode 100644 index 000000000000..d492d0d98c8f --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider_windows/example/windows/CMakeLists.txt b/packages/path_provider/path_provider_windows/example/windows/CMakeLists.txt new file mode 100644 index 000000000000..abf90408efb4 --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/windows/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.15) +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/path_provider/path_provider_windows/example/windows/flutter/CMakeLists.txt b/packages/path_provider/path_provider_windows/example/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000000..744f08a9389b --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,102 @@ +cmake_minimum_required(VERSION 3.15) + +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" +) +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/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 new file mode 100644 index 000000000000..8b6d4680af38 --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// 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 new file mode 100644 index 000000000000..dc139d85a931 --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// 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 new file mode 100644 index 000000000000..4d10c2518654 --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,15 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_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) diff --git a/packages/path_provider/path_provider_windows/example/windows/runner/CMakeLists.txt b/packages/path_provider/path_provider_windows/example/windows/runner/CMakeLists.txt new file mode 100644 index 000000000000..977e38b5d1d2 --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/windows/runner/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.15) +project(runner LANGUAGES CXX) + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "run_loop.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/path_provider/path_provider_windows/example/windows/runner/Runner.rc b/packages/path_provider/path_provider_windows/example/windows/runner/Runner.rc new file mode 100644 index 000000000000..944329afc03a --- /dev/null +++ b/packages/path_provider/path_provider_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) 2020 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/path_provider/path_provider_windows/example/windows/runner/flutter_window.cpp b/packages/path_provider/path_provider_windows/example/windows/runner/flutter_window.cpp new file mode 100644 index 000000000000..8e415602cf3b --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/windows/runner/flutter_window.cpp @@ -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. + +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(RunLoop* run_loop, + const flutter::DartProject& project) + : run_loop_(run_loop), 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()); + run_loop_->RegisterFlutterInstance(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + run_loop_->UnregisterFlutterInstance(flutter_controller_->engine()); + 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 opporutunity 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/path_provider/path_provider_windows/example/windows/runner/flutter_window.h b/packages/path_provider/path_provider_windows/example/windows/runner/flutter_window.h new file mode 100644 index 000000000000..8e9c12bbe022 --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/windows/runner/flutter_window.h @@ -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. + +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "run_loop.h" +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow driven by the |run_loop|, hosting a + // Flutter view running |project|. + explicit FlutterWindow(RunLoop* run_loop, + 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 run loop driving events for this window. + RunLoop* run_loop_; + + // 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/path_provider/path_provider_windows/example/windows/runner/main.cpp b/packages/path_provider/path_provider_windows/example/windows/runner/main.cpp new file mode 100644 index 000000000000..126302b0be18 --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/windows/runner/main.cpp @@ -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. + +#include +#include +#include + +#include "flutter_window.h" +#include "run_loop.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); + + RunLoop run_loop; + + flutter::DartProject project(L"data"); + FlutterWindow window(&run_loop, project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + run_loop.Run(); + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/packages/path_provider/path_provider_windows/example/windows/runner/resource.h b/packages/path_provider/path_provider_windows/example/windows/runner/resource.h new file mode 100644 index 000000000000..d5d958dc4257 --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider_windows/example/windows/runner/resources/app_icon.ico b/packages/path_provider/path_provider_windows/example/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000000..c04e20caf637 Binary files /dev/null and b/packages/path_provider/path_provider_windows/example/windows/runner/resources/app_icon.ico differ diff --git a/packages/path_provider/path_provider_windows/example/windows/runner/run_loop.cpp b/packages/path_provider/path_provider_windows/example/windows/runner/run_loop.cpp new file mode 100644 index 000000000000..1916500e6440 --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/windows/runner/run_loop.cpp @@ -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. + +#include "run_loop.h" + +#include + +#include + +RunLoop::RunLoop() {} + +RunLoop::~RunLoop() {} + +void RunLoop::Run() { + bool keep_running = true; + TimePoint next_flutter_event_time = TimePoint::clock::now(); + while (keep_running) { + std::chrono::nanoseconds wait_duration = + std::max(std::chrono::nanoseconds(0), + next_flutter_event_time - TimePoint::clock::now()); + ::MsgWaitForMultipleObjects( + 0, nullptr, FALSE, static_cast(wait_duration.count() / 1000), + QS_ALLINPUT); + bool processed_events = false; + MSG message; + // All pending Windows messages must be processed; MsgWaitForMultipleObjects + // won't return again for items left in the queue after PeekMessage. + while (::PeekMessage(&message, nullptr, 0, 0, PM_REMOVE)) { + processed_events = true; + if (message.message == WM_QUIT) { + keep_running = false; + break; + } + ::TranslateMessage(&message); + ::DispatchMessage(&message); + // Allow Flutter to process messages each time a Windows message is + // processed, to prevent starvation. + next_flutter_event_time = + std::min(next_flutter_event_time, ProcessFlutterMessages()); + } + // If the PeekMessage loop didn't run, process Flutter messages. + if (!processed_events) { + next_flutter_event_time = + std::min(next_flutter_event_time, ProcessFlutterMessages()); + } + } +} + +void RunLoop::RegisterFlutterInstance( + flutter::FlutterEngine* flutter_instance) { + flutter_instances_.insert(flutter_instance); +} + +void RunLoop::UnregisterFlutterInstance( + flutter::FlutterEngine* flutter_instance) { + flutter_instances_.erase(flutter_instance); +} + +RunLoop::TimePoint RunLoop::ProcessFlutterMessages() { + TimePoint next_event_time = TimePoint::max(); + for (auto instance : flutter_instances_) { + std::chrono::nanoseconds wait_duration = instance->ProcessMessages(); + if (wait_duration != std::chrono::nanoseconds::max()) { + next_event_time = + std::min(next_event_time, TimePoint::clock::now() + wait_duration); + } + } + return next_event_time; +} diff --git a/packages/path_provider/path_provider_windows/example/windows/runner/run_loop.h b/packages/path_provider/path_provider_windows/example/windows/runner/run_loop.h new file mode 100644 index 000000000000..819ed3ed4995 --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/windows/runner/run_loop.h @@ -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. + +#ifndef RUNNER_RUN_LOOP_H_ +#define RUNNER_RUN_LOOP_H_ + +#include + +#include +#include + +// A runloop that will service events for Flutter instances as well +// as native messages. +class RunLoop { + public: + RunLoop(); + ~RunLoop(); + + // Prevent copying + RunLoop(RunLoop const&) = delete; + RunLoop& operator=(RunLoop const&) = delete; + + // Runs the run loop until the application quits. + void Run(); + + // Registers the given Flutter instance for event servicing. + void RegisterFlutterInstance(flutter::FlutterEngine* flutter_instance); + + // Unregisters the given Flutter instance from event servicing. + void UnregisterFlutterInstance(flutter::FlutterEngine* flutter_instance); + + private: + using TimePoint = std::chrono::steady_clock::time_point; + + // Processes all currently pending messages for registered Flutter instances. + TimePoint ProcessFlutterMessages(); + + std::set flutter_instances_; +}; + +#endif // RUNNER_RUN_LOOP_H_ diff --git a/packages/path_provider/path_provider_windows/example/windows/runner/runner.exe.manifest b/packages/path_provider/path_provider_windows/example/windows/runner/runner.exe.manifest new file mode 100644 index 000000000000..c977c4a42589 --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + 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 new file mode 100644 index 000000000000..537728149601 --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/windows/runner/utils.cpp @@ -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. + +#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(); + } +} diff --git a/packages/path_provider/path_provider_windows/example/windows/runner/utils.h b/packages/path_provider/path_provider_windows/example/windows/runner/utils.h new file mode 100644 index 000000000000..16b3f0794597 --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/windows/runner/utils.h @@ -0,0 +1,12 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/path_provider/path_provider_windows/example/windows/runner/win32_window.cpp b/packages/path_provider/path_provider_windows/example/windows/runner/win32_window.cpp new file mode 100644 index 000000000000..a609a2002bb3 --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider_windows/example/windows/runner/win32_window.h b/packages/path_provider/path_provider_windows/example/windows/runner/win32_window.h new file mode 100644 index 000000000000..d2a730052223 --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider_windows/ios/path_provider_windows.podspec b/packages/path_provider/path_provider_windows/ios/path_provider_windows.podspec deleted file mode 100644 index 941a36c1c794..000000000000 --- a/packages/path_provider/path_provider_windows/ios/path_provider_windows.podspec +++ /dev/null @@ -1,22 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# Run `pod lib lint path_provider_windows.podspec' to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'path_provider_windows' - s.version = '0.0.1' - s.summary = 'path_provider_windows iOS stub' - s.description = <<-DESC - No-op implementation of the windows path_provider plugin to avoid build issues on iOS - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_windows' } - s.dependency 'Flutter' - s.platform = :ios, '8.0' - - # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } - s.swift_version = '5.0' -end diff --git a/packages/path_provider/path_provider_windows/lib/path_provider_windows.dart b/packages/path_provider/path_provider_windows/lib/path_provider_windows.dart index b7aeb7a6d5f6..9af55ac2616c 100644 --- a/packages/path_provider/path_provider_windows/lib/path_provider_windows.dart +++ b/packages/path_provider/path_provider_windows/lib/path_provider_windows.dart @@ -1,4 +1,4 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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_windows/lib/src/folders.dart b/packages/path_provider/path_provider_windows/lib/src/folders.dart index fc2ea8351476..55def29df2d7 100644 --- a/packages/path_provider/path_provider_windows/lib/src/folders.dart +++ b/packages/path_provider/path_provider_windows/lib/src/folders.dart @@ -1,9 +1,12 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:win32/win32.dart'; +// ignore_for_file: non_constant_identifier_names + +// ignore: avoid_classes_with_only_static_members /// A class containing the GUID references for each of the documented Windows /// known folders. A property of this class may be passed to the `getPath` /// method in the [PathProvidersWindows] class to retrieve a known folder from diff --git a/packages/path_provider/path_provider_windows/lib/src/folders_stub.dart b/packages/path_provider/path_provider_windows/lib/src/folders_stub.dart index d19103602cdc..34e9e6118f7d 100644 --- a/packages/path_provider/path_provider_windows/lib/src/folders_stub.dart +++ b/packages/path_provider/path_provider_windows/lib/src/folders_stub.dart @@ -1,4 +1,4 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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_windows/lib/src/path_provider_windows_real.dart b/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_real.dart index 7ff448abf020..2b87d51c1c49 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 @@ -1,10 +1,9 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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:ffi'; +import 'dart:io'; import 'package:ffi/ffi.dart'; import 'package:meta/meta.dart'; @@ -22,24 +21,24 @@ import 'folders.dart'; class VersionInfoQuerier { /// Returns the value for [key] in [versionInfo]s English strings section, or /// null if there is no such entry, or if versionInfo is null. - getStringValue(Pointer versionInfo, key) { + String? getStringValue(Pointer? versionInfo, String key) { if (versionInfo == null) { return null; } - const kEnUsLanguageCode = '040904e4'; - final keyPath = TEXT('\\StringFileInfo\\$kEnUsLanguageCode\\$key'); - final length = allocate(); - final valueAddress = allocate(); + const String kEnUsLanguageCode = '040904e4'; + final Pointer keyPath = + TEXT('\\StringFileInfo\\$kEnUsLanguageCode\\$key'); + final Pointer length = calloc(); + final Pointer> valueAddress = calloc>(); try { if (VerQueryValue(versionInfo, keyPath, valueAddress, length) == 0) { return null; } - return Pointer.fromAddress(valueAddress.value) - .unpackString(length.value); + return valueAddress.value.toDartString(); } finally { - free(keyPath); - free(length); - free(valueAddress); + calloc.free(keyPath); + calloc.free(length); + calloc.free(valueAddress); } } } @@ -48,49 +47,58 @@ class VersionInfoQuerier { /// /// This class implements the `package:path_provider` functionality for Windows. class PathProviderWindows extends PathProviderPlatform { + /// Registers the Windows implementation. + static void registerWith() { + PathProviderPlatform.instance = PathProviderWindows(); + } + /// The object to use for performing VerQueryValue calls. @visibleForTesting VersionInfoQuerier versionInfoQuerier = VersionInfoQuerier(); /// This is typically the same as the TMP environment variable. @override - Future getTemporaryPath() async { - final buffer = allocate(count: MAX_PATH + 1).cast(); + Future getTemporaryPath() async { + final Pointer buffer = calloc(MAX_PATH + 1).cast(); String path; try { - final length = GetTempPath(MAX_PATH, buffer); + final int length = GetTempPath(MAX_PATH, buffer); if (length == 0) { - final error = GetLastError(); + final int error = GetLastError(); throw WindowsException(error); } else { - path = buffer.unpackString(length); + path = buffer.toDartString(); // GetTempPath adds a trailing backslash, but SHGetKnownFolderPath does // not. Strip off trailing backslash for consistency with other methods // here. - if (path.endsWith('\\')) { + if (path.endsWith(r'\')) { path = path.substring(0, path.length - 1); } } // Ensure that the directory exists, since GetTempPath doesn't. - final directory = Directory(path); + final Directory directory = Directory(path); if (!directory.existsSync()) { await directory.create(recursive: true); } - return Future.value(path); + return path; } finally { - free(buffer); + calloc.free(buffer); } } @override - Future getApplicationSupportPath() async { - final appDataRoot = await getPath(WindowsKnownFolder.RoamingAppData); - final directory = Directory( + Future getApplicationSupportPath() async { + final String? appDataRoot = + await getPath(WindowsKnownFolder.RoamingAppData); + if (appDataRoot == null) { + return null; + } + final Directory directory = Directory( path.join(appDataRoot, _getApplicationSpecificSubdirectory())); // Ensure that the directory exists if possible, since it will on other // platforms. If the name is longer than MAXPATH, creating will fail, so @@ -105,38 +113,40 @@ class PathProviderWindows extends PathProviderPlatform { } @override - Future getApplicationDocumentsPath() => + Future getApplicationDocumentsPath() => getPath(WindowsKnownFolder.Documents); @override - Future getDownloadsPath() => getPath(WindowsKnownFolder.Downloads); + Future getDownloadsPath() => getPath(WindowsKnownFolder.Downloads); /// Retrieve any known folder from Windows. /// /// folderID is a GUID that represents a specific known folder ID, drawn from /// [WindowsKnownFolder]. - Future getPath(String folderID) { - final pathPtrPtr = allocate(); - Pointer pathPtr; + Future getPath(String folderID) { + final Pointer> pathPtrPtr = calloc>(); + final Pointer knownFolderID = calloc()..ref.setGUID(folderID); try { - GUID knownFolderID = GUID.fromString(folderID); - - final hr = SHGetKnownFolderPath( - knownFolderID.addressOf, KF_FLAG_DEFAULT, NULL, pathPtrPtr); + final int hr = SHGetKnownFolderPath( + knownFolderID, + KF_FLAG_DEFAULT, + NULL, + pathPtrPtr, + ); if (FAILED(hr)) { if (hr == E_INVALIDARG || hr == E_FAIL) { throw WindowsException(hr); } + return Future.value(null); } - pathPtr = Pointer.fromAddress(pathPtrPtr.value); - final path = pathPtr.unpackString(MAX_PATH); - return Future.value(path); + final String path = pathPtrPtr.value.toDartString(); + return Future.value(path); } finally { - CoTaskMemFree(pathPtr.cast()); - free(pathPtrPtr); + calloc.free(pathPtrPtr); + calloc.free(knownFolderID); } } @@ -151,28 +161,29 @@ class PathProviderWindows extends PathProviderPlatform { /// - If the product name isn't there, it will use the exe's filename (without /// extension). String _getApplicationSpecificSubdirectory() { - String companyName; - String productName; + String? companyName; + String? productName; final Pointer moduleNameBuffer = - allocate(count: MAX_PATH + 1).cast(); - final Pointer unused = allocate(); - Pointer infoBuffer; + calloc(MAX_PATH + 1).cast(); + final Pointer unused = calloc(); + Pointer? infoBuffer; try { // Get the module name. - final moduleNameLength = GetModuleFileName(0, moduleNameBuffer, MAX_PATH); + final int moduleNameLength = + GetModuleFileName(0, moduleNameBuffer, MAX_PATH); if (moduleNameLength == 0) { - final error = GetLastError(); + final int error = GetLastError(); throw WindowsException(error); } // From that, load the VERSIONINFO resource - int infoSize = GetFileVersionInfoSize(moduleNameBuffer, unused); + final int infoSize = GetFileVersionInfoSize(moduleNameBuffer, unused); if (infoSize != 0) { - infoBuffer = allocate(count: infoSize); + infoBuffer = calloc(infoSize); if (GetFileVersionInfo(moduleNameBuffer, 0, infoSize, infoBuffer) == 0) { - free(infoBuffer); + calloc.free(infoBuffer); infoBuffer = null; } } @@ -182,19 +193,17 @@ class PathProviderWindows extends PathProviderPlatform { versionInfoQuerier.getStringValue(infoBuffer, 'ProductName')); // If there was no product name, use the executable name. - if (productName == null) { - productName = path.basenameWithoutExtension( - moduleNameBuffer.unpackString(moduleNameLength)); - } + productName ??= + path.basenameWithoutExtension(moduleNameBuffer.toDartString()); return companyName != null ? path.join(companyName, productName) : productName; } finally { - free(moduleNameBuffer); - free(unused); + calloc.free(moduleNameBuffer); + calloc.free(unused); if (infoBuffer != null) { - free(infoBuffer); + calloc.free(infoBuffer); } } } @@ -203,7 +212,7 @@ class PathProviderWindows extends PathProviderPlatform { /// https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions /// /// If after sanitizing the string is empty, returns null. - String _sanitizedDirectoryName(String rawString) { + String? _sanitizedDirectoryName(String? rawString) { if (rawString == null) { return null; } @@ -214,7 +223,7 @@ class PathProviderWindows extends PathProviderPlatform { .trimRight() // Ensure that it does not end with a '.'. .replaceAll(RegExp(r'[.]+$'), ''); - const kMaxComponentLength = 255; + const int kMaxComponentLength = 255; if (sanitized.length > kMaxComponentLength) { sanitized = sanitized.substring(0, kMaxComponentLength); } diff --git a/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_stub.dart b/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_stub.dart index 11a946542f26..bc851831bf54 100644 --- a/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_stub.dart +++ b/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_stub.dart @@ -1,4 +1,4 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -14,12 +14,15 @@ import 'package:path_provider_platform_interface/path_provider_platform_interfac class PathProviderWindows extends PathProviderPlatform { /// Errors on attempted instantiation of the stub. It exists only to satisfy /// compile-time dependencies, and should never actually be created. - PathProviderWindows() { - assert(false); + PathProviderWindows() : assert(false); + + /// Registers the Windows implementation. + static void registerWith() { + PathProviderPlatform.instance = PathProviderWindows(); } /// Stub; see comment on VersionInfoQuerier. - VersionInfoQuerier versionInfoQuerier; + VersionInfoQuerier versionInfoQuerier = VersionInfoQuerier(); /// Match PathProviderWindows so that the analyzer won't report invalid /// overrides if tests provide fake PathProviderWindows implementations. diff --git a/packages/path_provider/path_provider_windows/pubspec.yaml b/packages/path_provider/path_provider_windows/pubspec.yaml index a04366cf0962..e00e6d1373f2 100644 --- a/packages/path_provider/path_provider_windows/pubspec.yaml +++ b/packages/path_provider/path_provider_windows/pubspec.yaml @@ -1,29 +1,31 @@ name: path_provider_windows description: Windows implementation of the path_provider plugin -homepage: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_windows -version: 0.0.4+1 +repository: https://github.com/flutter/plugins/tree/master/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.2 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: + implements: path_provider platforms: windows: dartPluginClass: PathProviderWindows pluginClass: none dependencies: - path_provider_platform_interface: ^1.0.3 - meta: ^1.0.5 - path: ^1.6.4 + ffi: ^1.0.0 flutter: sdk: flutter - ffi: ^0.1.3 - win32: ^1.7.1 + meta: ^1.3.0 + path: ^1.8.0 + path_provider_platform_interface: ^2.0.0 + win32: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4 <2.0.0" + 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 83ceea9cdf0c..e977e07d99e6 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 @@ -1,10 +1,11 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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 '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'; // A fake VersionInfoQuerier that just returns preset responses. @@ -13,20 +14,26 @@ class FakeVersionInfoQuerier implements VersionInfoQuerier { final Map responses; - getStringValue(Pointer versionInfo, key) => responses[key]; + String? getStringValue(Pointer? versionInfo, String key) => + responses[key]; } void main() { + test('registered instance', () { + PathProviderWindows.registerWith(); + expect(PathProviderPlatform.instance, isA()); + }); + test('getTemporaryPath', () async { - final pathProvider = PathProviderWindows(); + final PathProviderWindows pathProvider = PathProviderWindows(); expect(await pathProvider.getTemporaryPath(), contains(r'C:\')); }, skip: !Platform.isWindows); test('getApplicationSupportPath with no version info', () async { - final pathProvider = PathProviderWindows(); + final PathProviderWindows pathProvider = PathProviderWindows(); pathProvider.versionInfoQuerier = FakeVersionInfoQuerier({}); - final path = await pathProvider.getApplicationSupportPath(); + 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. @@ -34,75 +41,87 @@ void main() { }, skip: !Platform.isWindows); test('getApplicationSupportPath with full version info', () async { - final pathProvider = PathProviderWindows(); + final PathProviderWindows pathProvider = PathProviderWindows(); pathProvider.versionInfoQuerier = FakeVersionInfoQuerier({ 'CompanyName': 'A Company', 'ProductName': 'Amazing App', }); - final path = await pathProvider.getApplicationSupportPath(); - expect(path, endsWith(r'AppData\Roaming\A Company\Amazing App')); - expect(Directory(path).existsSync(), isTrue); + 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 missing company', () async { - final pathProvider = PathProviderWindows(); + final PathProviderWindows pathProvider = PathProviderWindows(); pathProvider.versionInfoQuerier = FakeVersionInfoQuerier({ 'ProductName': 'Amazing App', }); - final path = await pathProvider.getApplicationSupportPath(); - expect(path, endsWith(r'AppData\Roaming\Amazing App')); - expect(Directory(path).existsSync(), isTrue); + final String? path = await pathProvider.getApplicationSupportPath(); + expect(path, isNotNull); + if (path != null) { + expect(path, endsWith(r'AppData\Roaming\Amazing App')); + expect(Directory(path).existsSync(), isTrue); + } }, skip: !Platform.isWindows); test('getApplicationSupportPath with problematic values', () async { - final pathProvider = PathProviderWindows(); + final PathProviderWindows pathProvider = PathProviderWindows(); pathProvider.versionInfoQuerier = FakeVersionInfoQuerier({ 'CompanyName': r'A Company: Name.', 'ProductName': r'A"/Terrible\|App?*Name', }); - final path = await pathProvider.getApplicationSupportPath(); - expect( - path, - endsWith(r'AppData\Roaming\' - r'A _Bad_ Company_ Name\' - r'A__Terrible__App__Name')); - expect(Directory(path).existsSync(), isTrue); + final String? path = await pathProvider.getApplicationSupportPath(); + expect(path, isNotNull); + if (path != null) { + expect( + path, + endsWith(r'AppData\Roaming\' + r'A _Bad_ Company_ Name\' + r'A__Terrible__App__Name')); + expect(Directory(path).existsSync(), isTrue); + } }, skip: !Platform.isWindows); test('getApplicationSupportPath with a completely invalid company', () async { - final pathProvider = PathProviderWindows(); + final PathProviderWindows pathProvider = PathProviderWindows(); pathProvider.versionInfoQuerier = FakeVersionInfoQuerier({ 'CompanyName': r'..', 'ProductName': r'Amazing App', }); - final path = await pathProvider.getApplicationSupportPath(); - expect(path, endsWith(r'AppData\Roaming\Amazing App')); - expect(Directory(path).existsSync(), isTrue); + final String? path = await pathProvider.getApplicationSupportPath(); + expect(path, isNotNull); + if (path != null) { + expect(path, endsWith(r'AppData\Roaming\Amazing App')); + expect(Directory(path).existsSync(), isTrue); + } }, skip: !Platform.isWindows); test('getApplicationSupportPath with very long app name', () async { - final pathProvider = PathProviderWindows(); - final truncatedName = 'A' * 255; + final PathProviderWindows pathProvider = PathProviderWindows(); + final String truncatedName = 'A' * 255; pathProvider.versionInfoQuerier = FakeVersionInfoQuerier({ 'CompanyName': 'A Company', 'ProductName': truncatedName * 2, }); - final path = await pathProvider.getApplicationSupportPath(); + final String? path = await pathProvider.getApplicationSupportPath(); expect(path, endsWith('\\$truncatedName')); // The directory won't exist, since it's longer than MAXPATH, so don't check // that here. }, skip: !Platform.isWindows); test('getApplicationDocumentsPath', () async { - final pathProvider = PathProviderWindows(); - final path = await pathProvider.getApplicationDocumentsPath(); + final PathProviderWindows pathProvider = PathProviderWindows(); + final String? path = await pathProvider.getApplicationDocumentsPath(); expect(path, contains(r'C:\')); expect(path, contains(r'Documents')); }, skip: !Platform.isWindows); test('getDownloadsPath', () async { - final pathProvider = PathProviderWindows(); - final path = await pathProvider.getDownloadsPath(); + final PathProviderWindows pathProvider = PathProviderWindows(); + final String? path = await pathProvider.getDownloadsPath(); expect(path, contains(r'C:\')); expect(path, contains(r'Downloads')); }, skip: !Platform.isWindows); diff --git a/packages/plugin_platform_interface/AUTHORS b/packages/plugin_platform_interface/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/plugin_platform_interface/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/plugin_platform_interface/CHANGELOG.md b/packages/plugin_platform_interface/CHANGELOG.md index 01b5ff7d1252..cea8f2a76266 100644 --- a/packages/plugin_platform_interface/CHANGELOG.md +++ b/packages/plugin_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.0.0 + +* Migrate to null safety. + ## 1.0.3 * Fix homepage in `pubspec.yaml`. diff --git a/packages/plugin_platform_interface/LICENSE b/packages/plugin_platform_interface/LICENSE index 507569823f1b..c6823b81eb84 100644 --- a/packages/plugin_platform_interface/LICENSE +++ b/packages/plugin_platform_interface/LICENSE @@ -1,4 +1,4 @@ -Copyright 2019 The Chromium Authors. All rights reserved. +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: diff --git a/packages/plugin_platform_interface/analysis_options.yaml b/packages/plugin_platform_interface/analysis_options.yaml new file mode 100644 index 000000000000..cda4f6e153e6 --- /dev/null +++ b/packages/plugin_platform_interface/analysis_options.yaml @@ -0,0 +1 @@ +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 be4871928686..d9bd88168422 100644 --- a/packages/plugin_platform_interface/lib/plugin_platform_interface.dart +++ b/packages/plugin_platform_interface/lib/plugin_platform_interface.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -41,9 +41,9 @@ import 'package:meta/meta.dart'; /// [MockPlatformInterfaceMixin] for a sample of using Mockito to mock a platform interface. abstract class PlatformInterface { /// Pass a private, class-specific `const Object()` as the `token`. - PlatformInterface({@required Object token}) : _instanceToken = token; + PlatformInterface({required Object token}) : _instanceToken = token; - final Object _instanceToken; + final Object? _instanceToken; /// Ensures that the platform instance has a token that matches the /// provided token and throws [AssertionError] if not. diff --git a/packages/plugin_platform_interface/pubspec.yaml b/packages/plugin_platform_interface/pubspec.yaml index ae11b144ca8c..a59a81481fe2 100644 --- a/packages/plugin_platform_interface/pubspec.yaml +++ b/packages/plugin_platform_interface/pubspec.yaml @@ -1,5 +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 +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 # DO NOT INCREASE THE MAJOR VERSION OF THIS PACKAGE @@ -12,17 +14,15 @@ description: Reusable base class for Flutter plugin platform interfaces. # be done when absolutely necessary and after the ecosystem has already migrated to 1.X.Y version # that is forward compatible with 2.0.0 (ideally the ecosystem have migrated to depend on: # `plugin_platform_interface: >=1.X.Y <3.0.0`). -version: 1.0.3 - -repository: https://github.com/flutter/plugins/tree/master/packages/plugin_platform_interface +version: 2.0.0 environment: - sdk: ">=2.1.0 <3.0.0" + sdk: ">=2.12.0 <3.0.0" dependencies: - meta: ^1.0.0 + meta: ^1.3.0 dev_dependencies: - mockito: ^4.1.1 - test: ^1.9.4 - pedantic: ^1.8.0 + mockito: ^5.0.0 + test: ^1.16.0 + pedantic: ^1.10.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 0488c20f3efb..0079765f0b09 100644 --- a/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart +++ b/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/quick_actions/CHANGELOG.md b/packages/quick_actions/CHANGELOG.md deleted file mode 100644 index 49815fc68b29..000000000000 --- a/packages/quick_actions/CHANGELOG.md +++ /dev/null @@ -1,140 +0,0 @@ -## 0.4.0+9 - -* Keep handling deprecated Android v1 classes for backward compatibility. - -## 0.4.0+8 - -* Update package:e2e -> package:integration_test - -## 0.4.0+7 - -* Update package:e2e reference to use the local version in the flutter/plugins - repository. - -## 0.4.0+6 - -* Post-v2 Android embedding cleanup. - -## 0.4.0+5 - -* Update lower bound of dart dependency to 2.1.0. - -## 0.4.0+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 UIApplicationShortcutItem availability warnings. -* Fix CocoaPods podspec lint warnings. - -## 0.4.0+3 - -* Replace deprecated `getFlutterEngine` call on Android. - -## 0.4.0+2 - -* Make the pedantic dev_dependency explicit. - -## 0.4.0+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.0 - -- Added missing documentation. -- **Breaking change**. `channel` and `withMethodChannel` are now - `@visibleForTesting`. These methods are for plugin unit tests only and may be - removed in the future. -- **Breaking change**. Removed `runLaunchAction` from public API. This method - was not meant to be used by consumers of the plugin. - -## 0.3.3+1 - -* Update and migrate iOS example project by removing flutter_assets, change - "English" to "en", remove extraneous xcconfigs, update to Xcode 11 build - settings, and remove ARCHS and DEVELOPMENT_TEAM. - -## 0.3.3 - -* Support Android V2 embedding. -* Add e2e tests. -* Migrate to using the new e2e test binding. - -## 0.3.2+4 - -* Remove AndroidX warnings. - -## 0.3.2+3 - -* Define clang module for iOS. - -## 0.3.2+2 - -* Fix bug that would make the shortcut not open on Android. -* Report shortcut used on Android. -* Improves example. - -## 0.3.2+1 - -* Update usage example in README. - -## 0.3.2 - -* Fixed the quick actions launch on Android when the app is killed. - -## 0.3.1 - -* Added unit tests. - -## 0.3.0+2 - -* Add missing template type parameter to `invokeMethod` calls. -* Bump minimum Flutter version to 1.5.0. -* Replace invokeMethod with invokeMapMethod wherever necessary. - -## 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.2 - -* Allow to register more than once. - -## 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.2 - -* Add FLT prefix to iOS types - -## 0.0.1 - -* Initial release diff --git a/packages/quick_actions/LICENSE b/packages/quick_actions/LICENSE deleted file mode 100644 index a6d6c0749818..000000000000 --- a/packages/quick_actions/LICENSE +++ /dev/null @@ -1,25 +0,0 @@ -Copyright 2017 The Chromium Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided - with the distribution. - * Neither the name of Google Inc. nor the names of its - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/quick_actions/README.md b/packages/quick_actions/README.md deleted file mode 100644 index 21e7cfb619cb..000000000000 --- a/packages/quick_actions/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# quick_actions - -This Flutter plugin allows you to manage and interact with the application's -home screen quick actions. - -Quick actions refer to the [eponymous -concept](https://developer.apple.com/ios/human-interface-guidelines/extensions/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. - -## Usage in Dart - -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 -quick action. - -```dart -final QuickActions quickActions = const QuickActions(); -quickActions.initialize((shortcutType) { - if (shortcutType == 'action_main') { - print('The user tapped on the "Main view" action.'); - } - // More handling code... -}); -``` - -Finally, manage the app's quick actions, for instance: - -```dart -quickActions.setShortcutItems([ - const ShortcutItem(type: 'action_main', localizedTitle: 'Main view', icon: 'icon_main'), - const ShortcutItem(type: 'action_help', localizedTitle: 'Help', icon: 'icon_help') -]); -``` - -Please note, that the `type` argument should be unique within your application -(among all the registered shortcut items). The optional `icon` should be the -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](http://flutter.io/). - -For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code). diff --git a/packages/quick_actions/analysis_options.yaml b/packages/quick_actions/analysis_options.yaml new file mode 100644 index 000000000000..cda4f6e153e6 --- /dev/null +++ b/packages/quick_actions/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options_legacy.yaml diff --git a/packages/quick_actions/android/build.gradle b/packages/quick_actions/android/build.gradle deleted file mode 100644 index 648b654dbfcd..000000000000 --- a/packages/quick_actions/android/build.gradle +++ /dev/null @@ -1,34 +0,0 @@ -group 'io.flutter.plugins.quickactions' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' - } -} - -rootProject.allprojects { - repositories { - google() - jcenter() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 28 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - } -} diff --git a/packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java b/packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java deleted file mode 100644 index dcf2390570bd..000000000000 --- a/packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java +++ /dev/null @@ -1,131 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.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 { - - private static final String CHANNEL_ID = "plugins.flutter.io/quick_actions"; - private static final String EXTRA_ACTION = "some unique action key"; - - 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/example/README.md b/packages/quick_actions/example/README.md deleted file mode 100644 index 86cefd0d24f1..000000000000 --- a/packages/quick_actions/example/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# quick_actions_example - -Demonstrates how to use the quick_actions plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](http://flutter.io/). diff --git a/packages/quick_actions/example/android.iml b/packages/quick_actions/example/android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/quick_actions/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/quick_actions/example/android/app/build.gradle b/packages/quick_actions/example/android/app/build.gradle deleted file mode 100644 index b8ca2db446f8..000000000000 --- a/packages/quick_actions/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 28 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - applicationId "io.flutter.plugins.quickactionsexample" - 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/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1Activity.java b/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1Activity.java deleted file mode 100644 index 4a77793da48a..000000000000 --- a/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1Activity.java +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.quickactionsexample; - -import android.os.Bundle; -import io.flutter.plugins.quickactions.QuickActionsPlugin; - -@SuppressWarnings("deprecation") -public class EmbeddingV1Activity extends io.flutter.app.FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - QuickActionsPlugin.registerWith( - registrarFor("io.flutter.plugins.quickactions.QuickActionsPlugin")); - } -} diff --git a/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1ActivityTest.java b/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1ActivityTest.java deleted file mode 100644 index e9e9401128a4..000000000000 --- a/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.quickactionsexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -@SuppressWarnings("deprecation") -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/FlutterActivityTest.java b/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/FlutterActivityTest.java deleted file mode 100644 index 9ce1e8dfe4ea..000000000000 --- a/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/FlutterActivityTest.java +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.quickactionsexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import io.flutter.embedding.android.FlutterActivity; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -public class FlutterActivityTest { - @Rule - public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); -} diff --git a/packages/quick_actions/example/android/build.gradle b/packages/quick_actions/example/android/build.gradle deleted file mode 100644 index 541636cc492a..000000000000 --- a/packages/quick_actions/example/android/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' - } -} - -allprojects { - repositories { - google() - jcenter() - } -} - -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/quick_actions/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/quick_actions/example/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 019065d1d650..000000000000 --- a/packages/quick_actions/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/quick_actions/example/android/settings.gradle b/packages/quick_actions/example/android/settings.gradle deleted file mode 100644 index 115da6cb4f4d..000000000000 --- a/packages/quick_actions/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/quick_actions/example/integration_test/quick_actions_test.dart b/packages/quick_actions/example/integration_test/quick_actions_test.dart deleted file mode 100644 index 03ecfe491d93..000000000000 --- a/packages/quick_actions/example/integration_test/quick_actions_test.dart +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'package:flutter_test/flutter_test.dart'; -import 'package:integration_test/integration_test.dart'; -import 'package:quick_actions/quick_actions.dart'; - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('Can set shortcuts', (WidgetTester tester) async { - final QuickActions quickActions = QuickActions(); - quickActions.initialize(null); - - const ShortcutItem shortCutItem = ShortcutItem( - type: 'action_one', - localizedTitle: 'Action one', - icon: 'AppIcon', - ); - expect( - quickActions.setShortcutItems([shortCutItem]), completes); - }); -} diff --git a/packages/quick_actions/example/ios/Flutter/AppFrameworkInfo.plist b/packages/quick_actions/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6c2de8086bcd..000000000000 --- a/packages/quick_actions/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/quick_actions/example/ios/Flutter/Debug.xcconfig b/packages/quick_actions/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index e8efba114687..000000000000 --- a/packages/quick_actions/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/quick_actions/example/ios/Flutter/Release.xcconfig b/packages/quick_actions/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index 399e9340e6f6..000000000000 --- a/packages/quick_actions/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/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj b/packages/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index fdd275fcede5..000000000000 --- a/packages/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,490 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 83C36CAF23D629E5ABE75B2A /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CCC799F2B0AB50A9C34344F0 /* libPods-Runner.a */; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; - 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 = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - CCC799F2B0AB50A9C34344F0 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.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 */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - 83C36CAF23D629E5ABE75B2A /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B80C3931E831B6300D905FE /* App.framework */, - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - D0FE95BE2380323DD75CB891 /* Pods */, - A44AD0D63DEF785A2A2DEE28 /* 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 = ""; - }; - A44AD0D63DEF785A2A2DEE28 /* Frameworks */ = { - isa = PBXGroup; - children = ( - CCC799F2B0AB50A9C34344F0 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; - D0FE95BE2380323DD75CB891 /* Pods */ = { - isa = PBXGroup; - children = ( - 5278439583922091276A37C9 /* Pods-Runner.debug.xcconfig */, - F0609304FBCAEC2289164BD5 /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 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 */, - FEDDF02AA7C2BA0D1905BD95 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; - 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; - }; - FEDDF02AA7C2BA0D1905BD95 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.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 = io.flutter.plugins.quickActionsExample; - 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/quick_actions/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/quick_actions/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a16ed0f..000000000000 --- a/packages/quick_actions/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/packages/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 3bb3697ef41c..000000000000 --- a/packages/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/quick_actions/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/quick_actions/example/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14c74e..000000000000 --- a/packages/quick_actions/example/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/packages/quick_actions/example/ios/Runner/AppDelegate.h b/packages/quick_actions/example/ios/Runner/AppDelegate.h deleted file mode 100644 index d9e18e990f2e..000000000000 --- a/packages/quick_actions/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/quick_actions/example/ios/Runner/AppDelegate.m b/packages/quick_actions/example/ios/Runner/AppDelegate.m deleted file mode 100644 index f08675707182..000000000000 --- a/packages/quick_actions/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d22f10b2ab63..000000000000 --- a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf03016f..000000000000 Binary files a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca8..000000000000 Binary files a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cde12118dda..000000000000 Binary files a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb8..000000000000 Binary files a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index dcdc2306c285..000000000000 Binary files a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee..000000000000 Binary files a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a9..000000000000 Binary files a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7..000000000000 Binary files a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 6a84f41e14e2..000000000000 Binary files a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index d0e1f5853602..000000000000 Binary files a/packages/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/packages/quick_actions/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/quick_actions/example/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index ebf48f603974..000000000000 --- a/packages/quick_actions/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/quick_actions/example/ios/Runner/Base.lproj/Main.storyboard b/packages/quick_actions/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516fb38..000000000000 --- a/packages/quick_actions/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/quick_actions/example/ios/Runner/main.m b/packages/quick_actions/example/ios/Runner/main.m deleted file mode 100644 index bec320c0bee0..000000000000 --- a/packages/quick_actions/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/quick_actions/example/lib/main.dart b/packages/quick_actions/example/lib/main.dart deleted file mode 100644 index fc289810ea24..000000000000 --- a/packages/quick_actions/example/lib/main.dart +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -// ignore_for_file: public_member_api_docs - -import 'package:flutter/material.dart'; -import 'package:quick_actions/quick_actions.dart'; - -void main() { - runApp(MyApp()); -} - -class MyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Quick Actions Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: MyHomePage(), - ); - } -} - -class MyHomePage extends StatefulWidget { - MyHomePage({Key key}) : super(key: key); - - @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - String shortcut = "no action set"; - - @override - void initState() { - super.initState(); - - final QuickActions quickActions = QuickActions(); - quickActions.initialize((String shortcutType) { - setState(() { - if (shortcutType != null) shortcut = shortcutType; - }); - }); - - quickActions.setShortcutItems([ - // NOTE: This first action icon will only work on iOS. - // In a real world project keep the same file name for both platforms. - const ShortcutItem( - type: 'action_one', - localizedTitle: 'Action one', - icon: 'AppIcon', - ), - // NOTE: This second action icon will only work on Android. - // In a real world project keep the same file name for both platforms. - const ShortcutItem( - type: 'action_two', - localizedTitle: 'Action two', - icon: 'ic_launcher'), - ]); - } - - @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/example/pubspec.yaml b/packages/quick_actions/example/pubspec.yaml deleted file mode 100644 index 6f9c0efd01af..000000000000 --- a/packages/quick_actions/example/pubspec.yaml +++ /dev/null @@ -1,22 +0,0 @@ -name: quick_actions_example -description: Demonstrates how to use the quick_actions plugin. - -dependencies: - flutter: - sdk: flutter - quick_actions: - path: ../ - -dev_dependencies: - flutter_driver: - sdk: flutter - integration_test: - path: ../../integration_test - pedantic: ^1.8.0 - -flutter: - uses-material-design: true - -environment: - sdk: ">=2.0.0-dev.28.0 <3.0.0" - flutter: ">=1.9.1+hotfix.2 <2.0.0" diff --git a/packages/quick_actions/example/quick_actions_example.iml b/packages/quick_actions/example/quick_actions_example.iml deleted file mode 100644 index 9d5dae19540c..000000000000 --- a/packages/quick_actions/example/quick_actions_example.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/quick_actions/example/quick_actions_example_android.iml b/packages/quick_actions/example/quick_actions_example_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/quick_actions/example/quick_actions_example_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/quick_actions/example/test_driver/integration_test.dart b/packages/quick_actions/example/test_driver/integration_test.dart deleted file mode 100644 index 7a2c21338786..000000000000 --- a/packages/quick_actions/example/test_driver/integration_test.dart +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'package:flutter_driver/flutter_driver.dart'; - -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String data = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - await driver.close(); - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} diff --git a/packages/quick_actions/ios/Assets/.gitkeep b/packages/quick_actions/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/quick_actions/ios/Classes/FLTQuickActionsPlugin.m b/packages/quick_actions/ios/Classes/FLTQuickActionsPlugin.m deleted file mode 100644 index 88ff7397af8a..000000000000 --- a/packages/quick_actions/ios/Classes/FLTQuickActionsPlugin.m +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import "FLTQuickActionsPlugin.h" - -static NSString *const CHANNEL_NAME = @"plugins.flutter.io/quick_actions"; - -@interface FLTQuickActionsPlugin () -@property(nonatomic, retain) FlutterMethodChannel *channel; -@end - -@implementation FLTQuickActionsPlugin - -+ (void)registerWithRegistrar:(NSObject *)registrar { - FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:CHANNEL_NAME - binaryMessenger:[registrar messenger]]; - FLTQuickActionsPlugin *instance = [[FLTQuickActionsPlugin alloc] init]; - instance.channel = channel; - [registrar addMethodCallDelegate:instance channel:channel]; -} - -- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if (@available(iOS 9.0, *)) { - if ([call.method isEqualToString:@"setShortcutItems"]) { - _setShortcutItems(call.arguments); - result(nil); - } else if ([call.method isEqualToString:@"clearShortcutItems"]) { - [UIApplication sharedApplication].shortcutItems = @[]; - result(nil); - } else if ([call.method isEqualToString:@"getLaunchAction"]) { - result(nil); - } else { - result(FlutterMethodNotImplemented); - } - } else { - NSLog(@"Shortcuts are not supported prior to iOS 9."); - result(nil); - } -} - -- (void)dealloc { - [_channel setMethodCallHandler:nil]; - _channel = nil; -} - -- (BOOL)application:(UIApplication *)application - performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem - completionHandler:(void (^)(BOOL succeeded))completionHandler - API_AVAILABLE(ios(9.0)) { - [self.channel invokeMethod:@"launch" arguments:shortcutItem.type]; - return YES; -} - -#pragma mark Private functions - -NS_INLINE void _setShortcutItems(NSArray *items) API_AVAILABLE(ios(9.0)) { - NSMutableArray *newShortcuts = [[NSMutableArray alloc] init]; - - for (id item in items) { - UIApplicationShortcutItem *shortcut = _deserializeShortcutItem(item); - [newShortcuts addObject:shortcut]; - } - - [UIApplication sharedApplication].shortcutItems = newShortcuts; -} - -NS_INLINE UIApplicationShortcutItem *_deserializeShortcutItem(NSDictionary *serialized) - API_AVAILABLE(ios(9.0)) { - UIApplicationShortcutIcon *icon = - [serialized[@"icon"] isKindOfClass:[NSNull class]] - ? nil - : [UIApplicationShortcutIcon iconWithTemplateImageName:serialized[@"icon"]]; - - return [[UIApplicationShortcutItem alloc] initWithType:serialized[@"type"] - localizedTitle:serialized[@"localizedTitle"] - localizedSubtitle:nil - icon:icon - userInfo:nil]; -} - -@end diff --git a/packages/quick_actions/lib/quick_actions.dart b/packages/quick_actions/lib/quick_actions.dart deleted file mode 100644 index 933162a1a47c..000000000000 --- a/packages/quick_actions/lib/quick_actions.dart +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:async'; - -import 'package:flutter/services.dart'; -import 'package:meta/meta.dart'; - -const MethodChannel _kChannel = - MethodChannel('plugins.flutter.io/quick_actions'); - -/// Handler for a quick action launch event. -/// -/// The argument [type] corresponds to the [ShortcutItem]'s field. -typedef void QuickActionHandler(String type); - -/// Home screen quick-action shortcut item. -class ShortcutItem { - /// Constructs an instance with the given [type], [localizedTitle], and - /// [icon]. - /// - /// Only [icon] should be nullable. It will remain `null` if unset. - const ShortcutItem({ - @required this.type, - @required this.localizedTitle, - this.icon, - }); - - /// The identifier of this item; should be unique within the app. - final String type; - - /// Localized title of the item. - final String localizedTitle; - - /// Name of native resource (xcassets etc; NOT a Flutter asset) to be - /// displayed as the icon for this item. - final String icon; -} - -/// Quick actions plugin. -class QuickActions { - /// Gets an instance of the plugin with the default methodChannel. - /// - /// [initialize] should be called before using any other methods. - factory QuickActions() => _instance; - - /// This is a test-only constructor. Do not call this, it can break at any - /// time. - @visibleForTesting - QuickActions.withMethodChannel(this.channel); - - static final QuickActions _instance = - QuickActions.withMethodChannel(_kChannel); - - /// This is a test-only accessor. Do not call this, it can break at any time. - @visibleForTesting - final MethodChannel channel; - - /// Initializes this plugin. - /// - /// Call this once before any further interaction with the the plugin. - void initialize(QuickActionHandler handler) async { - channel.setMethodCallHandler((MethodCall call) async { - assert(call.method == 'launch'); - handler(call.arguments); - }); - final String action = await channel.invokeMethod('getLaunchAction'); - if (action != null) { - handler(action); - } - } - - /// Sets the [ShortcutItem]s to become the app's quick actions. - Future setShortcutItems(List items) async { - final List> itemsList = - items.map(_serializeItem).toList(); - await channel.invokeMethod('setShortcutItems', itemsList); - } - - /// Removes all [ShortcutItem]s registered for the app. - 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/pubspec.yaml b/packages/quick_actions/pubspec.yaml deleted file mode 100644 index f612ecdafa63..000000000000 --- a/packages/quick_actions/pubspec.yaml +++ /dev/null @@ -1,32 +0,0 @@ -name: quick_actions -description: Flutter plugin for creating shortcuts on home screen, also known as - Quick Actions on iOS and App Shortcuts on Android. -homepage: https://github.com/flutter/plugins/tree/master/packages/quick_actions -version: 0.4.0+9 - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.quickactions - pluginClass: QuickActionsPlugin - ios: - pluginClass: FLTQuickActionsPlugin - -dependencies: - flutter: - sdk: flutter - meta: ^1.0.5 - -dev_dependencies: - test: ^1.3.0 - mockito: ^3.0.0 - flutter_test: - sdk: flutter - integration_test: - path: ../integration_test - pedantic: ^1.8.0 - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" diff --git a/packages/quick_actions/quick_actions/CHANGELOG.md b/packages/quick_actions/quick_actions/CHANGELOG.md new file mode 100644 index 000000000000..179496476c42 --- /dev/null +++ b/packages/quick_actions/quick_actions/CHANGELOG.md @@ -0,0 +1,173 @@ +## 0.6.0+2 + +* Migrate maven repository from jcenter to mavenCentral. + +## 0.6.0+1 + +* Correctly handle iOS Application lifecycle events on cold start of the App. + +## 0.6.0 + +* Migrate to federated architecture. + +## 0.5.0+1 + +* Updated example app implementation. + +## 0.5.0 + +* Migrate to null safety. +* Fixes quick actions not working on iOS. + +## 0.4.0+12 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 0.4.0+11 + +* Update Flutter SDK constraint. + +## 0.4.0+10 + +* Update android compileSdkVersion to 29. + +## 0.4.0+9 + +* Keep handling deprecated Android v1 classes for backward compatibility. + +## 0.4.0+8 + +* Update package:e2e -> package:integration_test + +## 0.4.0+7 + +* Update package:e2e reference to use the local version in the flutter/plugins + repository. + +## 0.4.0+6 + +* Post-v2 Android embedding cleanup. + +## 0.4.0+5 + +* Update lower bound of dart dependency to 2.1.0. + +## 0.4.0+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 UIApplicationShortcutItem availability warnings. +* Fix CocoaPods podspec lint warnings. + +## 0.4.0+3 + +* Replace deprecated `getFlutterEngine` call on Android. + +## 0.4.0+2 + +* Make the pedantic dev_dependency explicit. + +## 0.4.0+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.0 + +- Added missing documentation. +- **Breaking change**. `channel` and `withMethodChannel` are now + `@visibleForTesting`. These methods are for plugin unit tests only and may be + removed in the future. +- **Breaking change**. Removed `runLaunchAction` from public API. This method + was not meant to be used by consumers of the plugin. + +## 0.3.3+1 + +* Update and migrate iOS example project by removing flutter_assets, change + "English" to "en", remove extraneous xcconfigs, update to Xcode 11 build + settings, and remove ARCHS and DEVELOPMENT_TEAM. + +## 0.3.3 + +* Support Android V2 embedding. +* Add e2e tests. +* Migrate to using the new e2e test binding. + +## 0.3.2+4 + +* Remove AndroidX warnings. + +## 0.3.2+3 + +* Define clang module for iOS. + +## 0.3.2+2 + +* Fix bug that would make the shortcut not open on Android. +* Report shortcut used on Android. +* Improves example. + +## 0.3.2+1 + +* Update usage example in README. + +## 0.3.2 + +* Fixed the quick actions launch on Android when the app is killed. + +## 0.3.1 + +* Added unit tests. + +## 0.3.0+2 + +* Add missing template type parameter to `invokeMethod` calls. +* Bump minimum Flutter version to 1.5.0. +* Replace invokeMethod with invokeMapMethod wherever necessary. + +## 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.2 + +* Allow to register more than once. + +## 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.2 + +* Add FLT prefix to iOS types + +## 0.0.1 + +* Initial release diff --git a/packages/quick_actions/quick_actions/LICENSE b/packages/quick_actions/quick_actions/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/quick_actions/quick_actions/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/README.md b/packages/quick_actions/quick_actions/README.md new file mode 100644 index 000000000000..46e87fa0b241 --- /dev/null +++ b/packages/quick_actions/quick_actions/README.md @@ -0,0 +1,48 @@ +# quick_actions + +This Flutter plugin allows you to manage and interact with the application's +home screen quick actions. + +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. + +## Usage in Dart + +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 +quick action. + +```dart +final QuickActions quickActions = const QuickActions(); +quickActions.initialize((shortcutType) { + if (shortcutType == 'action_main') { + print('The user tapped on the "Main view" action.'); + } + // More handling code... +}); +``` + +Finally, manage the app's quick actions, for instance: + +```dart +quickActions.setShortcutItems([ + const ShortcutItem(type: 'action_main', localizedTitle: 'Main view', icon: 'icon_main'), + const ShortcutItem(type: 'action_help', localizedTitle: 'Help', icon: 'icon_help') +]); +``` + +Please note, that the `type` argument should be unique within your application +(among all the registered shortcut items). The optional `icon` should be the +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/). + +For help on editing plugin code, view the [documentation](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin). diff --git a/packages/quick_actions/quick_actions/android/build.gradle b/packages/quick_actions/quick_actions/android/build.gradle new file mode 100644 index 000000000000..00de9453f86d --- /dev/null +++ b/packages/quick_actions/quick_actions/android/build.gradle @@ -0,0 +1,34 @@ +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' + } +} diff --git a/packages/quick_actions/android/gradle.properties b/packages/quick_actions/quick_actions/android/gradle.properties similarity index 100% rename from packages/quick_actions/android/gradle.properties rename to packages/quick_actions/quick_actions/android/gradle.properties diff --git a/packages/quick_actions/android/settings.gradle b/packages/quick_actions/quick_actions/android/settings.gradle similarity index 100% rename from packages/quick_actions/android/settings.gradle rename to packages/quick_actions/quick_actions/android/settings.gradle diff --git a/packages/quick_actions/android/src/main/AndroidManifest.xml b/packages/quick_actions/quick_actions/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/quick_actions/android/src/main/AndroidManifest.xml rename to packages/quick_actions/quick_actions/android/src/main/AndroidManifest.xml 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 new file mode 100644 index 000000000000..465283053442 --- /dev/null +++ b/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java @@ -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. + +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 { + + private static final String CHANNEL_ID = "plugins.flutter.io/quick_actions"; + private static final String EXTRA_ACTION = "some unique action key"; + + 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/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java b/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java similarity index 97% rename from packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java rename to packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java index 14e59951cf5e..ab3431325503 100644 --- a/packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java +++ b/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/quick_actions/quick_actions/example/README.md b/packages/quick_actions/quick_actions/example/README.md new file mode 100644 index 000000000000..d1b72891de9e --- /dev/null +++ b/packages/quick_actions/quick_actions/example/README.md @@ -0,0 +1,8 @@ +# 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 new file mode 100644 index 000000000000..57de7f6e5e03 --- /dev/null +++ b/packages/quick_actions/quick_actions/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 29 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + applicationId "io.flutter.plugins.quickactionsexample" + 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/quick_actions/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/quick_actions/quick_actions/example/android/app/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/quick_actions/example/android/app/gradle/wrapper/gradle-wrapper.properties rename to packages/quick_actions/quick_actions/example/android/app/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/quick_actions/example/android/app/src/main/AndroidManifest.xml b/packages/quick_actions/quick_actions/example/android/app/src/main/AndroidManifest.xml similarity index 100% rename from packages/quick_actions/example/android/app/src/main/AndroidManifest.xml rename to packages/quick_actions/quick_actions/example/android/app/src/main/AndroidManifest.xml diff --git a/packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1Activity.java b/packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1Activity.java new file mode 100644 index 000000000000..d85ead3b4e36 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1Activity.java @@ -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. + +package io.flutter.plugins.quickactionsexample; + +import android.os.Bundle; +import io.flutter.plugins.quickactions.QuickActionsPlugin; + +@SuppressWarnings("deprecation") +public class EmbeddingV1Activity extends io.flutter.app.FlutterActivity { + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + QuickActionsPlugin.registerWith( + registrarFor("io.flutter.plugins.quickactions.QuickActionsPlugin")); + } +} diff --git a/packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1ActivityTest.java b/packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1ActivityTest.java new file mode 100644 index 000000000000..a7fab3f052a4 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1ActivityTest.java @@ -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. + +package io.flutter.plugins.quickactionsexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@RunWith(FlutterTestRunner.class) +@SuppressWarnings("deprecation") +public class EmbeddingV1ActivityTest { + @Rule + public ActivityTestRule rule = + new ActivityTestRule<>(EmbeddingV1Activity.class); +} diff --git a/packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/FlutterActivityTest.java b/packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/FlutterActivityTest.java new file mode 100644 index 000000000000..0b60dfa53e1f --- /dev/null +++ b/packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/FlutterActivityTest.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.quickactionsexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@RunWith(FlutterTestRunner.class) +public class FlutterActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/quick_actions/example/android/app/src/main/res/drawable/ic_launcher_background.xml b/packages/quick_actions/quick_actions/example/android/app/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from packages/quick_actions/example/android/app/src/main/res/drawable/ic_launcher_background.xml rename to packages/quick_actions/quick_actions/example/android/app/src/main/res/drawable/ic_launcher_background.xml diff --git a/packages/integration_test/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/quick_actions/quick_actions/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from packages/integration_test/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/quick_actions/quick_actions/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/integration_test/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/quick_actions/quick_actions/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from packages/integration_test/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/quick_actions/quick_actions/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/integration_test/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/quick_actions/quick_actions/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from packages/integration_test/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/quick_actions/quick_actions/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/integration_test/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/quick_actions/quick_actions/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from packages/integration_test/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/quick_actions/quick_actions/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/integration_test/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/quick_actions/quick_actions/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from packages/integration_test/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/quick_actions/quick_actions/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/packages/quick_actions/example/android/app/src/main/res/values/styles.xml b/packages/quick_actions/quick_actions/example/android/app/src/main/res/values/styles.xml similarity index 100% rename from packages/quick_actions/example/android/app/src/main/res/values/styles.xml rename to packages/quick_actions/quick_actions/example/android/app/src/main/res/values/styles.xml diff --git a/packages/quick_actions/quick_actions/example/android/build.gradle b/packages/quick_actions/quick_actions/example/android/build.gradle new file mode 100644 index 000000000000..e101ac08df55 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/android/build.gradle @@ -0,0 +1,29 @@ +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/quick_actions/example/android/gradle.properties b/packages/quick_actions/quick_actions/example/android/gradle.properties similarity index 100% rename from packages/quick_actions/example/android/gradle.properties rename to packages/quick_actions/quick_actions/example/android/gradle.properties diff --git a/packages/camera/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/quick_actions/quick_actions/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/camera/example/android/gradle/wrapper/gradle-wrapper.properties rename to packages/quick_actions/quick_actions/example/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/settings.gradle b/packages/quick_actions/quick_actions/example/android/settings.gradle old mode 100755 new mode 100644 similarity index 100% rename from packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/android/settings.gradle rename to packages/quick_actions/quick_actions/example/android/settings.gradle 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 new file mode 100644 index 000000000000..cfe3eb0db656 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/integration_test/quick_actions_test.dart @@ -0,0 +1,25 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.9 +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:quick_actions/quick_actions.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('Can set shortcuts', (WidgetTester tester) async { + final QuickActions quickActions = QuickActions(); + await quickActions.initialize(null); + + const ShortcutItem shortCutItem = ShortcutItem( + type: 'action_one', + localizedTitle: 'Action one', + icon: 'AppIcon', + ); + expect( + quickActions.setShortcutItems([shortCutItem]), completes); + }); +} diff --git a/packages/android_intent/example/ios/Flutter/AppFrameworkInfo.plist b/packages/quick_actions/quick_actions/example/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from packages/android_intent/example/ios/Flutter/AppFrameworkInfo.plist rename to packages/quick_actions/quick_actions/example/ios/Flutter/AppFrameworkInfo.plist diff --git a/packages/camera/example/ios/Flutter/Debug.xcconfig b/packages/quick_actions/quick_actions/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/camera/example/ios/Flutter/Debug.xcconfig rename to packages/quick_actions/quick_actions/example/ios/Flutter/Debug.xcconfig diff --git a/packages/camera/example/ios/Flutter/Release.xcconfig b/packages/quick_actions/quick_actions/example/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/camera/example/ios/Flutter/Release.xcconfig rename to packages/quick_actions/quick_actions/example/ios/Flutter/Release.xcconfig diff --git a/packages/quick_actions/quick_actions/example/ios/Podfile b/packages/quick_actions/quick_actions/example/ios/Podfile new file mode 100644 index 000000000000..f7d6a5e68c3a --- /dev/null +++ b/packages/quick_actions/quick_actions/example/ios/Podfile @@ -0,0 +1,38 @@ +# 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/quick_actions/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj b/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..ee150598f59b --- /dev/null +++ b/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,593 @@ +// !$*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 */; }; + 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 */ + 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + CCC799F2B0AB50A9C34344F0 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.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 */ + 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 */ + 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 */, + 97C146EF1CF9000F007C117D /* Products */, + D0FE95BE2380323DD75CB891 /* Pods */, + A44AD0D63DEF785A2A2DEE28 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 686BE82D25E58CCF00862533 /* RunnerUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + A44AD0D63DEF785A2A2DEE28 /* Frameworks */ = { + isa = PBXGroup; + children = ( + CCC799F2B0AB50A9C34344F0 /* libPods-Runner.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + D0FE95BE2380323DD75CB891 /* Pods */ = { + isa = PBXGroup; + children = ( + 5278439583922091276A37C9 /* Pods-Runner.debug.xcconfig */, + F0609304FBCAEC2289164BD5 /* Pods-Runner.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 686BE82C25E58CCF00862533 /* RunnerUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 686BE83625E58CCF00862533 /* Build configuration list for PBXNativeTarget "RunnerUITests" */; + buildPhases = ( + 686BE82925E58CCF00862533 /* Sources */, + 686BE82A25E58CCF00862533 /* Frameworks */, + 686BE82B25E58CCF00862533 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 686BE83325E58CCF00862533 /* PBXTargetDependency */, + ); + name = RunnerUITests; + productName = RunnerUITests; + productReference = 686BE82D25E58CCF00862533 /* RunnerUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + 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 = 1100; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 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 */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 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"; + }; + 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 */ + 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 */ + 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 */ + 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 = 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.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 */ + 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/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..9850cc113026 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerUITests.xcscheme b/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerUITests.xcscheme new file mode 100644 index 000000000000..0164e94407dd --- /dev/null +++ b/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/RunnerUITests.xcscheme @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/quick_actions/quick_actions/example/ios/Runner.xcworkspace/contents.xcworkspacedata old mode 100755 new mode 100644 similarity index 100% rename from packages/google_sign_in/extension_google_sign_in_as_googleapis_auth/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/quick_actions/quick_actions/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/quick_actions/quick_actions/example/ios/Runner/AppDelegate.h b/packages/quick_actions/quick_actions/example/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..0681d288bb70 --- /dev/null +++ b/packages/quick_actions/quick_actions/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/quick_actions/quick_actions/example/ios/Runner/AppDelegate.m b/packages/quick_actions/quick_actions/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..a89d86c28c6f --- /dev/null +++ b/packages/quick_actions/quick_actions/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/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from packages/connectivity/connectivity_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/quick_actions/quick_actions/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/packages/android_intent/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/quick_actions/quick_actions/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from packages/android_intent/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/quick_actions/quick_actions/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/connectivity/connectivity_macos/example/ios/Runner/Base.lproj/Main.storyboard b/packages/quick_actions/quick_actions/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/connectivity/connectivity_macos/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/quick_actions/quick_actions/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/packages/quick_actions/example/ios/Runner/Info.plist b/packages/quick_actions/quick_actions/example/ios/Runner/Info.plist similarity index 100% rename from packages/quick_actions/example/ios/Runner/Info.plist rename to packages/quick_actions/quick_actions/example/ios/Runner/Info.plist diff --git a/packages/quick_actions/quick_actions/example/ios/Runner/main.m b/packages/quick_actions/quick_actions/example/ios/Runner/main.m new file mode 100644 index 000000000000..f97b9ef5c8a1 --- /dev/null +++ b/packages/quick_actions/quick_actions/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/quick_actions/quick_actions/example/ios/RunnerUITests/Info.plist b/packages/quick_actions/quick_actions/example/ios/RunnerUITests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/ios/RunnerUITests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/packages/quick_actions/quick_actions/example/ios/RunnerUITests/RunnerUITests.m b/packages/quick_actions/quick_actions/example/ios/RunnerUITests/RunnerUITests.m new file mode 100644 index 000000000000..0bad57f886de --- /dev/null +++ b/packages/quick_actions/quick_actions/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/example/lib/main.dart b/packages/quick_actions/quick_actions/example/lib/main.dart new file mode 100644 index 000000000000..8e47d16683dd --- /dev/null +++ b/packages/quick_actions/quick_actions/example/lib/main.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. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:quick_actions/quick_actions.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Quick Actions Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: MyHomePage(), + ); + } +} + +class MyHomePage extends StatefulWidget { + MyHomePage({Key? key}) : super(key: key); + + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + String shortcut = 'no action set'; + + @override + void initState() { + super.initState(); + + final QuickActions quickActions = QuickActions(); + quickActions.initialize((String shortcutType) { + setState(() { + if (shortcutType != null) { + shortcut = shortcutType; + } + }); + }); + + quickActions.setShortcutItems([ + // NOTE: This first action icon will only work on iOS. + // In a real world project keep the same file name for both platforms. + const ShortcutItem( + type: 'action_one', + localizedTitle: 'Action one', + icon: 'AppIcon', + ), + // NOTE: This second action icon will only work on Android. + // In a real world project keep the same file name for both platforms. + const ShortcutItem( + type: 'action_two', + localizedTitle: 'Action two', + icon: 'ic_launcher'), + ]).then((value) { + 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/example/pubspec.yaml b/packages/quick_actions/quick_actions/example/pubspec.yaml new file mode 100644 index 000000000000..eaf3de4b56e0 --- /dev/null +++ b/packages/quick_actions/quick_actions/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.12.0 <3.0.0" + flutter: ">=1.9.1+hotfix.2" + +dependencies: + flutter: + sdk: flutter + quick_actions: + # When depending on this package from a real application you should use: + # quick_actions: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + +dev_dependencies: + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + pedantic: ^1.10.0 + +flutter: + uses-material-design: true diff --git a/packages/quick_actions/quick_actions/example/test_driver/integration_test.dart b/packages/quick_actions/quick_actions/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..6a0e6fa82dbe --- /dev/null +++ b/packages/quick_actions/quick_actions/example/test_driver/integration_test.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. + +// @dart=2.9 + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/camera/ios/Assets/.gitkeep b/packages/quick_actions/quick_actions/ios/Assets/.gitkeep similarity index 100% rename from packages/camera/ios/Assets/.gitkeep rename to packages/quick_actions/quick_actions/ios/Assets/.gitkeep diff --git a/packages/quick_actions/ios/Classes/FLTQuickActionsPlugin.h b/packages/quick_actions/quick_actions/ios/Classes/FLTQuickActionsPlugin.h similarity index 76% rename from packages/quick_actions/ios/Classes/FLTQuickActionsPlugin.h rename to packages/quick_actions/quick_actions/ios/Classes/FLTQuickActionsPlugin.h index f0ef61d445e9..8f98cc35e8ba 100644 --- a/packages/quick_actions/ios/Classes/FLTQuickActionsPlugin.h +++ b/packages/quick_actions/quick_actions/ios/Classes/FLTQuickActionsPlugin.h @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/quick_actions/quick_actions/ios/Classes/FLTQuickActionsPlugin.m b/packages/quick_actions/quick_actions/ios/Classes/FLTQuickActionsPlugin.m new file mode 100644 index 000000000000..3a966f86a824 --- /dev/null +++ b/packages/quick_actions/quick_actions/ios/Classes/FLTQuickActionsPlugin.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 "FLTQuickActionsPlugin.h" + +static NSString *const CHANNEL_NAME = @"plugins.flutter.io/quick_actions"; + +@interface FLTQuickActionsPlugin () +@property(nonatomic, retain) FlutterMethodChannel *channel; +@property(nonatomic, retain) NSString *shortcutType; +@end + +@implementation FLTQuickActionsPlugin + ++ (void)registerWithRegistrar:(NSObject *)registrar { + FlutterMethodChannel *channel = + [FlutterMethodChannel methodChannelWithName:CHANNEL_NAME + binaryMessenger:[registrar messenger]]; + FLTQuickActionsPlugin *instance = [[FLTQuickActionsPlugin alloc] init]; + instance.channel = channel; + [registrar addMethodCallDelegate:instance channel:channel]; + [registrar addApplicationDelegate:instance]; +} + +- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { + if (@available(iOS 9.0, *)) { + if ([call.method isEqualToString:@"setShortcutItems"]) { + _setShortcutItems(call.arguments); + result(nil); + } else if ([call.method isEqualToString:@"clearShortcutItems"]) { + [UIApplication sharedApplication].shortcutItems = @[]; + result(nil); + } else if ([call.method isEqualToString:@"getLaunchAction"]) { + result(nil); + } else { + result(FlutterMethodNotImplemented); + } + } else { + NSLog(@"Shortcuts are not supported prior to iOS 9."); + result(nil); + } +} + +- (void)dealloc { + [_channel setMethodCallHandler:nil]; + _channel = nil; +} + +- (BOOL)application:(UIApplication *)application + performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem + completionHandler:(void (^)(BOOL succeeded))completionHandler + API_AVAILABLE(ios(9.0)) { + [self handleShortcut:shortcutItem.type]; + return YES; +} + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + if (@available(iOS 9.0, *)) { + UIApplicationShortcutItem *shortcutItem = + launchOptions[UIApplicationLaunchOptionsShortcutItemKey]; + if (shortcutItem) { + // Keep hold of the shortcut type and handle it in the + // `applicationDidBecomeActure:` method once the Dart MethodChannel + // is initialized. + self.shortcutType = shortcutItem.type; + + // Return NO to indicate we handled the quick action to ensure + // the `application:performActionFor:` method is not called (as + // per Apple's documentation: + // https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622935-application?language=objc). + return NO; + } + } + return YES; +} + +- (void)applicationDidBecomeActive:(UIApplication *)application { + if (self.shortcutType) { + [self handleShortcut:self.shortcutType]; + self.shortcutType = nil; + } +} + +#pragma mark Private functions + +- (void)handleShortcut:(NSString *)shortcut { + [self.channel invokeMethod:@"launch" arguments:shortcut]; +} + +NS_INLINE void _setShortcutItems(NSArray *items) API_AVAILABLE(ios(9.0)) { + NSMutableArray *newShortcuts = [[NSMutableArray alloc] init]; + + for (id item in items) { + UIApplicationShortcutItem *shortcut = _deserializeShortcutItem(item); + [newShortcuts addObject:shortcut]; + } + + [UIApplication sharedApplication].shortcutItems = newShortcuts; +} + +NS_INLINE UIApplicationShortcutItem *_deserializeShortcutItem(NSDictionary *serialized) + API_AVAILABLE(ios(9.0)) { + UIApplicationShortcutIcon *icon = + [serialized[@"icon"] isKindOfClass:[NSNull class]] + ? nil + : [UIApplicationShortcutIcon iconWithTemplateImageName:serialized[@"icon"]]; + + return [[UIApplicationShortcutItem alloc] initWithType:serialized[@"type"] + localizedTitle:serialized[@"localizedTitle"] + localizedSubtitle:nil + icon:icon + userInfo:nil]; +} + +@end diff --git a/packages/quick_actions/ios/quick_actions.podspec b/packages/quick_actions/quick_actions/ios/quick_actions.podspec similarity index 100% rename from packages/quick_actions/ios/quick_actions.podspec rename to packages/quick_actions/quick_actions/ios/quick_actions.podspec diff --git a/packages/quick_actions/quick_actions/lib/quick_actions.dart b/packages/quick_actions/quick_actions/lib/quick_actions.dart new file mode 100644 index 000000000000..f90a44e0443d --- /dev/null +++ b/packages/quick_actions/quick_actions/lib/quick_actions.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 'dart:async'; + +import 'package:quick_actions_platform_interface/platform_interface/quick_actions_platform.dart'; +import 'package:quick_actions_platform_interface/types/types.dart'; + +export 'package:quick_actions_platform_interface/types/types.dart'; + +/// Quick actions plugin. +class QuickActions { + /// Initializes this plugin. + /// + /// Call this once before any further interaction with the the plugin. + Future initialize(QuickActionHandler handler) async => + QuickActionsPlatform.instance.initialize(handler); + + /// Sets the [ShortcutItem]s to become the app's quick actions. + Future setShortcutItems(List items) async => + QuickActionsPlatform.instance.setShortcutItems(items); + + /// Removes all [ShortcutItem]s registered for the app. + Future clearShortcutItems() => + QuickActionsPlatform.instance.clearShortcutItems(); +} diff --git a/packages/quick_actions/quick_actions/pubspec.yaml b/packages/quick_actions/quick_actions/pubspec.yaml new file mode 100644 index 000000000000..7927fcc3b548 --- /dev/null +++ b/packages/quick_actions/quick_actions/pubspec.yaml @@ -0,0 +1,34 @@ +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 +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+quick_actions%22 +version: 0.6.0+2 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +flutter: + plugin: + platforms: + android: + package: io.flutter.plugins.quickactions + pluginClass: QuickActionsPlugin + ios: + pluginClass: FLTQuickActionsPlugin + +dependencies: + flutter: + sdk: flutter + meta: ^1.3.0 + quick_actions_platform_interface: ^1.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + mockito: ^5.0.0-nullsafety.7 + pedantic: ^1.11.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 new file mode 100644 index 000000000000..b8d7695735b6 --- /dev/null +++ b/packages/quick_actions/quick_actions/test/quick_actions_test.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_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', () { + setUp(() { + QuickActionsPlatform.instance = MockQuickActionsPlatform(); + }); + + test('initialize() PlatformInterface', () async { + QuickActions quickActions = QuickActions(); + QuickActionHandler handler = (type) {}; + + await quickActions.initialize(handler); + verify(QuickActionsPlatform.instance.initialize(handler)).called(1); + }); + + test('setShortcutItems() PlatformInterface', () { + QuickActions quickActions = QuickActions(); + QuickActionHandler handler = (type) {}; + quickActions.initialize(handler); + quickActions.setShortcutItems([]); + + verify(QuickActionsPlatform.instance.initialize(handler)).called(1); + verify(QuickActionsPlatform.instance.setShortcutItems([])).called(1); + }); + + test('clearShortcutItems() PlatformInterface', () { + QuickActions quickActions = QuickActions(); + QuickActionHandler handler = (type) {}; + + quickActions.initialize(handler); + quickActions.clearShortcutItems(); + + verify(QuickActionsPlatform.instance.initialize(handler)).called(1); + verify(QuickActionsPlatform.instance.clearShortcutItems()).called(1); + }); + }); +} + +class MockQuickActionsPlatform extends Mock + with MockPlatformInterfaceMixin + implements QuickActionsPlatform { + @override + Future clearShortcutItems() async => + super.noSuchMethod(Invocation.method(#clearShortcutItems, [])); + + @override + Future initialize(QuickActionHandler? handler) async => + super.noSuchMethod(Invocation.method(#initialize, [handler])); + + @override + Future setShortcutItems(List? items) async => + super.noSuchMethod(Invocation.method(#setShortcutItems, [items])); +} + +class MockQuickActions extends QuickActions {} diff --git a/packages/quick_actions/quick_actions_android.iml b/packages/quick_actions/quick_actions_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/quick_actions/quick_actions_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/quick_actions/quick_actions_platform_interface/CHANGELOG.md b/packages/quick_actions/quick_actions_platform_interface/CHANGELOG.md new file mode 100644 index 000000000000..4b63991e9c4a --- /dev/null +++ b/packages/quick_actions/quick_actions_platform_interface/CHANGELOG.md @@ -0,0 +1,3 @@ +# 1.0.0 + +* Initial release of quick_actions_platform_interface diff --git a/packages/quick_actions/quick_actions_platform_interface/LICENSE b/packages/quick_actions/quick_actions_platform_interface/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/quick_actions/quick_actions_platform_interface/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_platform_interface/README.md b/packages/quick_actions/quick_actions_platform_interface/README.md new file mode 100644 index 000000000000..ce8136ee9614 --- /dev/null +++ b/packages/quick_actions/quick_actions_platform_interface/README.md @@ -0,0 +1,26 @@ +# quick_actions_platform_interface + +A common platform interface for the [`quick_actions`][1] plugin. + +This interface allows platform-specific implementations of the `quick_actions` +plugin, as well as the plugin itself, to ensure they are supporting the +same interface. + +# Usage + +To implement a new platform-specific implementation of `quick_actions`, extend +[`QuickActionsPlatform`][2] with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`QuickActionsPlatform` by calling +`QuickActionsPlatform.instance = MyPlatformQuickActions()`. + +# 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]: ../quick_actions +[2]: lib/quick_actions_platform_interface.dart 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 new file mode 100644 index 000000000000..8172fe017a4d --- /dev/null +++ b/packages/quick_actions/quick_actions_platform_interface/lib/method_channel/method_channel_quick_actions.dart @@ -0,0 +1,52 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/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 = + MethodChannel('plugins.flutter.io/quick_actions'); + +/// An implementation of [QuickActionsPlatform] that uses method channels. +class MethodChannelQuickActions extends QuickActionsPlatform { + /// 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); + }); + 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_platform_interface/lib/platform_interface/quick_actions_platform.dart b/packages/quick_actions/quick_actions_platform_interface/lib/platform_interface/quick_actions_platform.dart new file mode 100644 index 000000000000..b15fb8b43233 --- /dev/null +++ b/packages/quick_actions/quick_actions_platform_interface/lib/platform_interface/quick_actions_platform.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:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:quick_actions_platform_interface/types/types.dart'; + +import '../method_channel/method_channel_quick_actions.dart'; + +/// The interface that implementations of quick_actions must implement. +/// +/// Platform implementations should extend this class rather than implement it as `quick_actions` +/// 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 +/// [QuickActionsPlatform] methods. +abstract class QuickActionsPlatform extends PlatformInterface { + /// Constructs a QuickActionsPlatform. + QuickActionsPlatform() : super(token: _token); + + static final Object _token = Object(); + + static QuickActionsPlatform _instance = MethodChannelQuickActions(); + + /// The default instance of [QuickActionsPlatform] to use. + /// + /// Defaults to [MethodChannelQuickActions]. + static QuickActionsPlatform get instance => _instance; + + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [QuickActionsPlatform] when they register themselves. + // TODO(amirh): Extract common platform interface logic. + // https://github.com/flutter/flutter/issues/43368 + static set instance(QuickActionsPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + /// Initializes this plugin. + /// + /// Call this once before any further interaction with the the plugin. + Future initialize(QuickActionHandler handler) async { + 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."); + } + + /// Removes all [ShortcutItem]s registered for the app. + Future clearShortcutItems() { + throw UnimplementedError("clearShortcutItems() has not been implemented."); + } +} diff --git a/packages/quick_actions/quick_actions_platform_interface/lib/quick_actions_platform_interface.dart b/packages/quick_actions/quick_actions_platform_interface/lib/quick_actions_platform_interface.dart new file mode 100644 index 000000000000..51bed8f230a8 --- /dev/null +++ b/packages/quick_actions/quick_actions_platform_interface/lib/quick_actions_platform_interface.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. + +export 'package:quick_actions_platform_interface/platform_interface/quick_actions_platform.dart'; +export 'package:quick_actions_platform_interface/types/types.dart'; 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 new file mode 100644 index 000000000000..27c6bb494dfd --- /dev/null +++ b/packages/quick_actions/quick_actions_platform_interface/lib/types/quick_action_handler.dart @@ -0,0 +1,8 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Handler for a quick action launch event. +/// +/// The argument [type] corresponds to the [ShortcutItem]'s field. +typedef void QuickActionHandler(String type); diff --git a/packages/quick_actions/quick_actions_platform_interface/lib/types/shortcut_item.dart b/packages/quick_actions/quick_actions_platform_interface/lib/types/shortcut_item.dart new file mode 100644 index 000000000000..1d84e16ac996 --- /dev/null +++ b/packages/quick_actions/quick_actions_platform_interface/lib/types/shortcut_item.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. + +/// Home screen quick-action shortcut item. +class ShortcutItem { + /// Constructs an instance with the given [type], [localizedTitle], and + /// [icon]. + /// + /// Only [icon] should be nullable. It will remain `null` if unset. + const ShortcutItem({ + required this.type, + required this.localizedTitle, + this.icon, + }); + + /// The identifier of this item; should be unique within the app. + final String type; + + /// Localized title of the item. + final String localizedTitle; + + /// Name of native resource (xcassets etc; NOT a Flutter asset) to be + /// displayed as the icon for this item. + final String? icon; +} diff --git a/packages/quick_actions/quick_actions_platform_interface/lib/types/types.dart b/packages/quick_actions/quick_actions_platform_interface/lib/types/types.dart new file mode 100644 index 000000000000..ab85ca8260ce --- /dev/null +++ b/packages/quick_actions/quick_actions_platform_interface/lib/types/types.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. + +export 'quick_action_handler.dart'; +export 'shortcut_item.dart'; diff --git a/packages/quick_actions/quick_actions_platform_interface/pubspec.yaml b/packages/quick_actions/quick_actions_platform_interface/pubspec.yaml new file mode 100644 index 000000000000..4b9542eb1649 --- /dev/null +++ b/packages/quick_actions/quick_actions_platform_interface/pubspec.yaml @@ -0,0 +1,23 @@ +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 +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 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +dependencies: + flutter: + sdk: flutter + meta: ^1.3.0 + plugin_platform_interface: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + mockito: ^5.0.1 + pedantic: ^1.11.0 diff --git a/packages/quick_actions/quick_actions_platform_interface/test/method_channel_quick_actions_test.dart b/packages/quick_actions/quick_actions_platform_interface/test/method_channel_quick_actions_test.dart new file mode 100644 index 000000000000..f3e172e207fe --- /dev/null +++ b/packages/quick_actions/quick_actions_platform_interface/test/method_channel_quick_actions_test.dart @@ -0,0 +1,155 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/services.dart'; +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/types/shortcut_item.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$MethodChannelQuickActions', () { + MethodChannelQuickActions quickActions = MethodChannelQuickActions(); + + final List log = []; + + setUp(() { + quickActions.channel + .setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return ''; + }); + + log.clear(); + }); + + group('#initialize', () { + test('passes getLaunchAction on launch method', () { + quickActions.initialize((type) { + 'launch'; + }); + + expect( + log, + [ + isMethodCall('getLaunchAction', arguments: null), + ], + ); + }); + + test('initialize', () async { + 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', () { + quickActions.initialize((type) { + 'launch'; + }); + quickActions.setShortcutItems([ + 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'; + 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', () { + quickActions.initialize((type) { + 'launch'; + }); + quickActions.clearShortcutItems(); + + expect( + log, + [ + isMethodCall('getLaunchAction', arguments: null), + isMethodCall('clearShortcutItems', arguments: null), + ], + ); + }); + + test('clearShortcutItems', () { + 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/test/quick_actions_platform_interface_test.dart b/packages/quick_actions/quick_actions_platform_interface/test/quick_actions_platform_interface_test.dart new file mode 100644 index 000000000000..8ce40816217f --- /dev/null +++ b/packages/quick_actions/quick_actions_platform_interface/test/quick_actions_platform_interface_test.dart @@ -0,0 +1,73 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_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'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$QuickActionsPlatform', () { + test('$MethodChannelQuickActions is the default instance', () { + expect(QuickActionsPlatform.instance, isA()); + }); + + test('Cannot be implemented with `implements`', () { + expect(() { + QuickActionsPlatform.instance = ImplementsQuickActionsPlatform(); + }, throwsNoSuchMethodError); + }); + + test('Can be extended', () { + QuickActionsPlatform.instance = ExtendsQuickActionsPlatform(); + }); + + test( + 'Default implementation of initialize() should throw unimplemented error', + () { + // Arrange + final QuickActionsPlatform = ExtendsQuickActionsPlatform(); + + // Act & Assert + expect( + () => QuickActionsPlatform.initialize((type) {}), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of setShortcutItems() should throw unimplemented error', + () { + // Arrange + final QuickActionsPlatform = ExtendsQuickActionsPlatform(); + + // Act & Assert + expect( + () => QuickActionsPlatform.setShortcutItems([]), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of clearShortcutItems() should throw unimplemented error', + () { + // Arrange + final QuickActionsPlatform = ExtendsQuickActionsPlatform(); + + // Act & Assert + expect( + () => QuickActionsPlatform.clearShortcutItems(), + throwsUnimplementedError, + ); + }); + }); +} + +class ImplementsQuickActionsPlatform implements QuickActionsPlatform { + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class ExtendsQuickActionsPlatform extends QuickActionsPlatform {} diff --git a/packages/quick_actions/test/quick_actions_test.dart b/packages/quick_actions/test/quick_actions_test.dart deleted file mode 100644 index ffb6de1024fd..000000000000 --- a/packages/quick_actions/test/quick_actions_test.dart +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. -import 'dart:async'; - -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:quick_actions/quick_actions.dart'; - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - QuickActions quickActions; - final List log = []; - - setUp(() { - quickActions = QuickActions(); - quickActions.channel.setMockMethodCallHandler( - (MethodCall methodCall) async { - log.add(methodCall); - return 'non empty response'; - }, - ); - }); - - test('setShortcutItems with demo data', () async { - const String type = 'type'; - const String localizedTitle = 'localizedTitle'; - const String icon = 'icon'; - await quickActions.setShortcutItems( - const [ - ShortcutItem(type: type, localizedTitle: localizedTitle, icon: icon) - ], - ); - expect( - log, - [ - isMethodCall( - 'setShortcutItems', - arguments: >[ - { - 'type': type, - 'localizedTitle': localizedTitle, - 'icon': icon, - } - ], - ), - ], - ); - log.clear(); - }); - - test('clearShortcutItems', () { - quickActions.clearShortcutItems(); - expect( - log, - [ - isMethodCall('clearShortcutItems', arguments: null), - ], - ); - log.clear(); - }); - - test('initialize', () async { - final Completer quickActionsHandler = Completer(); - quickActions.initialize((_) => quickActionsHandler.complete(true)); - expect( - log, - [ - isMethodCall('getLaunchAction', arguments: null), - ], - ); - log.clear(); - - expect(quickActionsHandler.future, completion(isTrue)); - }); - - 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/sensors/AUTHORS b/packages/sensors/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/sensors/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/sensors/CHANGELOG.md b/packages/sensors/CHANGELOG.md index aa9f2bc4fbc5..d7bf66d432a6 100644 --- a/packages/sensors/CHANGELOG.md +++ b/packages/sensors/CHANGELOG.md @@ -1,3 +1,31 @@ +## 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. diff --git a/packages/sensors/LICENSE b/packages/sensors/LICENSE index a6d6c0749818..c6823b81eb84 100644 --- a/packages/sensors/LICENSE +++ b/packages/sensors/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017 The Chromium Authors. All rights reserved. +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: diff --git a/packages/sensors/README.md b/packages/sensors/README.md index e3c80b2b2947..1f46ce1c3608 100644 --- a/packages/sensors/README.md +++ b/packages/sensors/README.md @@ -1,19 +1,26 @@ # sensors -**Please set your constraint to `sensors: '>=0.4.y+x <2.0.0'`** +--- -## Backward compatible 1.0.0 version is coming -The sensors plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.4.y+z`. -Please use `sensors: '>=0.4.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 +## Deprecation Notice -A Flutter plugin to access the accelerometer and gyroscope sensors. +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.io/platform-plugins/). +file](https://flutter.dev/docs/development/platform-integration/platform-channels). This will expose three classes of sensor events, through three different streams. diff --git a/packages/sensors/analysis_options.yaml b/packages/sensors/analysis_options.yaml new file mode 100644 index 000000000000..cda4f6e153e6 --- /dev/null +++ b/packages/sensors/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options_legacy.yaml diff --git a/packages/sensors/android/build.gradle b/packages/sensors/android/build.gradle index c42795200e62..50b4eac981e2 100644 --- a/packages/sensors/android/build.gradle +++ b/packages/sensors/android/build.gradle @@ -4,7 +4,7 @@ version '1.0-SNAPSHOT' buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -15,14 +15,14 @@ buildscript { rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } apply plugin: 'com.android.library' android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { minSdkVersion 16 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 index e01c80d1cdd2..c643edce3401 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/sensors/android/src/main/java/io/flutter/plugins/sensors/StreamHandlerImpl.java b/packages/sensors/android/src/main/java/io/flutter/plugins/sensors/StreamHandlerImpl.java index ac0546109f96..7e6da156386d 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/sensors/example/README.md b/packages/sensors/example/README.md index b2454123528a..9e7d7e0a76a9 100644 --- a/packages/sensors/example/README.md +++ b/packages/sensors/example/README.md @@ -5,4 +5,4 @@ Demonstrates how to use the sensors plugin. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). diff --git a/packages/sensors/example/android/app/build.gradle b/packages/sensors/example/android/app/build.gradle index 987def463562..d9c1e41f0759 100644 --- a/packages/sensors/example/android/app/build.gradle +++ b/packages/sensors/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 28 + compileSdkVersion 29 lintOptions { disable 'InvalidPackage' diff --git a/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/EmbeddingV1Activity.java b/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/EmbeddingV1Activity.java index 7813e7e8e43f..128768a097aa 100644 --- a/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/EmbeddingV1Activity.java +++ b/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/EmbeddingV1Activity.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/EmbeddingV1ActivityTest.java b/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/EmbeddingV1ActivityTest.java index 65a4dca981aa..c96ab243a778 100644 --- a/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/EmbeddingV1ActivityTest.java +++ b/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/EmbeddingV1ActivityTest.java @@ -1,3 +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. + package io.flutter.plugins.sensorsexample; import androidx.test.rule.ActivityTestRule; 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 index 0835b0f5945a..c1584aab107c 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/sensors/example/android/build.gradle b/packages/sensors/example/android/build.gradle index 541636cc492a..e101ac08df55 100644 --- a/packages/sensors/example/android/build.gradle +++ b/packages/sensors/example/android/build.gradle @@ -1,7 +1,7 @@ buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -12,7 +12,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/packages/sensors/integration_test/sensors_test.dart b/packages/sensors/example/integration_test/sensors_test.dart similarity index 76% rename from packages/sensors/integration_test/sensors_test.dart rename to packages/sensors/example/integration_test/sensors_test.dart index ea1db0375f38..3b8f614d2dcb 100644 --- a/packages/sensors/integration_test/sensors_test.dart +++ b/packages/sensors/example/integration_test/sensors_test.dart @@ -1,6 +1,8 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter 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'; diff --git a/packages/sensors/example/ios/Podfile b/packages/sensors/example/ios/Podfile new file mode 100644 index 000000000000..f7d6a5e68c3a --- /dev/null +++ b/packages/sensors/example/ios/Podfile @@ -0,0 +1,38 @@ +# 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 index 8bde68c84719..2360a4aaf532 100644 --- a/packages/sensors/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/sensors/example/ios/Runner.xcodeproj/project.pbxproj @@ -177,7 +177,7 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; + ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; @@ -438,7 +438,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.sensorsExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.sensorsExample; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -460,7 +460,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.sensorsExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.sensorsExample; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; diff --git a/packages/sensors/example/ios/Runner/AppDelegate.h b/packages/sensors/example/ios/Runner/AppDelegate.h index 36e21bbf9cf4..0681d288bb70 100644 --- a/packages/sensors/example/ios/Runner/AppDelegate.h +++ b/packages/sensors/example/ios/Runner/AppDelegate.h @@ -1,3 +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 #import diff --git a/packages/sensors/example/ios/Runner/AppDelegate.m b/packages/sensors/example/ios/Runner/AppDelegate.m index 59a72e90be12..30b87969f44a 100644 --- a/packages/sensors/example/ios/Runner/AppDelegate.m +++ b/packages/sensors/example/ios/Runner/AppDelegate.m @@ -1,3 +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. + #include "AppDelegate.h" #include "GeneratedPluginRegistrant.h" diff --git a/packages/sensors/example/ios/Runner/main.m b/packages/sensors/example/ios/Runner/main.m index dff6597e4513..f97b9ef5c8a1 100644 --- a/packages/sensors/example/ios/Runner/main.m +++ b/packages/sensors/example/ios/Runner/main.m @@ -1,3 +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 #import #import "AppDelegate.h" diff --git a/packages/sensors/example/lib/main.dart b/packages/sensors/example/lib/main.dart index 575e0493742f..0946a8e8421b 100644 --- a/packages/sensors/example/lib/main.dart +++ b/packages/sensors/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -28,7 +28,7 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); + MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @@ -41,21 +41,21 @@ class _MyHomePageState extends State { static const int _snakeColumns = 20; static const double _snakeCellSize = 10.0; - List _accelerometerValues; - List _userAccelerometerValues; - List _gyroscopeValues; + List? _accelerometerValues; + List? _userAccelerometerValues; + List? _gyroscopeValues; List> _streamSubscriptions = >[]; @override Widget build(BuildContext context) { - final List accelerometer = - _accelerometerValues?.map((double v) => v.toStringAsFixed(1))?.toList(); - final List gyroscope = - _gyroscopeValues?.map((double v) => v.toStringAsFixed(1))?.toList(); - final List userAccelerometer = _userAccelerometerValues + final List? accelerometer = + _accelerometerValues?.map((double v) => v.toStringAsFixed(1)).toList(); + final List? gyroscope = + _gyroscopeValues?.map((double v) => v.toStringAsFixed(1)).toList(); + final List? userAccelerometer = _userAccelerometerValues ?.map((double v) => v.toStringAsFixed(1)) - ?.toList(); + .toList(); return Scaffold( appBar: AppBar( diff --git a/packages/sensors/example/lib/snake.dart b/packages/sensors/example/lib/snake.dart index d6b2f9b48a23..47177681020f 100644 --- a/packages/sensors/example/lib/snake.dart +++ b/packages/sensors/example/lib/snake.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -56,15 +56,14 @@ class SnakeBoardPainter extends CustomPainter { } class SnakeState extends State { - SnakeState(int rows, int columns, this.cellSize) { - state = GameState(rows, columns); - } + SnakeState(int rows, int columns, this.cellSize) + : state = GameState(rows, columns); double cellSize; GameState state; - AccelerometerEvent acceleration; - StreamSubscription _streamSubscription; - Timer _timer; + AccelerometerEvent? acceleration; + late StreamSubscription _streamSubscription; + late Timer _timer; @override Widget build(BuildContext context) { @@ -96,21 +95,21 @@ class SnakeState extends State { } void _step() { - final math.Point newDirection = acceleration == null + final AccelerometerEvent? currentAcceleration = acceleration; + final math.Point? newDirection = currentAcceleration == null ? null - : acceleration.x.abs() < 1.0 && acceleration.y.abs() < 1.0 + : currentAcceleration.x.abs() < 1.0 && currentAcceleration.y.abs() < 1.0 ? null - : (acceleration.x.abs() < acceleration.y.abs()) - ? math.Point(0, acceleration.y.sign.toInt()) - : math.Point(-acceleration.x.sign.toInt(), 0); + : (currentAcceleration.x.abs() < currentAcceleration.y.abs()) + ? math.Point(0, currentAcceleration.y.sign.toInt()) + : math.Point(-currentAcceleration.x.sign.toInt(), 0); state.step(newDirection); } } class GameState { - GameState(this.rows, this.columns) { - snakeLength = math.min(rows, columns) - 5; - } + GameState(this.rows, this.columns) + : snakeLength = math.min(rows, columns) - 5; int rows; int columns; @@ -119,7 +118,7 @@ class GameState { List> body = >[const math.Point(0, 0)]; math.Point direction = const math.Point(1, 0); - void step(math.Point newDirection) { + void step(math.Point? newDirection) { math.Point next = body.last + direction; next = math.Point(next.x % columns, next.y % rows); diff --git a/packages/sensors/example/pubspec.yaml b/packages/sensors/example/pubspec.yaml index eb46c611a43a..fee7bd61f736 100644 --- a/packages/sensors/example/pubspec.yaml +++ b/packages/sensors/example/pubspec.yaml @@ -1,23 +1,28 @@ 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: - path: ../../integration_test - pedantic: ^1.8.0 + sdk: flutter + pedantic: ^1.10.0 flutter: uses-material-design: true - -environment: - sdk: ">=2.0.0-dev.28.0 <3.0.0" - flutter: ">=1.9.1+hotfix.2 <2.0.0" - diff --git a/packages/sensors/example/test_driver/integration_test.dart b/packages/sensors/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..6a0e6fa82dbe --- /dev/null +++ b/packages/sensors/example/test_driver/integration_test.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. + +// @dart=2.9 + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/sensors/example/test_driver/test/integration_test.dart b/packages/sensors/example/test_driver/test/integration_test.dart deleted file mode 100644 index 7a2c21338786..000000000000 --- a/packages/sensors/example/test_driver/test/integration_test.dart +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'package:flutter_driver/flutter_driver.dart'; - -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String data = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - await driver.close(); - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} diff --git a/packages/sensors/ios/Classes/FLTSensorsPlugin.h b/packages/sensors/ios/Classes/FLTSensorsPlugin.h index 288db1901ed2..8c3176b42a44 100644 --- a/packages/sensors/ios/Classes/FLTSensorsPlugin.h +++ b/packages/sensors/ios/Classes/FLTSensorsPlugin.h @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/sensors/ios/Classes/FLTSensorsPlugin.m b/packages/sensors/ios/Classes/FLTSensorsPlugin.m index ba8d542f488e..3d0ce66a2b25 100644 --- a/packages/sensors/ios/Classes/FLTSensorsPlugin.m +++ b/packages/sensors/ios/Classes/FLTSensorsPlugin.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -34,7 +34,7 @@ + (void)registerWithRegistrar:(NSObject*)registrar { const double GRAVITY = 9.8; CMMotionManager* _motionManager; -void _initMotionManager() { +void _initMotionManager(void) { if (!_motionManager) { _motionManager = [[CMMotionManager alloc] init]; } diff --git a/packages/sensors/lib/sensors.dart b/packages/sensors/lib/sensors.dart index 0b6f1b5a6067..8db29e017ad0 100644 --- a/packages/sensors/lib/sensors.dart +++ b/packages/sensors/lib/sensors.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -121,38 +121,50 @@ GyroscopeEvent _listToGyroscopeEvent(List list) { return GyroscopeEvent(list[0], list[1], list[2]); } -Stream _accelerometerEvents; -Stream _gyroscopeEvents; -Stream _userAccelerometerEvents; +Stream? _accelerometerEvents; +Stream? _gyroscopeEvents; +Stream? _userAccelerometerEvents; /// A broadcast stream of events from the device accelerometer. Stream get accelerometerEvents { - if (_accelerometerEvents == null) { - _accelerometerEvents = _accelerometerEventChannel - .receiveBroadcastStream() - .map( - (dynamic event) => _listToAccelerometerEvent(event.cast())); + Stream? accelerometerEvents = _accelerometerEvents; + if (accelerometerEvents == null) { + accelerometerEvents = + _accelerometerEventChannel.receiveBroadcastStream().map( + (dynamic event) => + _listToAccelerometerEvent(event.cast()), + ); + _accelerometerEvents = accelerometerEvents; } - return _accelerometerEvents; + + return accelerometerEvents; } /// A broadcast stream of events from the device gyroscope. Stream get gyroscopeEvents { - if (_gyroscopeEvents == null) { - _gyroscopeEvents = _gyroscopeEventChannel - .receiveBroadcastStream() - .map((dynamic event) => _listToGyroscopeEvent(event.cast())); + Stream? gyroscopeEvents = _gyroscopeEvents; + if (gyroscopeEvents == null) { + gyroscopeEvents = _gyroscopeEventChannel.receiveBroadcastStream().map( + (dynamic event) => _listToGyroscopeEvent(event.cast()), + ); + _gyroscopeEvents = gyroscopeEvents; } - return _gyroscopeEvents; + + return gyroscopeEvents; } /// Events from the device accelerometer with gravity removed. Stream get userAccelerometerEvents { - if (_userAccelerometerEvents == null) { - _userAccelerometerEvents = _userAccelerometerEventChannel - .receiveBroadcastStream() - .map((dynamic event) => - _listToUserAccelerometerEvent(event.cast())); + Stream? userAccelerometerEvents = + _userAccelerometerEvents; + if (userAccelerometerEvents == null) { + userAccelerometerEvents = + _userAccelerometerEventChannel.receiveBroadcastStream().map( + (dynamic event) => + _listToUserAccelerometerEvent(event.cast()), + ); + _userAccelerometerEvents = userAccelerometerEvents; } - return _userAccelerometerEvents; + + return userAccelerometerEvents; } diff --git a/packages/sensors/pubspec.yaml b/packages/sensors/pubspec.yaml index 3e44f1e1ea8e..b26819b64df0 100644 --- a/packages/sensors/pubspec.yaml +++ b/packages/sensors/pubspec.yaml @@ -1,11 +1,13 @@ name: sensors description: Flutter plugin for accessing the Android and iOS accelerometer and gyroscope sensors. -homepage: https://github.com/flutter/plugins/tree/master/packages/sensors -# 0.4.y+z is compatible with 1.0.0, if you land a breaking change bump -# the version to 2.0.0. -# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.4.2+5 +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: @@ -21,14 +23,10 @@ dependencies: sdk: flutter dev_dependencies: - test: ^1.3.0 + test: ^1.16.0 flutter_test: sdk: flutter integration_test: - path: ../integration_test - mockito: ^4.1.1 - pedantic: ^1.8.0 - -environment: - sdk: ">=2.1.0<3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + 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 index 832a2f8524b7..659c658c3604 100644 --- a/packages/sensors/test/sensors_test.dart +++ b/packages/sensors/test/sensors_test.dart @@ -1,13 +1,12 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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' show TestWidgetsFlutterBinding; +import 'package:flutter_test/flutter_test.dart'; import 'package:sensors/sensors.dart'; -import 'package:test/test.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -52,16 +51,16 @@ void main() { void _initializeFakeSensorChannel(String channelName, List sensorData) { const StandardMethodCodec standardMethod = StandardMethodCodec(); - void _emitEvent(ByteData event) { - ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage( + void _emitEvent(ByteData? event) { + ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage( channelName, event, - (ByteData reply) {}, + (ByteData? reply) {}, ); } - ServicesBinding.instance.defaultBinaryMessenger - .setMockMessageHandler(channelName, (ByteData message) async { + ServicesBinding.instance!.defaultBinaryMessenger + .setMockMessageHandler(channelName, (ByteData? message) async { final MethodCall methodCall = standardMethod.decodeMethodCall(message); if (methodCall.method == 'listen') { _emitEvent(standardMethod.encodeSuccessEnvelope(sensorData)); diff --git a/packages/share/AUTHORS b/packages/share/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/share/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/share/CHANGELOG.md b/packages/share/CHANGELOG.md index f13e819b8b74..a5e45110ebeb 100644 --- a/packages/share/CHANGELOG.md +++ b/packages/share/CHANGELOG.md @@ -1,3 +1,39 @@ +## 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. diff --git a/packages/share/LICENSE b/packages/share/LICENSE index 447867e0637e..c6823b81eb84 100644 --- a/packages/share/LICENSE +++ b/packages/share/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017, the Flutter project authors. All rights reserved. +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: diff --git a/packages/share/README.md b/packages/share/README.md index 750fca6a5b18..7fda1198f503 100644 --- a/packages/share/README.md +++ b/packages/share/README.md @@ -1,6 +1,21 @@ # Share plugin -[![pub package](https://img.shields.io/pub/v/share.svg)](https://pub.dartlang.org/packages/share) +--- + +## 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. @@ -8,16 +23,9 @@ share dialog. Wraps the ACTION_SEND Intent on Android and UIActivityViewController on iOS. -**Please set your constraint to `share: '>=0.6.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.6.y+z`. -Please use `share: '>=0.6.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ## Usage -To use this plugin, add `share` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). +To use this plugin, add `share` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/packages-and-plugins/using-packages/). ## Example @@ -44,4 +52,4 @@ To share one or multiple files invoke the static `shareFiles` method anywhere in ``` dart Share.shareFiles(['${directory.path}/image.jpg'], text: 'Great picture'); Share.shareFiles(['${directory.path}/image1.jpg', '${directory.path}/image2.jpg']); -``` \ No newline at end of file +``` diff --git a/packages/share/analysis_options.yaml b/packages/share/analysis_options.yaml new file mode 100644 index 000000000000..cda4f6e153e6 --- /dev/null +++ b/packages/share/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options_legacy.yaml diff --git a/packages/share/android/build.gradle b/packages/share/android/build.gradle index ffa1432fa17a..231aaa653f2b 100644 --- a/packages/share/android/build.gradle +++ b/packages/share/android/build.gradle @@ -1,33 +1,28 @@ group 'io.flutter.plugins.share' version '1.0-SNAPSHOT' -def args = ["-Xlint:deprecation","-Xlint:unchecked","-Werror"] buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:3.5.0' } } rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } -project.getTasks().withType(JavaCompile){ - options.compilerArgs.addAll(args) -} - apply plugin: 'com.android.library' android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { minSdkVersion 16 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 index 99baabeab6b7..7f162e883c32 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter 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/share/android/src/main/java/io/flutter/plugins/share/Share.java b/packages/share/android/src/main/java/io/flutter/plugins/share/Share.java index eb856bf572ee..fced7bb7f87c 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -10,7 +10,6 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; -import android.os.Environment; import androidx.annotation.NonNull; import androidx.core.content.FileProvider; import java.io.File; @@ -166,7 +165,7 @@ private String getMimeTypeBase(String mimeType) { private boolean fileIsOnExternal(File file) { try { String filePath = file.getCanonicalPath(); - File externalDir = Environment.getExternalStorageDirectory(); + File externalDir = context.getExternalFilesDir(null); return externalDir != null && filePath.startsWith(externalDir.getCanonicalPath()); } catch (IOException e) { return false; 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 index 87e4e42a03d4..fff48a6bad14 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter 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/share/android/src/main/java/io/flutter/plugins/share/SharePlugin.java b/packages/share/android/src/main/java/io/flutter/plugins/share/SharePlugin.java index af4a02eaba47..c596b8b71555 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -45,7 +45,7 @@ public void onAttachedToActivity(ActivityPluginBinding binding) { @Override public void onDetachedFromActivity() { - tearDownChannel(); + share.setActivity(null); } @Override @@ -64,9 +64,4 @@ private void setUpChannel(Context context, Activity activity, BinaryMessenger me handler = new MethodCallHandler(share); methodChannel.setMethodCallHandler(handler); } - - private void tearDownChannel() { - share.setActivity(null); - methodChannel.setMethodCallHandler(null); - } } diff --git a/packages/share/example/README.md b/packages/share/example/README.md index 189be05e46af..4081c8a5c9c3 100644 --- a/packages/share/example/README.md +++ b/packages/share/example/README.md @@ -5,4 +5,4 @@ Demonstrates how to use the share plugin. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). diff --git a/packages/share/example/android/app/build.gradle b/packages/share/example/android/app/build.gradle index 5fca10f77210..5b7b30bbad26 100644 --- a/packages/share/example/android/app/build.gradle +++ b/packages/share/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 28 + compileSdkVersion 29 lintOptions { disable 'InvalidPackage' @@ -34,7 +34,7 @@ android { defaultConfig { applicationId "io.flutter.plugins.shareexample" minSdkVersion 16 - targetSdkVersion 28 + targetSdkVersion 29 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1Activity.java b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1Activity.java index d14e39781c2b..85f4efb208af 100644 --- a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1Activity.java +++ b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1Activity.java @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1ActivityTest.java b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1ActivityTest.java index 489b224a62ea..cbe6c064371d 100644 --- a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1ActivityTest.java +++ b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1ActivityTest.java @@ -1,3 +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. + package io.flutter.plugins.shareexample; import androidx.test.rule.ActivityTestRule; 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 index 3b73737f15b2..070749dcff20 100644 --- 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/share/example/android/build.gradle b/packages/share/example/android/build.gradle index 541636cc492a..456d020f6e2c 100644 --- a/packages/share/example/android/build.gradle +++ b/packages/share/example/android/build.gradle @@ -1,18 +1,18 @@ buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:3.5.0' } } allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/packages/share/example/integration_test/share_test.dart b/packages/share/example/integration_test/share_test.dart new file mode 100644 index 000000000000..54d553bbb5a0 --- /dev/null +++ b/packages/share/example/integration_test/share_test.dart @@ -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. + +// @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/Podfile b/packages/share/example/ios/Podfile new file mode 100644 index 000000000000..f7d6a5e68c3a --- /dev/null +++ b/packages/share/example/ios/Podfile @@ -0,0 +1,38 @@ +# 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/share/example/ios/Runner.xcodeproj/project.pbxproj b/packages/share/example/ios/Runner.xcodeproj/project.pbxproj index 730c0d437d27..16acd6a7b886 100644 --- a/packages/share/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/share/example/ios/Runner.xcodeproj/project.pbxproj @@ -10,10 +10,7 @@ 28918A213BCB94C5470742D8 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 85392794417D70A970945C83 /* libPods-Runner.a */; }; 2D9222511EC45DE6007564B0 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D9222501EC45DE6007564B0 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 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 */; }; @@ -21,6 +18,16 @@ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 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; @@ -28,8 +35,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -42,14 +47,15 @@ 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 = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; + 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 = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -59,12 +65,17 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 683426A82538D314009B194C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 28918A213BCB94C5470742D8 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -81,6 +92,15 @@ name = Pods; sourceTree = ""; }; + 683426AC2538D314009B194C /* RunnerUITests */ = { + isa = PBXGroup; + children = ( + 683426AD2538D314009B194C /* FLTShareExampleUITests.m */, + 683426AF2538D314009B194C /* Info.plist */, + ); + path = RunnerUITests; + sourceTree = ""; + }; 8CA31EF57239BF20619316D9 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -92,9 +112,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, @@ -107,6 +125,7 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, + 683426AC2538D314009B194C /* RunnerUITests */, 97C146EF1CF9000F007C117D /* Products */, 16DDF472245BCC3E62219493 /* Pods */, 8CA31EF57239BF20619316D9 /* Frameworks */, @@ -117,6 +136,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + 683426AB2538D314009B194C /* RunnerUITests.xctest */, ); name = Products; sourceTree = ""; @@ -148,6 +168,24 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 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" */; @@ -159,7 +197,6 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 12A149CFB1B2610A83692801 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -177,8 +214,13 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; + ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { + 683426AA2538D314009B194C = { + CreatedOnToolsVersion = 11.7; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; }; @@ -198,11 +240,19 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + 683426AA2538D314009B194C /* RunnerUITests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 683426A92538D314009B194C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -217,21 +267,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 12A149CFB1B2610A83692801 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -244,7 +279,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 5F8AC0B5B699C537B657C107 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; @@ -281,6 +316,14 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 683426A72538D314009B194C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 683426AE2538D314009B194C /* FLTShareExampleUITests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -293,6 +336,14 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 683426B12538D314009B194C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 683426B02538D314009B194C /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -313,9 +364,53 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 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; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -372,7 +467,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -437,7 +531,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.shareExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.shareExample; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -458,7 +552,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.shareExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.shareExample; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; @@ -466,6 +560,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 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 = ( diff --git a/packages/share/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/share/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 3bb3697ef41c..000000000000 --- a/packages/share/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/share/example/ios/Runner/AppDelegate.h b/packages/share/example/ios/Runner/AppDelegate.h index d9e18e990f2e..0681d288bb70 100644 --- a/packages/share/example/ios/Runner/AppDelegate.h +++ b/packages/share/example/ios/Runner/AppDelegate.h @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/share/example/ios/Runner/AppDelegate.m b/packages/share/example/ios/Runner/AppDelegate.m index a4b51c88eb60..b790a0a52635 100644 --- a/packages/share/example/ios/Runner/AppDelegate.m +++ b/packages/share/example/ios/Runner/AppDelegate.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/share/example/ios/Runner/main.m b/packages/share/example/ios/Runner/main.m index bec320c0bee0..f97b9ef5c8a1 100644 --- a/packages/share/example/ios/Runner/main.m +++ b/packages/share/example/ios/Runner/main.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/share/example/ios/RunnerUITests/FLTShareExampleUITests.m b/packages/share/example/ios/RunnerUITests/FLTShareExampleUITests.m new file mode 100644 index 000000000000..c099cb946b92 --- /dev/null +++ b/packages/share/example/ios/RunnerUITests/FLTShareExampleUITests.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 +#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/ios/RunnerUITests/Info.plist b/packages/share/example/ios/RunnerUITests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/share/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/share/example/lib/image_previews.dart b/packages/share/example/lib/image_previews.dart index 61ecec43bdc7..9b5b807c77c6 100644 --- a/packages/share/example/lib/image_previews.dart +++ b/packages/share/example/lib/image_previews.dart @@ -1,3 +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 'dart:io'; import 'package:flutter/material.dart'; @@ -9,11 +13,11 @@ class ImagePreviews extends StatelessWidget { final List imagePaths; /// Callback when an image should be removed - final Function(int) onDelete; + final Function(int)? onDelete; /// Creates a widget for preview of images. [imagePaths] can not be empty /// and all contained paths need to be non empty. - const ImagePreviews(this.imagePaths, {Key key, this.onDelete}) + const ImagePreviews(this.imagePaths, {Key? key, this.onDelete}) : super(key: key); @override @@ -26,7 +30,7 @@ class ImagePreviews extends StatelessWidget { for (int i = 0; i < imagePaths.length; i++) { imageWidgets.add(_ImagePreview( imagePaths[i], - onDelete: onDelete != null ? () => onDelete(i) : null, + onDelete: onDelete != null ? () => onDelete!(i) : null, )); } @@ -39,9 +43,9 @@ class ImagePreviews extends StatelessWidget { class _ImagePreview extends StatelessWidget { final String imagePath; - final VoidCallback onDelete; + final VoidCallback? onDelete; - const _ImagePreview(this.imagePath, {Key key, this.onDelete}) + const _ImagePreview(this.imagePath, {Key? key, this.onDelete}) : super(key: key); @override diff --git a/packages/share/example/lib/main.dart b/packages/share/example/lib/main.dart index d6f1a1622b3c..f802b492df6d 100644 --- a/packages/share/example/lib/main.dart +++ b/packages/share/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -78,7 +78,7 @@ class DemoAppState extends State { const Padding(padding: EdgeInsets.only(top: 12.0)), Builder( builder: (BuildContext context) { - return RaisedButton( + return ElevatedButton( child: const Text('Share'), onPressed: text.isEmpty && imagePaths.isEmpty ? null @@ -86,6 +86,15 @@ class DemoAppState extends State { ); }, ), + const Padding(padding: EdgeInsets.only(top: 12.0)), + Builder( + builder: (BuildContext context) { + return ElevatedButton( + child: const Text('Share With Empty Origin'), + onPressed: () => _onShareWithEmptyOrigin(context), + ); + }, + ), ], ), ), @@ -101,13 +110,13 @@ class DemoAppState extends State { _onShare(BuildContext context) async { // A builder is used to retrieve the context immediately - // surrounding the RaisedButton. + // surrounding the ElevatedButton. // // The context's `findRenderObject` returns the first // RenderObject in its descendent tree when it's not - // a RenderObjectWidget. The RaisedButton's RenderObject + // a RenderObjectWidget. The ElevatedButton's RenderObject // has its position and size after it's built. - final RenderBox box = context.findRenderObject(); + final RenderBox box = context.findRenderObject() as RenderBox; if (imagePaths.isNotEmpty) { await Share.shareFiles(imagePaths, @@ -120,4 +129,8 @@ class DemoAppState extends State { 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 index 8b8623910b7a..8a28b43d46e4 100644 --- a/packages/share/example/pubspec.yaml +++ b/packages/share/example/pubspec.yaml @@ -1,23 +1,29 @@ 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.6.7+4 + image_picker: ^0.7.0 dev_dependencies: flutter_driver: sdk: flutter integration_test: - path: ../../integration_test - pedantic: ^1.8.0 + sdk: flutter + pedantic: ^1.10.0 flutter: uses-material-design: true - -environment: - sdk: ">=2.0.0-dev.28.0 <3.0.0" - flutter: ">=1.9.1+hotfix.2 <2.0.0" diff --git a/packages/share/example/test_driver/integration_test.dart b/packages/share/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..6a0e6fa82dbe --- /dev/null +++ b/packages/share/example/test_driver/integration_test.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. + +// @dart=2.9 + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/share/example/test_driver/test/integration_test.dart b/packages/share/example/test_driver/test/integration_test.dart deleted file mode 100644 index 7a2c21338786..000000000000 --- a/packages/share/example/test_driver/test/integration_test.dart +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; -import 'package:flutter_driver/flutter_driver.dart'; - -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String data = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - await driver.close(); - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} diff --git a/packages/share/integration_test/share_test.dart b/packages/share/integration_test/share_test.dart deleted file mode 100644 index 08a19ce2c27b..000000000000 --- a/packages/share/integration_test/share_test.dart +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -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/ios/Classes/FLTSharePlugin.h b/packages/share/ios/Classes/FLTSharePlugin.h index b06f1d0be606..8f6a6a538e2a 100644 --- a/packages/share/ios/Classes/FLTSharePlugin.h +++ b/packages/share/ios/Classes/FLTSharePlugin.h @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter 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/share/ios/Classes/FLTSharePlugin.m b/packages/share/ios/Classes/FLTSharePlugin.m index 837623a0119a..b8a3a7ffa316 100644 --- a/packages/share/ios/Classes/FLTSharePlugin.m +++ b/packages/share/ios/Classes/FLTSharePlugin.m @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -168,9 +168,8 @@ + (void)share:(NSArray *)shareItems UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:shareItems applicationActivities:nil]; activityViewController.popoverPresentationController.sourceView = controller.view; - if (!CGRectIsEmpty(origin)) { - activityViewController.popoverPresentationController.sourceRect = origin; - } + activityViewController.popoverPresentationController.sourceRect = origin; + [controller presentViewController:activityViewController animated:YES completion:nil]; } diff --git a/packages/share/ios/share.podspec b/packages/share/ios/share.podspec index 73d6030c68cb..786e1c7fb922 100644 --- a/packages/share/ios/share.podspec +++ b/packages/share/ios/share.podspec @@ -17,7 +17,6 @@ Downloaded by pub (not CocoaPods). 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 index 4a3ff6f1de09..6a3f5a317e31 100644 --- a/packages/share/lib/share.dart +++ b/packages/share/lib/share.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -33,8 +33,8 @@ class Share { /// from [MethodChannel]. static Future share( String text, { - String subject, - Rect sharePositionOrigin, + String? subject, + Rect? sharePositionOrigin, }) { assert(text != null); assert(text.isNotEmpty); @@ -67,10 +67,10 @@ class Share { /// from [MethodChannel]. static Future shareFiles( List paths, { - List mimeTypes, - String subject, - String text, - Rect sharePositionOrigin, + List? mimeTypes, + String? subject, + String? text, + Rect? sharePositionOrigin, }) { assert(paths != null); assert(paths.isNotEmpty); diff --git a/packages/share/pubspec.yaml b/packages/share/pubspec.yaml index dc55ed765453..4735995fff8a 100644 --- a/packages/share/pubspec.yaml +++ b/packages/share/pubspec.yaml @@ -1,11 +1,13 @@ name: share description: Flutter plugin for sharing content via the platform share UI, using the ACTION_SEND intent on Android and UIActivityViewController on iOS. -homepage: https://github.com/flutter/plugins/tree/master/packages/share -# 0.6.y+z is compatible with 1.0.0, if you land a breaking change bump -# the version to 2.0.0. -# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.6.5+2 +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: @@ -17,20 +19,14 @@ flutter: pluginClass: FLTSharePlugin dependencies: - meta: ^1.0.5 - mime: ^0.9.7 + meta: ^1.3.0 + mime: ^1.0.0 flutter: sdk: flutter dev_dependencies: - test: ^1.3.0 - mockito: ^3.0.0 flutter_test: sdk: flutter integration_test: - path: ../integration_test - pedantic: ^1.8.0 - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: flutter + pedantic: ^1.10.0 diff --git a/packages/share/test/share_test.dart b/packages/share/test/share_test.dart index e862d1baf579..d0049cef94ab 100644 --- a/packages/share/test/share_test.dart +++ b/packages/share/test/share_test.dart @@ -1,45 +1,34 @@ -// Copyright 2019 The Flutter Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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_test/flutter_test.dart' show TestWidgetsFlutterBinding; -import 'package:mockito/mockito.dart'; -import 'package:share/share.dart'; -import 'package:test/test.dart'; - import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:share/share.dart'; void main() { - TestWidgetsFlutterBinding.ensureInitialized(); + TestWidgetsFlutterBinding.ensureInitialized(); // Required for MethodChannels - MockMethodChannel mockChannel; + late FakeMethodChannel fakeChannel; setUp(() { - mockChannel = MockMethodChannel(); - // Re-pipe to mockito for easier verifies. + 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 mockChannel.invokeMethod(call.method, call.arguments); + await fakeChannel.invokeMethod(call.method, call.arguments); }); }); - test('sharing null fails', () { - expect( - () => Share.share(null), - throwsA(const TypeMatcher()), - ); - verifyZeroInteractions(mockChannel); - }); - test('sharing empty fails', () { expect( () => Share.share(''), - throwsA(const TypeMatcher()), + throwsA(isA()), ); - verifyZeroInteractions(mockChannel); + expect(fakeChannel.invocation, isNull); }); test('sharing origin sets the right params', () async { @@ -48,30 +37,28 @@ void main() { subject: 'some subject to share', sharePositionOrigin: const Rect.fromLTWH(1.0, 2.0, 3.0, 4.0), ); - verify(mockChannel.invokeMethod('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 null file fails', () { expect( - () => Share.shareFiles([null]), - throwsA(const TypeMatcher()), + 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, + } + }), ); - verifyZeroInteractions(mockChannel); }); test('sharing empty file fails', () { expect( () => Share.shareFiles(['']), - throwsA(const TypeMatcher()), + throwsA(isA()), ); - verifyZeroInteractions(mockChannel); + expect(fakeChannel.invocation, isNull); }); test('sharing file sets correct mimeType', () async { @@ -79,11 +66,18 @@ void main() { final File file = File(path); try { file.createSync(); + await Share.shareFiles([path]); - verify(mockChannel.invokeMethod('shareFiles', { - 'paths': [path], - 'mimeTypes': ['image/png'], - })); + + expect( + fakeChannel.invocation, + equals({ + 'shareFiles': { + 'paths': [path], + 'mimeTypes': ['image/png'], + } + }), + ); } finally { file.deleteSync(); } @@ -94,15 +88,30 @@ void main() { final File file = File(path); try { file.createSync(); + await Share.shareFiles([path], mimeTypes: ['*/*']); - verify(mockChannel.invokeMethod('shareFiles', { - 'paths': [file.path], - 'mimeTypes': ['*/*'], - })); + + expect( + fakeChannel.invocation, + equals({ + 'shareFiles': { + 'paths': [file.path], + 'mimeTypes': ['*/*'], + } + }), + ); } finally { file.deleteSync(); } }); } -class MockMethodChannel extends Mock implements MethodChannel {} +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 new file mode 100644 index 000000000000..cda4f6e153e6 --- /dev/null +++ b/packages/shared_preferences/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options_legacy.yaml diff --git a/packages/shared_preferences/shared_preferences/AUTHORS b/packages/shared_preferences/shared_preferences/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/shared_preferences/shared_preferences/CHANGELOG.md b/packages/shared_preferences/shared_preferences/CHANGELOG.md index 139df388ec73..3476f4eff3f0 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,3 +1,67 @@ +## NEXT + +* Add iOS unit test target. + +## 2.0.6 + +* Migrate maven repository from jcenter to mavenCentral. + +## 2.0.5 + +* Fix missing declaration of windows' default_package + +## 2.0.4 + +* Fix a regression with simultaneous writes on Android. + +## 2.0.3 + +* Android: don't create additional Handler when method channel is called. + +## 2.0.2 + +* Don't create additional thread pools when method channel is called. + +## 2.0.1 + +* Removed deprecated [AsyncTask](https://developer.android.com/reference/android/os/AsyncTask) was deprecated in API level 30 ([#3481](https://github.com/flutter/plugins/pull/3481)) + +## 2.0.0 + +* Migrate to null-safety. + +**Breaking changes**: + +* Setters no longer accept null to mean removing values. If you were previously using `set*(key, null)` for removing, use `remove(key)` instead. + +## 0.5.13+2 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 0.5.13+1 + +* Update Flutter SDK constraint. + +## 0.5.13 + +* Update integration test examples to use `testWidgets` instead of `test`. + +## 0.5.12+4 + +* Remove unused `test` dependency. + +## 0.5.12+3 + +* Check in windows/ directory for example/ + +## 0.5.12+2 + +* Update android compileSdkVersion to 29. + +## 0.5.12+1 + +* Check in linux/ directory for example/ + ## 0.5.12 * Keep handling deprecated Android v1 classes for backward compatibility. diff --git a/packages/shared_preferences/shared_preferences/LICENSE b/packages/shared_preferences/shared_preferences/LICENSE index a6d6c0749818..c6823b81eb84 100644 --- a/packages/shared_preferences/shared_preferences/LICENSE +++ b/packages/shared_preferences/shared_preferences/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017 The Chromium Authors. All rights reserved. +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: diff --git a/packages/shared_preferences/shared_preferences/README.md b/packages/shared_preferences/shared_preferences/README.md index 9ccac0ac49ae..e51ddea1c890 100644 --- a/packages/shared_preferences/shared_preferences/README.md +++ b/packages/shared_preferences/shared_preferences/README.md @@ -1,22 +1,14 @@ # Shared preferences plugin -[![pub package](https://img.shields.io/pub/v/shared_preferences.svg)](https://pub.dartlang.org/packages/shared_preferences) +[![pub package](https://img.shields.io/pub/v/shared_preferences.svg)](https://pub.dev/packages/shared_preferences) Wraps platform-specific persistent storage for simple data (NSUserDefaults on iOS and macOS, SharedPreferences on Android, etc.). Data may be persisted to disk asynchronously, and there is no guarantee that writes will be persisted to disk after returning, so this plugin must not be used for storing critical data. - -**Please set your constraint to `shared_preferences: '>=0.5.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.5.y+z`. -Please use `shared_preferences: '>=0.5.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ## Usage -To use this plugin, add `shared_preferences` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). +To use this plugin, add `shared_preferences` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). ### Example diff --git a/packages/shared_preferences/shared_preferences/android/build.gradle b/packages/shared_preferences/shared_preferences/android/build.gradle index 8ba16e9dd900..4d2336436022 100644 --- a/packages/shared_preferences/shared_preferences/android/build.gradle +++ b/packages/shared_preferences/shared_preferences/android/build.gradle @@ -4,7 +4,7 @@ version '1.0-SNAPSHOT' buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -15,7 +15,7 @@ buildscript { rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } @@ -30,7 +30,7 @@ allprojects { apply plugin: 'com.android.library' android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { minSdkVersion 16 diff --git a/packages/shared_preferences/shared_preferences/android/gradle/wrapper/gradle-wrapper.properties b/packages/shared_preferences/shared_preferences/android/gradle/wrapper/gradle-wrapper.properties index caf54fa2801c..3c9d0852bfa5 100644 --- a/packages/shared_preferences/shared_preferences/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/shared_preferences/shared_preferences/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java b/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java index 33f2474592fa..71ec14e7d06b 100644 --- a/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java +++ b/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/MethodCallHandlerImpl.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -6,7 +6,8 @@ import android.content.Context; import android.content.SharedPreferences; -import android.os.AsyncTask; +import android.os.Handler; +import android.os.Looper; import android.util.Base64; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; @@ -21,6 +22,10 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; /** * Implementation of the {@link MethodChannel.MethodCallHandler} for the plugin. It is also @@ -38,12 +43,18 @@ class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { private final android.content.SharedPreferences preferences; + private final ExecutorService executor; + private final Handler handler; + /** * Constructs a {@link MethodCallHandlerImpl} instance. Creates a {@link * android.content.SharedPreferences} based on the {@code context}. */ MethodCallHandlerImpl(Context context) { preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE); + executor = + new ThreadPoolExecutor(0, 1, 30L, TimeUnit.SECONDS, new LinkedBlockingQueue()); + handler = new Handler(Looper.getMainLooper()); } @Override @@ -116,19 +127,27 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { } } + public void teardown() { + handler.removeCallbacksAndMessages(null); + executor.shutdown(); + } + private void commitAsync( final SharedPreferences.Editor editor, final MethodChannel.Result result) { - new AsyncTask() { - @Override - protected Boolean doInBackground(Void... voids) { - return editor.commit(); - } - - @Override - protected void onPostExecute(Boolean value) { - result.success(value); - } - }.execute(); + executor.execute( + new Runnable() { + @Override + public void run() { + final boolean response = editor.commit(); + handler.post( + new Runnable() { + @Override + public void run() { + result.success(response); + } + }); + } + }); } private List decodeList(String encodedList) throws IOException { diff --git a/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java b/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java index be627f3ce613..d41328ee6202 100644 --- a/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java +++ b/packages/shared_preferences/shared_preferences/android/src/main/java/io/flutter/plugins/sharedpreferences/SharedPreferencesPlugin.java @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -13,6 +13,7 @@ public class SharedPreferencesPlugin implements FlutterPlugin { private static final String CHANNEL_NAME = "plugins.flutter.io/shared_preferences"; private MethodChannel channel; + private MethodCallHandlerImpl handler; @SuppressWarnings("deprecation") public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { @@ -32,11 +33,13 @@ public void onDetachedFromEngine(FlutterPlugin.FlutterPluginBinding binding) { private void setupChannel(BinaryMessenger messenger, Context context) { channel = new MethodChannel(messenger, CHANNEL_NAME); - MethodCallHandlerImpl handler = new MethodCallHandlerImpl(context); + handler = new MethodCallHandlerImpl(context); channel.setMethodCallHandler(handler); } private void teardownChannel() { + handler.teardown(); + handler = null; channel.setMethodCallHandler(null); channel = null; } diff --git a/packages/shared_preferences/shared_preferences/example/.gitignore b/packages/shared_preferences/shared_preferences/example/.gitignore new file mode 100644 index 000000000000..0fa6b675c0a5 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/.gitignore @@ -0,0 +1,46 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/shared_preferences/shared_preferences/example/.metadata b/packages/shared_preferences/shared_preferences/example/.metadata new file mode 100644 index 000000000000..e0e9530fccc9 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 79b49b9e1057f90ebf797725233c6b311722de69 + channel: dev + +project_type: app diff --git a/packages/shared_preferences/shared_preferences/example/README.md b/packages/shared_preferences/shared_preferences/example/README.md index 9d3bf1faf406..7dd9e9c4aa42 100644 --- a/packages/shared_preferences/shared_preferences/example/README.md +++ b/packages/shared_preferences/shared_preferences/example/README.md @@ -5,4 +5,4 @@ Demonstrates how to use the shared_preferences plugin. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). diff --git a/packages/shared_preferences/shared_preferences/example/android/.gitignore b/packages/shared_preferences/shared_preferences/example/android/.gitignore new file mode 100644 index 000000000000..0a741cb43d66 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/android/.gitignore @@ -0,0 +1,11 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties diff --git a/packages/shared_preferences/shared_preferences/example/android/app/build.gradle b/packages/shared_preferences/shared_preferences/example/android/app/build.gradle index 7a285ba704ab..3b7ee369beee 100644 --- a/packages/shared_preferences/shared_preferences/example/android/app/build.gradle +++ b/packages/shared_preferences/shared_preferences/example/android/app/build.gradle @@ -22,22 +22,23 @@ if (flutterVersionName == null) { } apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 28 + compileSdkVersion 30 - lintOptions { - disable 'InvalidPackage' + sourceSets { + main.java.srcDirs += 'src/main/kotlin' } defaultConfig { - applicationId "io.flutter.plugins.sharedpreferencesexample" + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "io.flutter.plugins.example" minSdkVersion 16 - targetSdkVersion 28 + targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { @@ -54,7 +55,5 @@ flutter { } dependencies { - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" } diff --git a/packages/shared_preferences/shared_preferences/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/shared_preferences/shared_preferences/example/android/app/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index ee69dd68d1a6..000000000000 --- a/packages/shared_preferences/shared_preferences/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/debug/AndroidManifest.xml b/packages/shared_preferences/shared_preferences/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..2d5b32857609 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/main/AndroidManifest.xml b/packages/shared_preferences/shared_preferences/example/android/app/src/main/AndroidManifest.xml index 04a642a4c748..2a12ff8e0009 100644 --- a/packages/shared_preferences/shared_preferences/example/android/app/src/main/AndroidManifest.xml +++ b/packages/shared_preferences/shared_preferences/example/android/app/src/main/AndroidManifest.xml @@ -1,26 +1,41 @@ - - - - - - - + + + + + + - + + diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1Activity.java b/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1Activity.java deleted file mode 100644 index 3857aea6ce6b..000000000000 --- a/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1Activity.java +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.sharedpreferencesexample; - -import android.os.Bundle; -import dev.flutter.plugins.integration_test.IntegrationTestPlugin; -import io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin; - -@SuppressWarnings("deprecation") -public class EmbeddingV1Activity extends io.flutter.app.FlutterActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - IntegrationTestPlugin.registerWith( - registrarFor("dev.flutter.plugins.integration_test.IntegrationTestPlugin")); - SharedPreferencesPlugin.registerWith( - registrarFor("io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin")); - } -} diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1ActivityTest.java b/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1ActivityTest.java deleted file mode 100644 index 5b090d0f52c2..000000000000 --- a/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,15 +0,0 @@ - -package io.flutter.plugins.sharedpreferencesexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -@SuppressWarnings("deprecation") -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/FlutterActivityTest.java b/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/FlutterActivityTest.java deleted file mode 100644 index 7a63d6d90c91..000000000000 --- a/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/FlutterActivityTest.java +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package io.flutter.plugins.sharedpreferencesexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.integration_test.FlutterTestRunner; -import io.flutter.embedding.android.FlutterActivity; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -public class FlutterActivityTest { - @Rule - public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); -} diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/main/kotlin/io/flutter/plugins/example/MainActivity.kt b/packages/shared_preferences/shared_preferences/example/android/app/src/main/kotlin/io/flutter/plugins/example/MainActivity.kt new file mode 100644 index 000000000000..9059dae9e4c4 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/android/app/src/main/kotlin/io/flutter/plugins/example/MainActivity.kt @@ -0,0 +1,6 @@ +package io.flutter.plugins.example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/drawable-v21/launch_background.xml b/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 000000000000..f74085f3f6a2 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/drawable/launch_background.xml b/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000000..304732f88420 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/values-night/styles.xml b/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 000000000000..449a9f930826 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/values/styles.xml b/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000000..d74aa35c2826 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/profile/AndroidManifest.xml b/packages/shared_preferences/shared_preferences/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 000000000000..2d5b32857609 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/packages/shared_preferences/shared_preferences/example/android/build.gradle b/packages/shared_preferences/shared_preferences/example/android/build.gradle index 54cc96612793..24047dce5d43 100644 --- a/packages/shared_preferences/shared_preferences/example/android/build.gradle +++ b/packages/shared_preferences/shared_preferences/example/android/build.gradle @@ -1,18 +1,20 @@ buildscript { + ext.kotlin_version = '1.3.50' repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.0' + classpath 'com.android.tools.build:gradle:4.1.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/packages/shared_preferences/shared_preferences/example/android/gradle.properties b/packages/shared_preferences/shared_preferences/example/android/gradle.properties index a6738207fd15..94adc3a3f97a 100644 --- a/packages/shared_preferences/shared_preferences/example/android/gradle.properties +++ b/packages/shared_preferences/shared_preferences/example/android/gradle.properties @@ -1,4 +1,3 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true -android.enableR8=true diff --git a/packages/shared_preferences/shared_preferences/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/shared_preferences/shared_preferences/example/android/gradle/wrapper/gradle-wrapper.properties index d757f3d33fcc..bc6a58afdda2 100644 --- a/packages/shared_preferences/shared_preferences/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/shared_preferences/shared_preferences/example/android/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Fri Jun 23 08:50:38 CEST 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/packages/shared_preferences/shared_preferences/example/android/settings.gradle b/packages/shared_preferences/shared_preferences/example/android/settings.gradle index 115da6cb4f4d..44e62bcf06ae 100644 --- a/packages/shared_preferences/shared_preferences/example/android/settings.gradle +++ b/packages/shared_preferences/shared_preferences/example/android/settings.gradle @@ -1,15 +1,11 @@ include ':app' -def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withInputStream { stream -> plugins.load(stream) } -} +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory -} +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart index 0d49ed95dd2d..1d46ed5751b0 100644 --- a/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart @@ -1,3 +1,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 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -23,7 +27,7 @@ void main() { 'flutter.List': ['baz', 'quox'], }; - SharedPreferences preferences; + late SharedPreferences preferences; setUp(() async { preferences = await SharedPreferences.getInstance(); @@ -33,7 +37,7 @@ void main() { preferences.clear(); }); - test('reading', () async { + testWidgets('reading', (WidgetTester _) async { expect(preferences.get('String'), isNull); expect(preferences.get('bool'), isNull); expect(preferences.get('int'), isNull); @@ -46,7 +50,7 @@ void main() { expect(preferences.getStringList('List'), isNull); }); - test('writing', () async { + testWidgets('writing', (WidgetTester _) async { await Future.wait(>[ preferences.setString('String', kTestValues2['flutter.String']), preferences.setBool('bool', kTestValues2['flutter.bool']), @@ -61,7 +65,7 @@ void main() { expect(preferences.getStringList('List'), kTestValues2['flutter.List']); }); - test('removing', () async { + testWidgets('removing', (WidgetTester _) async { const String key = 'testKey'; await preferences.setString(key, kTestValues['flutter.String']); await preferences.setBool(key, kTestValues['flutter.bool']); @@ -72,7 +76,7 @@ void main() { expect(preferences.get('testKey'), isNull); }); - test('clearing', () async { + testWidgets('clearing', (WidgetTester _) async { await preferences.setString('String', kTestValues['flutter.String']); await preferences.setBool('bool', kTestValues['flutter.bool']); await preferences.setInt('int', kTestValues['flutter.int']); @@ -85,5 +89,18 @@ void main() { expect(preferences.getDouble('double'), null); expect(preferences.getStringList('List'), null); }); + + testWidgets('simultaneous writes', (WidgetTester _) async { + final List> writes = >[]; + final int writeCount = 100; + for (int i = 1; i <= writeCount; i++) { + writes.add(preferences.setInt('int', i)); + } + List result = await Future.wait(writes, eagerError: true); + // All writes should succeed. + expect(result.where((element) => !element), isEmpty); + // The last write should win. + expect(preferences.getInt('int'), writeCount); + }); }); } diff --git a/packages/espresso/example/ios/.gitignore b/packages/shared_preferences/shared_preferences/example/ios/.gitignore similarity index 100% rename from packages/espresso/example/ios/.gitignore rename to packages/shared_preferences/shared_preferences/example/ios/.gitignore diff --git a/packages/shared_preferences/shared_preferences/example/ios/Flutter/Debug.xcconfig b/packages/shared_preferences/shared_preferences/example/ios/Flutter/Debug.xcconfig index 9803018ca79d..d0eccdcaf401 100644 --- a/packages/shared_preferences/shared_preferences/example/ios/Flutter/Debug.xcconfig +++ b/packages/shared_preferences/shared_preferences/example/ios/Flutter/Debug.xcconfig @@ -1,2 +1,3 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" diff --git a/packages/shared_preferences/shared_preferences/example/ios/Flutter/Release.xcconfig b/packages/shared_preferences/shared_preferences/example/ios/Flutter/Release.xcconfig index a4a8c604e13d..c751c1d022fa 100644 --- a/packages/shared_preferences/shared_preferences/example/ios/Flutter/Release.xcconfig +++ b/packages/shared_preferences/shared_preferences/example/ios/Flutter/Release.xcconfig @@ -1,2 +1,3 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" diff --git a/packages/shared_preferences/shared_preferences/example/ios/Podfile b/packages/shared_preferences/shared_preferences/example/ios/Podfile new file mode 100644 index 000000000000..3924e59aa0f9 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/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/example/ios/Runner.xcodeproj/project.pbxproj b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.pbxproj index 2adc7021c6bf..395e3009aa37 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,18 +9,26 @@ /* 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 */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 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 */; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 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; @@ -28,8 +36,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -42,20 +48,24 @@ 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 = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; 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 = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 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 */ @@ -63,12 +73,18 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 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 */ @@ -77,6 +93,8 @@ children = ( 942E815CEF30E101E045B849 /* Pods-Runner.debug.xcconfig */, 081A3238A89B77A99B096D83 /* Pods-Runner.release.xcconfig */, + A2FC4F1DC78D7C01312F877F /* Pods-RunnerTests.debug.xcconfig */, + D896CE48B6CC2EB7D42CB6B6 /* Pods-RunnerTests.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -84,9 +102,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, @@ -99,6 +115,7 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, + F76AC2072669B6AE0040C8BC /* RunnerTests */, 97C146EF1CF9000F007C117D /* Products */, 840012C8B5EDBCF56B0E4AC1 /* Pods */, CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, @@ -109,6 +126,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + F76AC2062669B6AE0040C8BC /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -141,10 +159,20 @@ 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 */ @@ -158,7 +186,6 @@ 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, - 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, ); buildRules = ( @@ -170,6 +197,25 @@ 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 */ @@ -177,11 +223,16 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; + 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" */; @@ -198,6 +249,7 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + F76AC2052669B6AE0040C8BC /* RunnerTests */, ); }; /* End PBXProject section */ @@ -214,6 +266,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + F76AC2042669B6AE0040C8BC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -229,49 +288,56 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "[CP] Embed Pods Frameworks"; + name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - 9740EEB61CF901F6004384FC /* Run Script */ = { + AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); - name = "Run Script"; + name = "[CP] Check Pods Manifest.lock"; 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\" build"; + 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; }; - AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */ = { + 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-Runner-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -291,8 +357,24 @@ ); 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; @@ -315,7 +397,6 @@ /* Begin XCBuildConfiguration section */ 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -372,7 +453,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -437,7 +517,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.sharedPreferencesExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.sharedPreferencesExample; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -458,11 +538,39 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.sharedPreferencesExample; + 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 */ @@ -484,6 +592,15 @@ 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.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 3bb3697ef41c..5e29b432c48c 100644 --- a/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -37,6 +37,16 @@ + + + + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/shared_preferences/shared_preferences/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/shared_preferences/shared_preferences/example/ios/Runner/AppDelegate.h b/packages/shared_preferences/shared_preferences/example/ios/Runner/AppDelegate.h index d9e18e990f2e..0681d288bb70 100644 --- a/packages/shared_preferences/shared_preferences/example/ios/Runner/AppDelegate.h +++ b/packages/shared_preferences/shared_preferences/example/ios/Runner/AppDelegate.h @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/shared_preferences/shared_preferences/example/ios/Runner/AppDelegate.m b/packages/shared_preferences/shared_preferences/example/ios/Runner/AppDelegate.m index a4b51c88eb60..b790a0a52635 100644 --- a/packages/shared_preferences/shared_preferences/example/ios/Runner/AppDelegate.m +++ b/packages/shared_preferences/shared_preferences/example/ios/Runner/AppDelegate.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/shared_preferences/shared_preferences/example/ios/Runner/AppDelegate.swift b/packages/shared_preferences/shared_preferences/example/ios/Runner/AppDelegate.swift new file mode 100644 index 000000000000..caf998393333 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/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/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png similarity index 100% rename from packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to packages/shared_preferences/shared_preferences/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/packages/shared_preferences/shared_preferences/example/ios/Runner/Runner-Bridging-Header.h b/packages/shared_preferences/shared_preferences/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 000000000000..eb7e8ba8052f --- /dev/null +++ b/packages/shared_preferences/shared_preferences/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/example/ios/Runner/main.m b/packages/shared_preferences/shared_preferences/example/ios/Runner/main.m index bec320c0bee0..f97b9ef5c8a1 100644 --- a/packages/shared_preferences/shared_preferences/example/ios/Runner/main.m +++ b/packages/shared_preferences/shared_preferences/example/ios/Runner/main.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/shared_preferences/shared_preferences/example/ios/RunnerTests/Info.plist b/packages/shared_preferences/shared_preferences/example/ios/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/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/shared_preferences/shared_preferences/example/ios/RunnerTests/SharedPreferencesTests.m b/packages/shared_preferences/shared_preferences/example/ios/RunnerTests/SharedPreferencesTests.m new file mode 100644 index 000000000000..08116fc38ee7 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/ios/RunnerTests/SharedPreferencesTests.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 shared_preferences; +@import XCTest; + +@interface SharedPreferencesTests : XCTestCase +@end + +@implementation SharedPreferencesTests + +- (void)testPlugin { + FLTSharedPreferencesPlugin* plugin = [[FLTSharedPreferencesPlugin alloc] init]; + XCTAssertNotNil(plugin); +} + +@end diff --git a/packages/shared_preferences/shared_preferences/example/lib/main.dart b/packages/shared_preferences/shared_preferences/example/lib/main.dart index 46daeff6706f..103481a2d295 100644 --- a/packages/shared_preferences/shared_preferences/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -24,7 +24,7 @@ class MyApp extends StatelessWidget { } class SharedPreferencesDemo extends StatefulWidget { - SharedPreferencesDemo({Key key}) : super(key: key); + SharedPreferencesDemo({Key? key}) : super(key: key); @override SharedPreferencesDemoState createState() => SharedPreferencesDemoState(); @@ -32,7 +32,7 @@ class SharedPreferencesDemo extends StatefulWidget { class SharedPreferencesDemoState extends State { Future _prefs = SharedPreferences.getInstance(); - Future _counter; + late Future _counter; Future _incrementCounter() async { final SharedPreferences prefs = await _prefs; diff --git a/packages/shared_preferences/shared_preferences/example/linux/.gitignore b/packages/shared_preferences/shared_preferences/example/linux/.gitignore new file mode 100644 index 000000000000..d3896c98444f --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/packages/shared_preferences/shared_preferences/example/linux/CMakeLists.txt b/packages/shared_preferences/shared_preferences/example/linux/CMakeLists.txt new file mode 100644 index 000000000000..79f729164ee3 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/linux/CMakeLists.txt @@ -0,0 +1,106 @@ +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +set(BINARY_NAME "example") +set(APPLICATION_ID "dev.flutter.plugins.shared_preferences_example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") +endif() + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_14) + target_compile_options(${TARGET} PRIVATE -Wall -Werror) + target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") + target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) + +add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") + +# Application build +add_executable(${BINARY_NAME} + "main.cc" + "my_application.cc" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" +) +apply_standard_settings(${BINARY_NAME}) +target_link_libraries(${BINARY_NAME} PRIVATE flutter) +target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) +add_dependencies(${BINARY_NAME} flutter_assemble) +# Only the install-generated bundle's copy of the executable will launch +# correctly, since the resources must in the right relative locations. To avoid +# people trying to run the unbundled copy, put it in a subdirectory instead of +# the default top-level location. +set_target_properties(${BINARY_NAME} + PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" +) + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# By default, "installing" just makes a relocatable bundle in the build +# directory. +set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +# Start with a clean build bundle directory every time. +install(CODE " + file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") + " COMPONENT Runtime) + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") + install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() diff --git a/packages/shared_preferences/shared_preferences/example/linux/flutter/CMakeLists.txt b/packages/shared_preferences/shared_preferences/example/linux/flutter/CMakeLists.txt new file mode 100644 index 000000000000..4f48a7ced5f4 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,88 @@ +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) +pkg_check_modules(BLKID REQUIRED IMPORTED_TARGET blkid) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO + PkgConfig::BLKID +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + linux-x64 ${CMAKE_BUILD_TYPE} +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) 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 new file mode 100644 index 000000000000..e71a16d23d05 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// 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 new file mode 100644 index 000000000000..e0f0a47bc08f --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// 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/linux/flutter/generated_plugins.cmake b/packages/shared_preferences/shared_preferences/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..51436ae8c982 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,15 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) diff --git a/packages/shared_preferences/shared_preferences/example/linux/main.cc b/packages/shared_preferences/shared_preferences/example/linux/main.cc new file mode 100644 index 000000000000..88a5fd45ce1b --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/linux/main.cc @@ -0,0 +1,15 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "my_application.h" + +int main(int argc, char** argv) { + // Only X11 is currently supported. + // Wayland support is being developed: + // https://github.com/flutter/flutter/issues/57932. + gdk_set_allowed_backends("x11"); + + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/packages/shared_preferences/shared_preferences/example/linux/my_application.cc b/packages/shared_preferences/shared_preferences/example/linux/my_application.cc new file mode 100644 index 000000000000..9cb411ba475b --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/linux/my_application.cc @@ -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. + +#include "my_application.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +struct _MyApplication { + GtkApplication parent_instance; +}; + +G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) + +// Implements GApplication::activate. +static void my_application_activate(GApplication* application) { + GtkWindow* window = + GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); + GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); + gtk_widget_show(GTK_WIDGET(header_bar)); + gtk_header_bar_set_title(header_bar, "example"); + gtk_header_bar_set_show_close_button(header_bar, TRUE); + gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); + gtk_window_set_default_size(window, 1280, 720); + gtk_widget_show(GTK_WIDGET(window)); + + g_autoptr(FlDartProject) project = fl_dart_project_new(); + + FlView* view = fl_view_new(project); + gtk_widget_show(GTK_WIDGET(view)); + gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); + + fl_register_plugins(FL_PLUGIN_REGISTRY(view)); + + gtk_widget_grab_focus(GTK_WIDGET(view)); +} + +static void my_application_class_init(MyApplicationClass* klass) { + G_APPLICATION_CLASS(klass)->activate = my_application_activate; +} + +static void my_application_init(MyApplication* self) {} + +MyApplication* my_application_new() { + return MY_APPLICATION(g_object_new( + my_application_get_type(), "application-id", APPLICATION_ID, nullptr)); +} diff --git a/packages/shared_preferences/shared_preferences/example/linux/my_application.h b/packages/shared_preferences/shared_preferences/example/linux/my_application.h new file mode 100644 index 000000000000..6e9f0c3ff665 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/linux/my_application.h @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_MY_APPLICATION_H_ +#define FLUTTER_MY_APPLICATION_H_ + +#include + +G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, + GtkApplication) + +/** + * my_application_new: + * + * Creates a new Flutter-based application. + * + * Returns: a new #MyApplication. + */ +MyApplication* my_application_new(); + +#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/packages/shared_preferences/shared_preferences/example/macos/.gitignore b/packages/shared_preferences/shared_preferences/example/macos/.gitignore new file mode 100644 index 000000000000..d2fd3772308c --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/macos/.gitignore @@ -0,0 +1,6 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/xcuserdata/ diff --git a/packages/shared_preferences/shared_preferences/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/shared_preferences/shared_preferences/example/macos/Flutter/Flutter-Debug.xcconfig index 785633d3a86b..4b81f9b2d200 100644 --- a/packages/shared_preferences/shared_preferences/example/macos/Flutter/Flutter-Debug.xcconfig +++ b/packages/shared_preferences/shared_preferences/example/macos/Flutter/Flutter-Debug.xcconfig @@ -1,2 +1,2 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/shared_preferences/shared_preferences/example/macos/Flutter/Flutter-Release.xcconfig b/packages/shared_preferences/shared_preferences/example/macos/Flutter/Flutter-Release.xcconfig index 5fba960c3af2..5caa9d1579e4 100644 --- a/packages/shared_preferences/shared_preferences/example/macos/Flutter/Flutter-Release.xcconfig +++ b/packages/shared_preferences/shared_preferences/example/macos/Flutter/Flutter-Release.xcconfig @@ -1,2 +1,2 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/packages/shared_preferences/shared_preferences/example/macos/Podfile b/packages/shared_preferences/shared_preferences/example/macos/Podfile new file mode 100644 index 000000000000..dade8dfad0dc --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.11' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/project.pbxproj b/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/project.pbxproj index 0e2413493f6e..cc89c8782812 100644 --- a/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/project.pbxproj @@ -26,11 +26,6 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; }; - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; }; - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */ = {isa = PBXBuildFile; fileRef = D73912EF22F37F9E000D13A0 /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - EA473EC5F2038B17A2FE4D78 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 748ADDF1719804343BB18004 /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -50,8 +45,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */, - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */, ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; @@ -61,7 +54,7 @@ /* Begin PBXFileReference section */ 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* connectivity_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = connectivity_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "example.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -70,17 +63,11 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = FlutterMacOS.framework; path = Flutter/ephemeral/FlutterMacOS.framework; sourceTree = SOURCE_ROOT; }; 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 748ADDF1719804343BB18004 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 80418F0A2F74D683C63A4D0A /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - AA19B00394637215A825CF5E /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - D73912EF22F37F9E000D13A0 /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/ephemeral/App.framework; sourceTree = SOURCE_ROOT; }; - E960ED3977AF6DF197F74FFA /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -88,9 +75,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */, - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */, - EA473EC5F2038B17A2FE4D78 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -115,14 +99,13 @@ 33CEB47122A05771004F2AC0 /* Flutter */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, - D42EAEE5849744148CC78D83 /* Pods */, ); sourceTree = ""; }; 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* connectivity_example.app */, + 33CC10ED2044A3C60003C045 /* example.app */, ); name = Products; sourceTree = ""; @@ -145,8 +128,6 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - D73912EF22F37F9E000D13A0 /* App.framework */, - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */, ); path = Flutter; sourceTree = ""; @@ -164,21 +145,9 @@ path = Runner; sourceTree = ""; }; - D42EAEE5849744148CC78D83 /* Pods */ = { - isa = PBXGroup; - children = ( - 80418F0A2F74D683C63A4D0A /* Pods-Runner.debug.xcconfig */, - E960ED3977AF6DF197F74FFA /* Pods-Runner.release.xcconfig */, - AA19B00394637215A825CF5E /* Pods-Runner.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( - 748ADDF1719804343BB18004 /* Pods_Runner.framework */, ); name = Frameworks; sourceTree = ""; @@ -190,13 +159,11 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - B24477CAB9D5BDFC8F3553DA /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, - 84A8D21305B2F01D093A8F9C /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -205,7 +172,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* connectivity_example.app */; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -216,7 +183,7 @@ attributes = { LastSwiftUpdateCheck = 0920; LastUpgradeCheck = 0930; - ORGANIZATIONNAME = "Google LLC"; + ORGANIZATIONNAME = ""; TargetAttributes = { 33CC10EC2044A3C60003C045 = { CreatedOnToolsVersion = 9.2; @@ -235,7 +202,7 @@ }; }; buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 8.0"; + compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -281,7 +248,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename\n"; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; }; 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -301,44 +268,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh\ntouch Flutter/ephemeral/tripwire\n"; - }; - 84A8D21305B2F01D093A8F9C /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - B24477CAB9D5BDFC8F3553DA /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; }; /* End PBXShellScriptBuildPhase section */ @@ -431,10 +361,6 @@ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -561,10 +487,6 @@ CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -585,10 +507,6 @@ CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter/ephemeral", - ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", diff --git a/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 2a7d3e7f34ac..ae8ff59d97b3 100644 --- a/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/shared_preferences/shared_preferences/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -15,7 +15,7 @@ @@ -27,23 +27,11 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> - - - - - - @@ -66,7 +54,7 @@ @@ -75,7 +63,7 @@ diff --git a/packages/shared_preferences/shared_preferences/example/macos/Runner/AppDelegate.swift b/packages/shared_preferences/shared_preferences/example/macos/Runner/AppDelegate.swift index d53ef6437726..5cec4c48f620 100644 --- a/packages/shared_preferences/shared_preferences/example/macos/Runner/AppDelegate.swift +++ b/packages/shared_preferences/shared_preferences/example/macos/Runner/AppDelegate.swift @@ -1,3 +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 Cocoa import FlutterMacOS diff --git a/packages/shared_preferences/shared_preferences/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/shared_preferences/shared_preferences/example/macos/Runner/Configs/AppInfo.xcconfig index a95148814518..e82c4235dcf8 100644 --- a/packages/shared_preferences/shared_preferences/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/packages/shared_preferences/shared_preferences/example/macos/Runner/Configs/AppInfo.xcconfig @@ -5,10 +5,10 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = connectivity_example +PRODUCT_NAME = example // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.connectivityExample +PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.example // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2019 io.flutter.plugins. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2021 The Flutter Authors. All rights reserved. diff --git a/packages/shared_preferences/shared_preferences/example/macos/Runner/MainFlutterWindow.swift b/packages/shared_preferences/shared_preferences/example/macos/Runner/MainFlutterWindow.swift index 2722837ec918..32aaeedceb1f 100644 --- a/packages/shared_preferences/shared_preferences/example/macos/Runner/MainFlutterWindow.swift +++ b/packages/shared_preferences/shared_preferences/example/macos/Runner/MainFlutterWindow.swift @@ -1,3 +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 Cocoa import FlutterMacOS diff --git a/packages/shared_preferences/shared_preferences/example/pubspec.yaml b/packages/shared_preferences/shared_preferences/example/pubspec.yaml index 2b970949bee6..8f14d5c1ec5b 100644 --- a/packages/shared_preferences/shared_preferences/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/example/pubspec.yaml @@ -1,24 +1,28 @@ name: shared_preferences_example description: Demonstrates how to use the shared_preferences plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.9.1+hotfix.2" dependencies: flutter: sdk: flutter shared_preferences: + # When depending on this package from a real application you should use: + # shared_preferences: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: flutter_driver: sdk: flutter - test: any integration_test: - path: ../../../integration_test - pedantic: ^1.8.0 + sdk: flutter + pedantic: ^1.10.0 flutter: uses-material-design: true - -environment: - sdk: ">=2.0.0-dev.28.0 <3.0.0" - flutter: ">=1.9.1+hotfix.2 <2.0.0" - diff --git a/packages/shared_preferences/shared_preferences/example/test_driver/integration_test.dart b/packages/shared_preferences/shared_preferences/example/test_driver/integration_test.dart index 7a2c21338786..4f10f2a522f3 100644 --- a/packages/shared_preferences/shared_preferences/example/test_driver/integration_test.dart +++ b/packages/shared_preferences/shared_preferences/example/test_driver/integration_test.dart @@ -1,17 +1,7 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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'; +import 'package:integration_test/integration_test_driver.dart'; -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String data = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - await driver.close(); - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} +Future main() => integrationDriver(); diff --git a/packages/shared_preferences/shared_preferences/example/web/favicon.png b/packages/shared_preferences/shared_preferences/example/web/favicon.png new file mode 100644 index 000000000000..8aaa46ac1ae2 Binary files /dev/null and b/packages/shared_preferences/shared_preferences/example/web/favicon.png differ diff --git a/packages/shared_preferences/shared_preferences/example/web/icons/Icon-192.png b/packages/shared_preferences/shared_preferences/example/web/icons/Icon-192.png new file mode 100644 index 000000000000..b749bfef0747 Binary files /dev/null and b/packages/shared_preferences/shared_preferences/example/web/icons/Icon-192.png differ diff --git a/packages/shared_preferences/shared_preferences/example/web/icons/Icon-512.png b/packages/shared_preferences/shared_preferences/example/web/icons/Icon-512.png new file mode 100644 index 000000000000..88cfd48dff11 Binary files /dev/null and b/packages/shared_preferences/shared_preferences/example/web/icons/Icon-512.png differ diff --git a/packages/shared_preferences/shared_preferences/example/web/index.html b/packages/shared_preferences/shared_preferences/example/web/index.html index 6eff9a740d43..7fb138cc90fa 100644 --- a/packages/shared_preferences/shared_preferences/example/web/index.html +++ b/packages/shared_preferences/shared_preferences/example/web/index.html @@ -1,4 +1,7 @@ + diff --git a/packages/shared_preferences/shared_preferences/example/web/manifest.json b/packages/shared_preferences/shared_preferences/example/web/manifest.json new file mode 100644 index 000000000000..8c012917dab7 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/web/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/packages/shared_preferences/shared_preferences/example/windows/.gitignore b/packages/shared_preferences/shared_preferences/example/windows/.gitignore new file mode 100644 index 000000000000..d492d0d98c8f --- /dev/null +++ b/packages/shared_preferences/shared_preferences/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/shared_preferences/shared_preferences/example/windows/CMakeLists.txt b/packages/shared_preferences/shared_preferences/example/windows/CMakeLists.txt new file mode 100644 index 000000000000..abf90408efb4 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/windows/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.15) +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/shared_preferences/shared_preferences/example/windows/flutter/CMakeLists.txt b/packages/shared_preferences/shared_preferences/example/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000000..c7a8c7607d81 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,101 @@ +cmake_minimum_required(VERSION 3.15) + +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" +) +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 $ +) +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/shared_preferences/shared_preferences/example/windows/flutter/generated_plugin_registrant.cc b/packages/shared_preferences/shared_preferences/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000000..8b6d4680af38 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// 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 new file mode 100644 index 000000000000..dc139d85a931 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// 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/flutter/generated_plugins.cmake b/packages/shared_preferences/shared_preferences/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..4d10c2518654 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,15 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_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) diff --git a/packages/shared_preferences/shared_preferences/example/windows/runner/CMakeLists.txt b/packages/shared_preferences/shared_preferences/example/windows/runner/CMakeLists.txt new file mode 100644 index 000000000000..977e38b5d1d2 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/windows/runner/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.15) +project(runner LANGUAGES CXX) + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "run_loop.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/shared_preferences/shared_preferences/example/windows/runner/Runner.rc b/packages/shared_preferences/shared_preferences/example/windows/runner/Runner.rc new file mode 100644 index 000000000000..dbda44723259 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/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", "Flutter Dev" "\0" + VALUE "FileDescription", "A new Flutter project." "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2020 The Flutter Authors. 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/shared_preferences/shared_preferences/example/windows/runner/flutter_window.cpp b/packages/shared_preferences/shared_preferences/example/windows/runner/flutter_window.cpp new file mode 100644 index 000000000000..8e415602cf3b --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/windows/runner/flutter_window.cpp @@ -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. + +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(RunLoop* run_loop, + const flutter::DartProject& project) + : run_loop_(run_loop), 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()); + run_loop_->RegisterFlutterInstance(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + run_loop_->UnregisterFlutterInstance(flutter_controller_->engine()); + 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 opporutunity 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/shared_preferences/shared_preferences/example/windows/runner/flutter_window.h b/packages/shared_preferences/shared_preferences/example/windows/runner/flutter_window.h new file mode 100644 index 000000000000..8e9c12bbe022 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/windows/runner/flutter_window.h @@ -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. + +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "run_loop.h" +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow driven by the |run_loop|, hosting a + // Flutter view running |project|. + explicit FlutterWindow(RunLoop* run_loop, + 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 run loop driving events for this window. + RunLoop* run_loop_; + + // 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/shared_preferences/shared_preferences/example/windows/runner/main.cpp b/packages/shared_preferences/shared_preferences/example/windows/runner/main.cpp new file mode 100644 index 000000000000..126302b0be18 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/windows/runner/main.cpp @@ -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. + +#include +#include +#include + +#include "flutter_window.h" +#include "run_loop.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); + + RunLoop run_loop; + + flutter::DartProject project(L"data"); + FlutterWindow window(&run_loop, project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + run_loop.Run(); + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/packages/shared_preferences/shared_preferences/example/windows/runner/resource.h b/packages/shared_preferences/shared_preferences/example/windows/runner/resource.h new file mode 100644 index 000000000000..d5d958dc4257 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/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/shared_preferences/shared_preferences/example/windows/runner/resources/app_icon.ico b/packages/shared_preferences/shared_preferences/example/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000000..c04e20caf637 Binary files /dev/null and b/packages/shared_preferences/shared_preferences/example/windows/runner/resources/app_icon.ico differ diff --git a/packages/shared_preferences/shared_preferences/example/windows/runner/run_loop.cpp b/packages/shared_preferences/shared_preferences/example/windows/runner/run_loop.cpp new file mode 100644 index 000000000000..1916500e6440 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/windows/runner/run_loop.cpp @@ -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. + +#include "run_loop.h" + +#include + +#include + +RunLoop::RunLoop() {} + +RunLoop::~RunLoop() {} + +void RunLoop::Run() { + bool keep_running = true; + TimePoint next_flutter_event_time = TimePoint::clock::now(); + while (keep_running) { + std::chrono::nanoseconds wait_duration = + std::max(std::chrono::nanoseconds(0), + next_flutter_event_time - TimePoint::clock::now()); + ::MsgWaitForMultipleObjects( + 0, nullptr, FALSE, static_cast(wait_duration.count() / 1000), + QS_ALLINPUT); + bool processed_events = false; + MSG message; + // All pending Windows messages must be processed; MsgWaitForMultipleObjects + // won't return again for items left in the queue after PeekMessage. + while (::PeekMessage(&message, nullptr, 0, 0, PM_REMOVE)) { + processed_events = true; + if (message.message == WM_QUIT) { + keep_running = false; + break; + } + ::TranslateMessage(&message); + ::DispatchMessage(&message); + // Allow Flutter to process messages each time a Windows message is + // processed, to prevent starvation. + next_flutter_event_time = + std::min(next_flutter_event_time, ProcessFlutterMessages()); + } + // If the PeekMessage loop didn't run, process Flutter messages. + if (!processed_events) { + next_flutter_event_time = + std::min(next_flutter_event_time, ProcessFlutterMessages()); + } + } +} + +void RunLoop::RegisterFlutterInstance( + flutter::FlutterEngine* flutter_instance) { + flutter_instances_.insert(flutter_instance); +} + +void RunLoop::UnregisterFlutterInstance( + flutter::FlutterEngine* flutter_instance) { + flutter_instances_.erase(flutter_instance); +} + +RunLoop::TimePoint RunLoop::ProcessFlutterMessages() { + TimePoint next_event_time = TimePoint::max(); + for (auto instance : flutter_instances_) { + std::chrono::nanoseconds wait_duration = instance->ProcessMessages(); + if (wait_duration != std::chrono::nanoseconds::max()) { + next_event_time = + std::min(next_event_time, TimePoint::clock::now() + wait_duration); + } + } + return next_event_time; +} diff --git a/packages/shared_preferences/shared_preferences/example/windows/runner/run_loop.h b/packages/shared_preferences/shared_preferences/example/windows/runner/run_loop.h new file mode 100644 index 000000000000..819ed3ed4995 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/windows/runner/run_loop.h @@ -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. + +#ifndef RUNNER_RUN_LOOP_H_ +#define RUNNER_RUN_LOOP_H_ + +#include + +#include +#include + +// A runloop that will service events for Flutter instances as well +// as native messages. +class RunLoop { + public: + RunLoop(); + ~RunLoop(); + + // Prevent copying + RunLoop(RunLoop const&) = delete; + RunLoop& operator=(RunLoop const&) = delete; + + // Runs the run loop until the application quits. + void Run(); + + // Registers the given Flutter instance for event servicing. + void RegisterFlutterInstance(flutter::FlutterEngine* flutter_instance); + + // Unregisters the given Flutter instance from event servicing. + void UnregisterFlutterInstance(flutter::FlutterEngine* flutter_instance); + + private: + using TimePoint = std::chrono::steady_clock::time_point; + + // Processes all currently pending messages for registered Flutter instances. + TimePoint ProcessFlutterMessages(); + + std::set flutter_instances_; +}; + +#endif // RUNNER_RUN_LOOP_H_ diff --git a/packages/shared_preferences/shared_preferences/example/windows/runner/runner.exe.manifest b/packages/shared_preferences/shared_preferences/example/windows/runner/runner.exe.manifest new file mode 100644 index 000000000000..c977c4a42589 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/packages/shared_preferences/shared_preferences/example/windows/runner/utils.cpp b/packages/shared_preferences/shared_preferences/example/windows/runner/utils.cpp new file mode 100644 index 000000000000..537728149601 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/windows/runner/utils.cpp @@ -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. + +#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(); + } +} diff --git a/packages/shared_preferences/shared_preferences/example/windows/runner/utils.h b/packages/shared_preferences/shared_preferences/example/windows/runner/utils.h new file mode 100644 index 000000000000..16b3f0794597 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/windows/runner/utils.h @@ -0,0 +1,12 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/shared_preferences/shared_preferences/example/windows/runner/win32_window.cpp b/packages/shared_preferences/shared_preferences/example/windows/runner/win32_window.cpp new file mode 100644 index 000000000000..a609a2002bb3 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/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/shared_preferences/shared_preferences/example/windows/runner/win32_window.h b/packages/shared_preferences/shared_preferences/example/windows/runner/win32_window.h new file mode 100644 index 000000000000..d2a730052223 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/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/shared_preferences/shared_preferences/ios/Classes/FLTSharedPreferencesPlugin.h b/packages/shared_preferences/shared_preferences/ios/Classes/FLTSharedPreferencesPlugin.h index 6bb1d5eecbeb..d2d04aee3b64 100644 --- a/packages/shared_preferences/shared_preferences/ios/Classes/FLTSharedPreferencesPlugin.h +++ b/packages/shared_preferences/shared_preferences/ios/Classes/FLTSharedPreferencesPlugin.h @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/shared_preferences/shared_preferences/ios/Classes/FLTSharedPreferencesPlugin.m b/packages/shared_preferences/shared_preferences/ios/Classes/FLTSharedPreferencesPlugin.m index dd68fb5b98c4..09308d42d762 100644 --- a/packages/shared_preferences/shared_preferences/ios/Classes/FLTSharedPreferencesPlugin.m +++ b/packages/shared_preferences/shared_preferences/ios/Classes/FLTSharedPreferencesPlugin.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/shared_preferences/shared_preferences/lib/shared_preferences.dart b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart index c4c8710769df..3d2dd051f61c 100644 --- a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart +++ b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -7,10 +7,9 @@ import 'dart:io' show Platform; import 'package:flutter/foundation.dart' show kIsWeb; import 'package:meta/meta.dart'; - import 'package:shared_preferences_linux/shared_preferences_linux.dart'; -import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; import 'package:shared_preferences_platform_interface/method_channel_shared_preferences.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; import 'package:shared_preferences_windows/shared_preferences_windows.dart'; /// Wraps NSUserDefaults (on iOS) and SharedPreferences (on Android), providing @@ -21,13 +20,12 @@ class SharedPreferences { SharedPreferences._(this._preferenceCache); static const String _prefix = 'flutter.'; - static Completer _completer; + static Completer? _completer; static bool _manualDartRegistrationNeeded = true; static SharedPreferencesStorePlatform get _store { - // This is to manually endorse the Linux implementation until automatic - // registration of dart plugins is implemented. For details see - // https://github.com/flutter/flutter/issues/52267. + // 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. @@ -52,21 +50,22 @@ class SharedPreferences { /// performance-sensitive blocks. static Future getInstance() async { if (_completer == null) { - _completer = Completer(); + final completer = Completer(); try { final Map preferencesMap = await _getSharedPreferencesMap(); - _completer.complete(SharedPreferences._(preferencesMap)); + completer.complete(SharedPreferences._(preferencesMap)); } on Exception catch (e) { // If there's an error, explicitly return the future with an error. // then set the completer to null so we can retry. - _completer.completeError(e); - final Future sharedPrefsFuture = _completer.future; + completer.completeError(e); + final Future sharedPrefsFuture = completer.future; _completer = null; return sharedPrefsFuture; } + _completer = completer; } - return _completer.future; + return _completer!.future; } /// The cache that holds all preferences. @@ -83,86 +82,76 @@ class SharedPreferences { Set getKeys() => Set.from(_preferenceCache.keys); /// Reads a value of any type from persistent storage. - dynamic get(String key) => _preferenceCache[key]; + Object? get(String key) => _preferenceCache[key]; /// Reads a value from persistent storage, throwing an exception if it's not a /// bool. - bool getBool(String key) => _preferenceCache[key]; + bool? getBool(String key) => _preferenceCache[key] as bool?; /// Reads a value from persistent storage, throwing an exception if it's not /// an int. - int getInt(String key) => _preferenceCache[key]; + int? getInt(String key) => _preferenceCache[key] as int?; /// Reads a value from persistent storage, throwing an exception if it's not a /// double. - double getDouble(String key) => _preferenceCache[key]; + double? getDouble(String key) => _preferenceCache[key] as double?; /// Reads a value from persistent storage, throwing an exception if it's not a /// String. - String getString(String key) => _preferenceCache[key]; + String? getString(String key) => _preferenceCache[key] as String?; /// Returns true if persistent storage the contains the given [key]. bool containsKey(String key) => _preferenceCache.containsKey(key); /// Reads a set of string values from persistent storage, throwing an /// exception if it's not a string set. - List getStringList(String key) { - List list = _preferenceCache[key]; + List? getStringList(String key) { + List? list = _preferenceCache[key] as List?; if (list != null && list is! List) { list = list.cast().toList(); _preferenceCache[key] = list; } // Make a copy of the list so that later mutations won't propagate - return list?.toList(); + return list?.toList() as List?; } /// Saves a boolean [value] to persistent storage in the background. - /// - /// If [value] is null, this is equivalent to calling [remove()] on the [key]. Future setBool(String key, bool value) => _setValue('Bool', key, value); /// Saves an integer [value] to persistent storage in the background. - /// - /// If [value] is null, this is equivalent to calling [remove()] on the [key]. Future setInt(String key, int value) => _setValue('Int', key, value); /// Saves a double [value] to persistent storage in the background. /// /// Android doesn't support storing doubles, so it will be stored as a float. - /// - /// If [value] is null, this is equivalent to calling [remove()] on the [key]. Future setDouble(String key, double value) => _setValue('Double', key, value); /// Saves a string [value] to persistent storage in the background. - /// - /// If [value] is null, this is equivalent to calling [remove()] on the [key]. Future setString(String key, String value) => _setValue('String', key, value); /// Saves a list of strings [value] to persistent storage in the background. - /// - /// If [value] is null, this is equivalent to calling [remove()] on the [key]. Future setStringList(String key, List value) => _setValue('StringList', key, value); /// Removes an entry from persistent storage. - Future remove(String key) => _setValue(null, key, null); + Future remove(String key) { + final String prefixedKey = '$_prefix$key'; + _preferenceCache.remove(key); + return _store.remove(prefixedKey); + } Future _setValue(String valueType, String key, Object value) { + ArgumentError.checkNotNull(value, 'value'); final String prefixedKey = '$_prefix$key'; - if (value == null) { - _preferenceCache.remove(key); - return _store.remove(prefixedKey); + if (value is List) { + // Make a copy of the list so that later mutations won't propagate + _preferenceCache[key] = value.toList(); } else { - if (value is List) { - // Make a copy of the list so that later mutations won't propagate - _preferenceCache[key] = value.toList(); - } else { - _preferenceCache[key] = value; - } - return _store.setValue(valueType, prefixedKey, value); + _preferenceCache[key] = value; } + return _store.setValue(valueType, prefixedKey, value); } /// Always returns true. @@ -194,7 +183,7 @@ class SharedPreferences { final Map preferencesMap = {}; for (String key in fromSystem.keys) { assert(key.startsWith(_prefix)); - preferencesMap[key.substring(_prefix.length)] = fromSystem[key]; + preferencesMap[key.substring(_prefix.length)] = fromSystem[key]!; } return preferencesMap; } @@ -203,14 +192,14 @@ class SharedPreferences { /// /// If the singleton instance has been initialized already, it is nullified. @visibleForTesting - static void setMockInitialValues(Map values) { - final Map newValues = - values.map((String key, dynamic value) { + static void setMockInitialValues(Map values) { + final Map newValues = + values.map((String key, Object value) { String newKey = key; if (!key.startsWith(_prefix)) { newKey = '$_prefix$key'; } - return MapEntry(newKey, value); + return MapEntry(newKey, value); }); SharedPreferencesStorePlatform.instance = InMemorySharedPreferencesStore.withData(newValues); diff --git a/packages/shared_preferences/shared_preferences/macos/shared_preferences.podspec b/packages/shared_preferences/shared_preferences/macos/shared_preferences.podspec deleted file mode 100644 index 5eeb3df11b23..000000000000 --- a/packages/shared_preferences/shared_preferences/macos/shared_preferences.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 = 'shared_preferences' - s.version = '0.0.1' - s.summary = 'No-op implementation of the macos shared_preferences to avoid build issues on macos' - s.description = <<-DESC - No-op implementation of the shared_preferences plugin to avoid build issues on macos. - https://github.com/flutter/flutter/issues/46618 - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/shared_preferences' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - - s.platform = :osx - s.osx.deployment_target = '10.11' -end - diff --git a/packages/shared_preferences/shared_preferences/pubspec.yaml b/packages/shared_preferences/shared_preferences/pubspec.yaml index 91404eced74d..c3039a98588b 100644 --- a/packages/shared_preferences/shared_preferences/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/pubspec.yaml @@ -1,11 +1,13 @@ name: shared_preferences description: Flutter plugin for reading and writing simple key-value pairs. Wraps NSUserDefaults on iOS and SharedPreferences on Android. -homepage: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences -# 0.5.y+z is compatible with 1.0.0, if you land a breaking change bump -# the version to 2.0.0. -# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.5.12 +repository: https://github.com/flutter/plugins/tree/master/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.6 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: @@ -21,32 +23,29 @@ flutter: default_package: shared_preferences_macos web: default_package: shared_preferences_web + windows: + default_package: shared_preferences_windows dependencies: - meta: ^1.0.4 flutter: sdk: flutter - shared_preferences_platform_interface: ^1.0.0 + meta: ^1.3.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 + # 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). + # TODO(franciscojma): Revisit this (either update this part in the design or the pub tool). # https://github.com/flutter/flutter/issues/46264 - shared_preferences_linux: ^0.0.2 - shared_preferences_macos: ^0.0.1 - shared_preferences_web: ^0.1.2 - shared_preferences_windows: ^0.0.1 + shared_preferences_linux: ^2.0.0 + shared_preferences_macos: ^2.0.0 + shared_preferences_platform_interface: ^2.0.0 + shared_preferences_web: ^2.0.0 + shared_preferences_windows: ^2.0.0 dev_dependencies: - flutter_test: - sdk: flutter flutter_driver: sdk: flutter - test: any + flutter_test: + sdk: flutter integration_test: - path: ../../integration_test - pedantic: ^1.8.0 - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: flutter + pedantic: ^1.10.0 diff --git a/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart index b219774d1992..878021a03361 100755 --- a/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences/test/shared_preferences_test.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -11,7 +11,7 @@ void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('SharedPreferences', () { - const Map kTestValues = { + const Map kTestValues = { 'flutter.String': 'hello world', 'flutter.bool': true, 'flutter.int': 42, @@ -27,8 +27,8 @@ void main() { 'flutter.List': ['baz', 'quox'], }; - FakeSharedPreferencesStore store; - SharedPreferences preferences; + late FakeSharedPreferencesStore store; + late SharedPreferences preferences; setUp(() async { store = FakeSharedPreferencesStore(kTestValues); @@ -39,6 +39,7 @@ void main() { tearDown(() async { await preferences.clear(); + await store.clear(); }); test('reading', () async { @@ -105,16 +106,11 @@ void main() { test('removing', () async { const String key = 'testKey'; - await preferences.setString(key, null); - await preferences.setBool(key, null); - await preferences.setInt(key, null); - await preferences.setDouble(key, null); - await preferences.setStringList(key, null); await preferences.remove(key); expect( store.log, List.filled( - 6, + 1, isMethodCall( 'remove', arguments: 'flutter.$key', @@ -143,10 +139,12 @@ void main() { }); test('reloading', () async { - await preferences.setString('String', kTestValues['flutter.String']); + await preferences.setString( + 'String', kTestValues['flutter.String'] as String); expect(preferences.getString('String'), kTestValues['flutter.String']); - SharedPreferences.setMockInitialValues(kTestValues2); + SharedPreferences.setMockInitialValues( + kTestValues2.cast()); expect(preferences.getString('String'), kTestValues['flutter.String']); await preferences.reload(); @@ -159,23 +157,32 @@ void main() { expect(await first, await second); }); + test('string list type is dynamic (usually from method channel)', () async { + SharedPreferences.setMockInitialValues({ + 'dynamic_list': ['1', '2'] + }); + final SharedPreferences prefs = await SharedPreferences.getInstance(); + final List? value = prefs.getStringList('dynamic_list'); + expect(value, ['1', '2']); + }); + group('mocking', () { const String _key = 'dummy'; const String _prefixedKey = 'flutter.' + _key; test('test 1', () async { SharedPreferences.setMockInitialValues( - {_prefixedKey: 'my string'}); + {_prefixedKey: 'my string'}); final SharedPreferences prefs = await SharedPreferences.getInstance(); - final String value = prefs.getString(_key); + final String? value = prefs.getString(_key); expect(value, 'my string'); }); test('test 2', () async { SharedPreferences.setMockInitialValues( - {_prefixedKey: 'my other string'}); + {_prefixedKey: 'my other string'}); final SharedPreferences prefs = await SharedPreferences.getInstance(); - final String value = prefs.getString(_key); + final String? value = prefs.getString(_key); expect(value, 'my other string'); }); }); @@ -185,7 +192,7 @@ void main() { await preferences.setStringList("myList", myList); myList.add("foobar"); - final List cachedList = preferences.getStringList('myList'); + final List cachedList = preferences.getStringList('myList')!; expect(cachedList, []); cachedList.add("foobar2"); @@ -195,11 +202,11 @@ void main() { }); test('calling mock initial values with non-prefixed keys succeeds', () async { - SharedPreferences.setMockInitialValues({ + SharedPreferences.setMockInitialValues({ 'test': 'foo', }); final SharedPreferences prefs = await SharedPreferences.getInstance(); - final String value = prefs.getString('test'); + final String? value = prefs.getString('test'); expect(value, 'foo'); }); } diff --git a/packages/shared_preferences/shared_preferences_linux/AUTHORS b/packages/shared_preferences/shared_preferences_linux/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_linux/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md index 5bfd6651edfa..9a17d2455ad8 100644 --- a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md @@ -1,3 +1,29 @@ +## 2.0.1 + +* Add `implements` to the pubspec. +* Add `registerWith` to the Dart main class. + +## 2.0.0 + +* Migrate to null-safety. + +## 0.0.3+1 + +* Update Flutter SDK constraint. + +## 0.0.3 + +* Update integration test examples to use `testWidgets` instead of `test`. + +## 0.0.2+4 + +* Remove unused `test` dependency. +* Update Dart SDK constraint in example. + +## 0.0.2+3 + +* Check in linux/ directory for example/ + ## 0.0.2+2 * Bump the `file` package dependency to resolve dep conflicts with `flutter_driver`. diff --git a/packages/shared_preferences/shared_preferences_linux/LICENSE b/packages/shared_preferences/shared_preferences_linux/LICENSE index d7412e0a1e0c..c6823b81eb84 100644 --- a/packages/shared_preferences/shared_preferences_linux/LICENSE +++ b/packages/shared_preferences/shared_preferences_linux/LICENSE @@ -1,4 +1,4 @@ -Copyright 2020 The Chromium Authors. All rights reserved. +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: diff --git a/packages/shared_preferences/shared_preferences_linux/example/README.md b/packages/shared_preferences/shared_preferences_linux/example/README.md index 9d3bf1faf406..7dd9e9c4aa42 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/README.md +++ b/packages/shared_preferences/shared_preferences_linux/example/README.md @@ -5,4 +5,4 @@ Demonstrates how to use the shared_preferences plugin. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[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 0d49ed95dd2d..5dba3def31d0 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences_linux/example/integration_test/shared_preferences_test.dart @@ -1,12 +1,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 'dart:async'; + import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:integration_test/integration_test.dart'; +import 'package:shared_preferences_linux/shared_preferences_linux.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('$SharedPreferences', () { + group('SharedPreferencesLinux', () { const Map kTestValues = { 'flutter.String': 'hello world', 'flutter.bool': true, @@ -23,67 +28,73 @@ void main() { 'flutter.List': ['baz', 'quox'], }; - SharedPreferences preferences; + late SharedPreferencesLinux preferences; setUp(() async { - preferences = await SharedPreferences.getInstance(); + preferences = SharedPreferencesLinux.instance; }); tearDown(() { preferences.clear(); }); - test('reading', () async { - expect(preferences.get('String'), isNull); - expect(preferences.get('bool'), isNull); - expect(preferences.get('int'), isNull); - expect(preferences.get('double'), isNull); - expect(preferences.get('List'), isNull); - expect(preferences.getString('String'), isNull); - expect(preferences.getBool('bool'), isNull); - expect(preferences.getInt('int'), isNull); - expect(preferences.getDouble('double'), isNull); - expect(preferences.getStringList('List'), isNull); + testWidgets('reading', (WidgetTester _) async { + final all = await preferences.getAll(); + expect(all['String'], isNull); + expect(all['bool'], isNull); + expect(all['int'], isNull); + expect(all['double'], isNull); + expect(all['List'], isNull); }); - test('writing', () async { + testWidgets('writing', (WidgetTester _) async { await Future.wait(>[ - preferences.setString('String', kTestValues2['flutter.String']), - preferences.setBool('bool', kTestValues2['flutter.bool']), - preferences.setInt('int', kTestValues2['flutter.int']), - preferences.setDouble('double', kTestValues2['flutter.double']), - preferences.setStringList('List', kTestValues2['flutter.List']) + preferences.setValue( + 'String', 'String', kTestValues2['flutter.String']), + preferences.setValue('Bool', 'bool', kTestValues2['flutter.bool']), + preferences.setValue('Int', 'int', kTestValues2['flutter.int']), + preferences.setValue( + 'Double', 'double', kTestValues2['flutter.double']), + preferences.setValue('StringList', 'List', kTestValues2['flutter.List']) ]); - expect(preferences.getString('String'), kTestValues2['flutter.String']); - expect(preferences.getBool('bool'), kTestValues2['flutter.bool']); - expect(preferences.getInt('int'), kTestValues2['flutter.int']); - expect(preferences.getDouble('double'), kTestValues2['flutter.double']); - expect(preferences.getStringList('List'), kTestValues2['flutter.List']); + final all = await preferences.getAll(); + expect(all['String'], kTestValues2['flutter.String']); + expect(all['bool'], kTestValues2['flutter.bool']); + expect(all['int'], kTestValues2['flutter.int']); + expect(all['double'], kTestValues2['flutter.double']); + expect(all['List'], kTestValues2['flutter.List']); }); - test('removing', () async { + testWidgets('removing', (WidgetTester _) async { const String key = 'testKey'; - await preferences.setString(key, kTestValues['flutter.String']); - await preferences.setBool(key, kTestValues['flutter.bool']); - await preferences.setInt(key, kTestValues['flutter.int']); - await preferences.setDouble(key, kTestValues['flutter.double']); - await preferences.setStringList(key, kTestValues['flutter.List']); + + await Future.wait([ + preferences.setValue('String', key, kTestValues['flutter.String']), + preferences.setValue('Bool', key, kTestValues['flutter.bool']), + preferences.setValue('Int', key, kTestValues['flutter.int']), + preferences.setValue('Double', key, kTestValues['flutter.double']), + preferences.setValue('StringList', key, kTestValues['flutter.List']) + ]); await preferences.remove(key); - expect(preferences.get('testKey'), isNull); + final all = await preferences.getAll(); + expect(all['testKey'], isNull); }); - test('clearing', () async { - await preferences.setString('String', kTestValues['flutter.String']); - await preferences.setBool('bool', kTestValues['flutter.bool']); - await preferences.setInt('int', kTestValues['flutter.int']); - await preferences.setDouble('double', kTestValues['flutter.double']); - await preferences.setStringList('List', kTestValues['flutter.List']); + testWidgets('clearing', (WidgetTester _) async { + await Future.wait(>[ + preferences.setValue('String', 'String', kTestValues['flutter.String']), + preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']), + preferences.setValue('Int', 'int', kTestValues['flutter.int']), + preferences.setValue('Double', 'double', kTestValues['flutter.double']), + preferences.setValue('StringList', 'List', kTestValues['flutter.List']) + ]); await preferences.clear(); - expect(preferences.getString('String'), null); - expect(preferences.getBool('bool'), null); - expect(preferences.getInt('int'), null); - expect(preferences.getDouble('double'), null); - expect(preferences.getStringList('List'), null); + final all = await preferences.getAll(); + expect(all['String'], null); + expect(all['bool'], null); + expect(all['int'], null); + expect(all['double'], null); + expect(all['List'], null); }); }); } diff --git a/packages/shared_preferences/shared_preferences_linux/example/lib/main.dart b/packages/shared_preferences/shared_preferences_linux/example/lib/main.dart index ceacf2f95f28..aee71d00d44d 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences_linux/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -24,7 +24,7 @@ class MyApp extends StatelessWidget { } class SharedPreferencesDemo extends StatefulWidget { - SharedPreferencesDemo({Key key}) : super(key: key); + SharedPreferencesDemo({Key? key}) : super(key: key); @override SharedPreferencesDemoState createState() => SharedPreferencesDemoState(); @@ -32,14 +32,14 @@ class SharedPreferencesDemo extends StatefulWidget { class SharedPreferencesDemoState extends State { final prefs = SharedPreferencesLinux.instance; - Future _counter; + late Future _counter; Future _incrementCounter() async { final values = await prefs.getAll(); - final int counter = (values['counter'] as int ?? 0) + 1; + final int counter = (values['counter'] as int? ?? 0) + 1; setState(() { - _counter = prefs.setValue(null, "counter", counter).then((bool success) { + _counter = prefs.setValue('Int', 'counter', counter).then((bool success) { return counter; }); }); @@ -49,7 +49,7 @@ class SharedPreferencesDemoState extends State { void initState() { super.initState(); _counter = prefs.getAll().then((Map values) { - return (values['counter'] ?? 0); + return (values['counter'] as int? ?? 0); }); } diff --git a/packages/shared_preferences/shared_preferences_linux/example/linux/.gitignore b/packages/shared_preferences/shared_preferences_linux/example/linux/.gitignore new file mode 100644 index 000000000000..d3896c98444f --- /dev/null +++ b/packages/shared_preferences/shared_preferences_linux/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral 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 index 890de29bbab1..e71a16d23d05 100644 --- 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 @@ -2,6 +2,10 @@ // Generated file. Do not edit. // +// clang-format off + #include "generated_plugin_registrant.h" -void fl_register_plugins(FlPluginRegistry* registry) {} + +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 index 9bf7478940c1..e0f0a47bc08f 100644 --- 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 @@ -2,6 +2,8 @@ // Generated file. Do not edit. // +// clang-format off + #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/shared_preferences/shared_preferences_linux/example/linux/main.cc b/packages/shared_preferences/shared_preferences_linux/example/linux/main.cc index e7c5c5437037..1507d02825e7 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/linux/main.cc +++ b/packages/shared_preferences/shared_preferences_linux/example/linux/main.cc @@ -1,3 +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. + #include "my_application.h" int main(int argc, char** argv) { diff --git a/packages/shared_preferences/shared_preferences_linux/example/linux/my_application.cc b/packages/shared_preferences/shared_preferences_linux/example/linux/my_application.cc index f079e19eb396..878cd973d997 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/linux/my_application.cc +++ b/packages/shared_preferences/shared_preferences_linux/example/linux/my_application.cc @@ -1,3 +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. + #include "my_application.h" #include diff --git a/packages/shared_preferences/shared_preferences_linux/example/linux/my_application.h b/packages/shared_preferences/shared_preferences_linux/example/linux/my_application.h index 72271d5e4170..6e9f0c3ff665 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/linux/my_application.h +++ b/packages/shared_preferences/shared_preferences_linux/example/linux/my_application.h @@ -1,3 +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. + #ifndef FLUTTER_MY_APPLICATION_H_ #define FLUTTER_MY_APPLICATION_H_ diff --git a/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml index 5936e3ace02f..d34973b9dde6 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml @@ -1,27 +1,28 @@ name: shared_preferences_linux_example description: Demonstrates how to use the shared_preferences_linux plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: sdk: flutter - shared_preferences: any - shared_preferences_linux: ^0.1.0 - -dependency_overrides: shared_preferences_linux: + # When depending on this package from a real application you should use: + # shared_preferences_linux: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ - # Remove this override once the endorsement is published. - shared_preferences: - path: ../../shared_preferences/ dev_dependencies: flutter_driver: sdk: flutter - test: any integration_test: - path: ../../../integration_test - pedantic: ^1.8.0 + sdk: flutter + pedantic: ^1.10.0 flutter: uses-material-design: true - diff --git a/packages/shared_preferences/shared_preferences_linux/example/test_driver/integration_test.dart b/packages/shared_preferences/shared_preferences_linux/example/test_driver/integration_test.dart index 7a2c21338786..4f10f2a522f3 100644 --- a/packages/shared_preferences/shared_preferences_linux/example/test_driver/integration_test.dart +++ b/packages/shared_preferences/shared_preferences_linux/example/test_driver/integration_test.dart @@ -1,17 +1,7 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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'; +import 'package:integration_test/integration_test_driver.dart'; -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String data = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - await driver.close(); - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} +Future main() => integrationDriver(); diff --git a/packages/shared_preferences/shared_preferences_linux/ios/shared_preferences_linux.podspec b/packages/shared_preferences/shared_preferences_linux/ios/shared_preferences_linux.podspec deleted file mode 100644 index 8f4d3cdddcd5..000000000000 --- a/packages/shared_preferences/shared_preferences_linux/ios/shared_preferences_linux.podspec +++ /dev/null @@ -1,22 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint shared_preferences_launcher_linux.podspec' to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'shared_preferences_linux' - s.version = '0.0.1' - s.summary = 'shared_preferences_linux iOS stub' - s.description = <<-DESC - No-op implementation of the Linux shared_preferences plugin to avoid build issues on iOS - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_linux' } - s.dependency 'Flutter' - s.platform = :ios, '8.0' - - # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } - s.swift_version = '5.0' -end diff --git a/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart b/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart index c975ad1a7544..5ec988216074 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 @@ -1,4 +1,4 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -17,19 +17,27 @@ 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 static SharedPreferencesLinux instance = SharedPreferencesLinux(); + /// Registers the Linux implementation. + static void registerWith() { + SharedPreferencesStorePlatform.instance = instance; + } + /// Local copy of preferences - Map _cachedPreferences; + Map? _cachedPreferences; /// File system used to store to disk. Exposed for testing only. @visibleForTesting FileSystem fs = LocalFileSystem(); /// Gets the file where the preferences are stored. - Future _getLocalDataFile() async { + Future _getLocalDataFile() async { final pathProvider = PathProviderLinux(); final directory = await pathProvider.getApplicationSupportPath(); + if (directory == null) return null; return fs.file(path.join(directory, 'shared_preferences.json')); } @@ -37,19 +45,19 @@ class SharedPreferencesLinux extends SharedPreferencesStorePlatform { /// maintained in memory. Future> _readPreferences() async { if (_cachedPreferences != null) { - return _cachedPreferences; + return _cachedPreferences!; } - _cachedPreferences = {}; - var localDataFile = await _getLocalDataFile(); - if (localDataFile.existsSync()) { + Map preferences = {}; + final File? localDataFile = await _getLocalDataFile(); + if (localDataFile != null && localDataFile.existsSync()) { String stringMap = localDataFile.readAsStringSync(); if (stringMap.isNotEmpty) { - _cachedPreferences = json.decode(stringMap) as Map; + preferences = json.decode(stringMap).cast(); } } - - return _cachedPreferences; + _cachedPreferences = preferences; + return preferences; } /// Writes the cached preferences to disk. Returns [true] if the operation @@ -57,6 +65,10 @@ class SharedPreferencesLinux extends SharedPreferencesStorePlatform { Future _writePreferences(Map preferences) async { try { var localDataFile = await _getLocalDataFile(); + if (localDataFile == null) { + print("Unable to determine where to write preferences."); + return false; + } if (!localDataFile.existsSync()) { localDataFile.createSync(recursive: true); } diff --git a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml index 8a19c46fd531..9bfe24dfa829 100644 --- a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml @@ -1,29 +1,31 @@ name: shared_preferences_linux description: Linux implementation of the shared_preferences plugin -version: 0.0.2+2 -homepage: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_linux +repository: https://github.com/flutter/plugins/tree/master/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.1 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: + implements: shared_preferences platforms: linux: dartPluginClass: SharedPreferencesLinux pluginClass: none -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" - dependencies: - file: ">=5.1.0 <7.0.0" + file: ^6.0.0 + meta: ^1.3.0 flutter: sdk: flutter - meta: ^1.0.4 - path: ^1.6.4 - path_provider_linux: ^0.0.1 - shared_preferences_platform_interface: ^1.0.0 + path: ^1.8.0 + path_provider_linux: ^2.0.0 + shared_preferences_platform_interface: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 diff --git a/packages/shared_preferences/shared_preferences_linux/test/shared_preferences_linux_test.dart b/packages/shared_preferences/shared_preferences_linux/test/shared_preferences_linux_test.dart index 8c659f212aa5..62ec2b66c07a 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 @@ -1,4 +1,4 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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/memory.dart'; @@ -6,20 +6,21 @@ 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:shared_preferences_linux/shared_preferences_linux.dart'; - -MemoryFileSystem fs; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; void main() { + late MemoryFileSystem fs; + + SharedPreferencesLinux.registerWith(); + setUp(() { fs = MemoryFileSystem.test(); }); - tearDown(() {}); - Future _getFilePath() async { final pathProvider = PathProviderLinux(); final directory = await pathProvider.getApplicationSupportPath(); - return path.join(directory, 'shared_preferences.json'); + return path.join(directory!, 'shared_preferences.json'); } _writeTestFile(String value) async { @@ -38,6 +39,11 @@ void main() { return prefs; } + test('registered instance', () { + expect( + SharedPreferencesStorePlatform.instance, isA()); + }); + test('getAll', () async { await _writeTestFile('{"key1": "one", "key2": 2}'); var prefs = _getPreferences(); diff --git a/packages/shared_preferences/shared_preferences_macos/AUTHORS b/packages/shared_preferences/shared_preferences_macos/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_macos/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md b/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md index ae79f0169326..d5ace31073ad 100644 --- a/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md @@ -1,3 +1,24 @@ +## NEXT + +* Add native unit tests. + +## 2.0.1 + +* Add `implements` to the pubspec. + +## 2.0.0 + +* Migrate to null safety. + +## 0.0.1+12 + +* Update Flutter SDK constraint. + +## 0.0.1+11 + +* Remove unused `test` dependency. +* Update Dart SDK constraint in example. + ## 0.0.1+10 * Remove iOS and Android folders from the example app. diff --git a/packages/shared_preferences/shared_preferences_macos/LICENSE b/packages/shared_preferences/shared_preferences_macos/LICENSE index 447867e0637e..c6823b81eb84 100644 --- a/packages/shared_preferences/shared_preferences_macos/LICENSE +++ b/packages/shared_preferences/shared_preferences_macos/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017, the Flutter project authors. All rights reserved. +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: diff --git a/packages/shared_preferences/shared_preferences_macos/README.md b/packages/shared_preferences/shared_preferences_macos/README.md index c2949c9f5d33..170a8270c402 100644 --- a/packages/shared_preferences/shared_preferences_macos/README.md +++ b/packages/shared_preferences/shared_preferences_macos/README.md @@ -2,13 +2,6 @@ The macos implementation of [`shared_preferences`][1]. -**Please set your constraint to `shared_preferences_macos: '>=0.0.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.0.y+z`. -Please use `shared_preferences_macos: '>=0.0.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ## Usage ### Import the package diff --git a/packages/shared_preferences/shared_preferences_macos/example/README.md b/packages/shared_preferences/shared_preferences_macos/example/README.md index 9d3bf1faf406..7dd9e9c4aa42 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/README.md +++ b/packages/shared_preferences/shared_preferences_macos/example/README.md @@ -5,4 +5,4 @@ Demonstrates how to use the shared_preferences plugin. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[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 0d49ed95dd2d..e7a267f7e51a 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/integration_test/shared_preferences_test.dart +++ b/packages/shared_preferences/shared_preferences_macos/example/integration_test/shared_preferences_test.dart @@ -1,12 +1,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 'dart:async'; import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; import 'package:integration_test/integration_test.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - group('$SharedPreferences', () { + group('SharedPreferencesMacOS', () { const Map kTestValues = { 'flutter.String': 'hello world', 'flutter.bool': true, @@ -23,67 +27,81 @@ void main() { 'flutter.List': ['baz', 'quox'], }; - SharedPreferences preferences; + late SharedPreferencesStorePlatform preferences; setUp(() async { - preferences = await SharedPreferences.getInstance(); + preferences = SharedPreferencesStorePlatform.instance; }); tearDown(() { preferences.clear(); }); - test('reading', () async { - expect(preferences.get('String'), isNull); - expect(preferences.get('bool'), isNull); - expect(preferences.get('int'), isNull); - expect(preferences.get('double'), isNull); - expect(preferences.get('List'), isNull); - expect(preferences.getString('String'), isNull); - expect(preferences.getBool('bool'), isNull); - expect(preferences.getInt('int'), isNull); - expect(preferences.getDouble('double'), isNull); - expect(preferences.getStringList('List'), isNull); + // Normally the app-facing package adds the prefix, but since this test + // bypasses the app-facing package it needs to be manually added. + String _prefixedKey(String key) { + return 'flutter.$key'; + } + + testWidgets('reading', (WidgetTester _) async { + final Map values = await preferences.getAll(); + expect(values[_prefixedKey('String')], isNull); + expect(values[_prefixedKey('bool')], isNull); + expect(values[_prefixedKey('int')], isNull); + expect(values[_prefixedKey('double')], isNull); + expect(values[_prefixedKey('List')], isNull); }); - test('writing', () async { + testWidgets('writing', (WidgetTester _) async { await Future.wait(>[ - preferences.setString('String', kTestValues2['flutter.String']), - preferences.setBool('bool', kTestValues2['flutter.bool']), - preferences.setInt('int', kTestValues2['flutter.int']), - preferences.setDouble('double', kTestValues2['flutter.double']), - preferences.setStringList('List', kTestValues2['flutter.List']) + preferences.setValue( + 'String', _prefixedKey('String'), kTestValues2['flutter.String']), + preferences.setValue( + 'Bool', _prefixedKey('bool'), kTestValues2['flutter.bool']), + preferences.setValue( + 'Int', _prefixedKey('int'), kTestValues2['flutter.int']), + preferences.setValue( + 'Double', _prefixedKey('double'), kTestValues2['flutter.double']), + preferences.setValue( + 'StringList', _prefixedKey('List'), kTestValues2['flutter.List']) ]); - expect(preferences.getString('String'), kTestValues2['flutter.String']); - expect(preferences.getBool('bool'), kTestValues2['flutter.bool']); - expect(preferences.getInt('int'), kTestValues2['flutter.int']); - expect(preferences.getDouble('double'), kTestValues2['flutter.double']); - expect(preferences.getStringList('List'), kTestValues2['flutter.List']); + final Map values = await preferences.getAll(); + expect(values[_prefixedKey('String')], kTestValues2['flutter.String']); + expect(values[_prefixedKey('bool')], kTestValues2['flutter.bool']); + expect(values[_prefixedKey('int')], kTestValues2['flutter.int']); + expect(values[_prefixedKey('double')], kTestValues2['flutter.double']); + expect(values[_prefixedKey('List')], kTestValues2['flutter.List']); }); - test('removing', () async { - const String key = 'testKey'; - await preferences.setString(key, kTestValues['flutter.String']); - await preferences.setBool(key, kTestValues['flutter.bool']); - await preferences.setInt(key, kTestValues['flutter.int']); - await preferences.setDouble(key, kTestValues['flutter.double']); - await preferences.setStringList(key, kTestValues['flutter.List']); + testWidgets('removing', (WidgetTester _) async { + final String key = _prefixedKey('testKey'); + await preferences.setValue('String', key, kTestValues['flutter.String']); + await preferences.setValue('Bool', key, kTestValues['flutter.bool']); + await preferences.setValue('Int', key, kTestValues['flutter.int']); + await preferences.setValue('Double', key, kTestValues['flutter.double']); + await preferences.setValue( + 'StringList', key, kTestValues['flutter.List']); await preferences.remove(key); - expect(preferences.get('testKey'), isNull); + final Map values = await preferences.getAll(); + expect(values[key], isNull); }); - test('clearing', () async { - await preferences.setString('String', kTestValues['flutter.String']); - await preferences.setBool('bool', kTestValues['flutter.bool']); - await preferences.setInt('int', kTestValues['flutter.int']); - await preferences.setDouble('double', kTestValues['flutter.double']); - await preferences.setStringList('List', kTestValues['flutter.List']); + testWidgets('clearing', (WidgetTester _) async { + await preferences.setValue( + 'String', 'String', kTestValues['flutter.String']); + await preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']); + await preferences.setValue('Int', 'int', kTestValues['flutter.int']); + await preferences.setValue( + 'Double', 'double', kTestValues['flutter.double']); + await preferences.setValue( + 'StringList', 'List', kTestValues['flutter.List']); await preferences.clear(); - expect(preferences.getString('String'), null); - expect(preferences.getBool('bool'), null); - expect(preferences.getInt('int'), null); - expect(preferences.getDouble('double'), null); - expect(preferences.getStringList('List'), null); + final Map values = await preferences.getAll(); + expect(values['String'], null); + expect(values['bool'], null); + expect(values['int'], null); + expect(values['double'], null); + expect(values['List'], null); }); }); } diff --git a/packages/shared_preferences/shared_preferences_macos/example/lib/main.dart b/packages/shared_preferences/shared_preferences_macos/example/lib/main.dart index 46daeff6706f..fb85c301f623 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences_macos/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -7,7 +7,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; void main() { runApp(MyApp()); @@ -24,22 +24,28 @@ class MyApp extends StatelessWidget { } class SharedPreferencesDemo extends StatefulWidget { - SharedPreferencesDemo({Key key}) : super(key: key); + SharedPreferencesDemo({Key? key}) : super(key: key); @override SharedPreferencesDemoState createState() => SharedPreferencesDemoState(); } class SharedPreferencesDemoState extends State { - Future _prefs = SharedPreferences.getInstance(); - Future _counter; + SharedPreferencesStorePlatform _prefs = + SharedPreferencesStorePlatform.instance; + late Future _counter; + + // Includes the prefix because this is using the platform interface directly, + // but the prefix (which the native code assumes is present) is added by the + // app-facing package. + static const String _prefKey = 'flutter.counter'; Future _incrementCounter() async { - final SharedPreferences prefs = await _prefs; - final int counter = (prefs.getInt('counter') ?? 0) + 1; + final Map values = await _prefs.getAll(); + final int counter = ((values[_prefKey] as int?) ?? 0) + 1; setState(() { - _counter = prefs.setInt("counter", counter).then((bool success) { + _counter = _prefs.setValue('Int', _prefKey, counter).then((bool success) { return counter; }); }); @@ -48,8 +54,8 @@ class SharedPreferencesDemoState extends State { @override void initState() { super.initState(); - _counter = _prefs.then((SharedPreferences prefs) { - return (prefs.getInt('counter') ?? 0); + _counter = _prefs.getAll().then((Map values) { + return (values[_prefKey] as int?) ?? 0; }); } diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Podfile b/packages/shared_preferences/shared_preferences_macos/example/macos/Podfile new file mode 100644 index 000000000000..e8da8332969a --- /dev/null +++ b/packages/shared_preferences/shared_preferences_macos/example/macos/Podfile @@ -0,0 +1,44 @@ +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__)) + + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcodeproj/project.pbxproj b/packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcodeproj/project.pbxproj index a95e62daada1..96f46f062f91 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcodeproj/project.pbxproj @@ -21,15 +21,13 @@ /* End PBXAggregateTarget section */ /* Begin PBXBuildFile section */ + 2664C8CF4F7C09B469256E8C /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FBE52A82BBDAFEA0EB8C219A /* Pods_RunnerTests.framework */; }; 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, ); }; }; + 33EBD39B26727BD10013E557 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33EBD39A26727BD10013E557 /* RunnerTests.swift */; }; DD4A1B9DEDBB72C87CD7AE27 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5067D74CB28D28AE3B3DD05B /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ @@ -41,6 +39,13 @@ remoteGlobalIDString = 33CC111A2044C6BA0003C045; remoteInfo = FLX; }; + 33EBD39D26727BD10013E557 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -50,8 +55,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */, - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */, ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; @@ -59,6 +62,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 2E4DBB55AB946A7F1AA7D737 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 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 /* url_launcher_example_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = url_launcher_example_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -70,17 +74,21 @@ 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 = ""; }; + 33EBD39826727BD10013E557 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 33EBD39A26727BD10013E557 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 33EBD39C26727BD10013E557 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 5067D74CB28D28AE3B3DD05B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 53F020549CA1E801ACA3428F /* 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 = ""; }; 899489AD6AA35AECA4E2BEA6 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + AD26B2A2C7409B621A8ADDA0 /* 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 = ""; }; B36FDC1D769C9045B8821207 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - D73912EF22F37F9E000D13A0 /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/ephemeral/App.framework; sourceTree = SOURCE_ROOT; }; + CC4DAF1C0735E2069209EED8 /* 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 = ""; }; + FBE52A82BBDAFEA0EB8C219A /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -88,12 +96,18 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */, - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */, DD4A1B9DEDBB72C87CD7AE27 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; + 33EBD39526727BD10013E557 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2664C8CF4F7C09B469256E8C /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -113,6 +127,7 @@ children = ( 33FAB671232836740065AC1E /* Runner */, 33CEB47122A05771004F2AC0 /* Flutter */, + 33EBD39926727BD10013E557 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, 96C1F6D923BD5787E8EBE8FC /* Pods */, @@ -123,6 +138,7 @@ isa = PBXGroup; children = ( 33CC10ED2044A3C60003C045 /* url_launcher_example_example.app */, + 33EBD39826727BD10013E557 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -145,12 +161,19 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - D73912EF22F37F9E000D13A0 /* App.framework */, - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */, ); path = Flutter; sourceTree = ""; }; + 33EBD39926727BD10013E557 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 33EBD39A26727BD10013E557 /* RunnerTests.swift */, + 33EBD39C26727BD10013E557 /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; 33FAB671232836740065AC1E /* Runner */ = { isa = PBXGroup; children = ( @@ -170,8 +193,10 @@ 899489AD6AA35AECA4E2BEA6 /* Pods-Runner.debug.xcconfig */, B36FDC1D769C9045B8821207 /* Pods-Runner.release.xcconfig */, 53F020549CA1E801ACA3428F /* Pods-Runner.profile.xcconfig */, + AD26B2A2C7409B621A8ADDA0 /* Pods-RunnerTests.debug.xcconfig */, + CC4DAF1C0735E2069209EED8 /* Pods-RunnerTests.release.xcconfig */, + 2E4DBB55AB946A7F1AA7D737 /* Pods-RunnerTests.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -179,6 +204,7 @@ isa = PBXGroup; children = ( 5067D74CB28D28AE3B3DD05B /* Pods_Runner.framework */, + FBE52A82BBDAFEA0EB8C219A /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -208,13 +234,32 @@ productReference = 33CC10ED2044A3C60003C045 /* url_launcher_example_example.app */; productType = "com.apple.product-type.application"; }; + 33EBD39726727BD10013E557 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33EBD3A226727BD10013E557 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 057C8B472E54526F53651CE7 /* [CP] Check Pods Manifest.lock */, + 33EBD39426727BD10013E557 /* Sources */, + 33EBD39526727BD10013E557 /* Frameworks */, + 33EBD39626727BD10013E557 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 33EBD39E26727BD10013E557 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 33EBD39826727BD10013E557 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 33CC10E52044A3C60003C045 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 0920; + LastSwiftUpdateCheck = 1250; LastUpgradeCheck = 0930; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { @@ -232,6 +277,10 @@ CreatedOnToolsVersion = 9.2; ProvisioningStyle = Manual; }; + 33EBD39726727BD10013E557 = { + CreatedOnToolsVersion = 12.5; + TestTargetID = 33CC10EC2044A3C60003C045; + }; }; }; buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; @@ -249,6 +298,7 @@ targets = ( 33CC10EC2044A3C60003C045 /* Runner */, 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + 33EBD39726727BD10013E557 /* RunnerTests */, ); }; /* End PBXProject section */ @@ -263,9 +313,38 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 33EBD39626727BD10013E557 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 057C8B472E54526F53651CE7 /* [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; + }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -281,7 +360,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename\n"; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; }; 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -308,10 +387,13 @@ buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/shared_preferences_macos/shared_preferences_macos.framework", ); name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_macos.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -353,6 +435,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 33EBD39426727BD10013E557 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33EBD39B26727BD10013E557 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -361,6 +451,11 @@ target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; }; + 33EBD39E26727BD10013E557 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 33EBD39D26727BD10013E557 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -615,6 +710,63 @@ }; name = Release; }; + 33EBD39F26727BD10013E557 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AD26B2A2C7409B621A8ADDA0 /* Pods-RunnerTests.debug.xcconfig */; + 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)/url_launcher_example_example.app/Contents/MacOS/url_launcher_example_example"; + }; + name = Debug; + }; + 33EBD3A026727BD10013E557 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CC4DAF1C0735E2069209EED8 /* Pods-RunnerTests.release.xcconfig */; + 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)/url_launcher_example_example.app/Contents/MacOS/url_launcher_example_example"; + }; + name = Release; + }; + 33EBD3A126727BD10013E557 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 2E4DBB55AB946A7F1AA7D737 /* Pods-RunnerTests.profile.xcconfig */; + 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)/url_launcher_example_example.app/Contents/MacOS/url_launcher_example_example"; + }; + name = Profile; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -648,6 +800,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 33EBD3A226727BD10013E557 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33EBD39F26727BD10013E557 /* Debug */, + 33EBD3A026727BD10013E557 /* Release */, + 33EBD3A126727BD10013E557 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 660c47db95c3..208a9bafa77a 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/shared_preferences/shared_preferences_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -27,6 +27,15 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + @@ -38,18 +47,17 @@ ReferencedContainer = "container:Runner.xcodeproj"> + + + + - - - - - - - - + + diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/AppDelegate.swift b/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/AppDelegate.swift index d53ef6437726..5cec4c48f620 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/AppDelegate.swift +++ b/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/AppDelegate.swift @@ -1,3 +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 Cocoa import FlutterMacOS diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Configs/AppInfo.xcconfig index eddfd3e0bab0..f19f849dea77 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = url_launcher_example_example // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.urlLauncherExample +PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.urlLauncherExample // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2019 io.flutter.plugins. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2019 The Flutter Authors. All rights reserved. diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/MainFlutterWindow.swift b/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/MainFlutterWindow.swift index 2722837ec918..32aaeedceb1f 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/MainFlutterWindow.swift +++ b/packages/shared_preferences/shared_preferences_macos/example/macos/Runner/MainFlutterWindow.swift @@ -1,3 +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 Cocoa import FlutterMacOS diff --git a/packages/shared_preferences/shared_preferences_macos/example/macos/RunnerTests/Info.plist b/packages/shared_preferences/shared_preferences_macos/example/macos/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_macos/example/macos/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/shared_preferences/shared_preferences_macos/example/macos/RunnerTests/RunnerTests.swift b/packages/shared_preferences/shared_preferences_macos/example/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 000000000000..7da66cbc80df --- /dev/null +++ b/packages/shared_preferences/shared_preferences_macos/example/macos/RunnerTests/RunnerTests.swift @@ -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 FlutterMacOS +import XCTest +import shared_preferences_macos + +class RunnerTests: XCTestCase { + func testHandlesCommitNoOp() throws { + let plugin = SharedPreferencesPlugin() + let call = FlutterMethodCall(methodName: "commit", arguments: nil) + var called = false + plugin.handle( + call, + result: { (result: Any?) -> Void in + called = true + XCTAssert(result as? Bool == true) + }) + XCTAssert(called) + } + + func testSetAndGet() throws { + let plugin = SharedPreferencesPlugin() + let setCall = FlutterMethodCall( + methodName: "setInt", + arguments: [ + "key": "flutter.foo", + "value": 42, + ]) + plugin.handle( + setCall, + result: { (result: Any?) -> Void in + XCTAssert(result as? Bool == true) + }) + + var value: Int? + plugin.handle( + FlutterMethodCall(methodName: "getAll", arguments: nil), + result: { (result: Any?) -> Void in + if let prefs = result as? [String: Any] { + value = prefs["flutter.foo"] as? Int + } + }) + XCTAssertEqual(value, 42) + } + + func testClear() throws { + let plugin = SharedPreferencesPlugin() + let setCall = FlutterMethodCall( + methodName: "setInt", + arguments: [ + "key": "flutter.foo", + "value": 42, + ]) + plugin.handle(setCall, result: { (result: Any?) -> Void in }) + + // Make sure there is something to clear, so the test can't pass due to a set failure. + let getCall = FlutterMethodCall(methodName: "getAll", arguments: nil) + var value: Int? + plugin.handle( + getCall, + result: { (result: Any?) -> Void in + if let prefs = result as? [String: Any] { + value = prefs["flutter.foo"] as? Int + } + }) + XCTAssertEqual(value, 42) + + // Clear the value. + plugin.handle( + FlutterMethodCall(methodName: "clear", arguments: nil), + result: { (result: Any?) -> Void in + XCTAssert(result as? Bool == true) + }) + + // Get the value again, which should clear |value|. + plugin.handle( + getCall, + result: { (result: Any?) -> Void in + if let prefs = result as? [String: Any] { + value = prefs["flutter.foo"] as? Int + XCTAssert(prefs.isEmpty) + } + }) + XCTAssertEqual(value, nil) + } +} diff --git a/packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml index de4b6ed9d379..e6bf972cf5b4 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml @@ -1,21 +1,29 @@ name: shared_preferences_example description: Demonstrates how to use the shared_preferences plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.12.8" dependencies: flutter: sdk: flutter - shared_preferences: any + shared_preferences_platform_interface: ^2.0.0 shared_preferences_macos: + # When depending on this package from a real application you should use: + # shared_preferences_macos: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: flutter_driver: sdk: flutter - test: any integration_test: - path: ../../../integration_test - pedantic: ^1.8.0 + sdk: flutter + pedantic: ^1.10.0 flutter: uses-material-design: true - diff --git a/packages/shared_preferences/shared_preferences_macos/example/test_driver/integration_test.dart b/packages/shared_preferences/shared_preferences_macos/example/test_driver/integration_test.dart index 7a2c21338786..4f10f2a522f3 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/test_driver/integration_test.dart +++ b/packages/shared_preferences/shared_preferences_macos/example/test_driver/integration_test.dart @@ -1,17 +1,7 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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'; +import 'package:integration_test/integration_test_driver.dart'; -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String data = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - await driver.close(); - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} +Future main() => integrationDriver(); diff --git a/packages/shared_preferences/shared_preferences_macos/ios/shared_preferences_macos.podspec b/packages/shared_preferences/shared_preferences_macos/ios/shared_preferences_macos.podspec deleted file mode 100644 index 8e2a2bd30dac..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/ios/shared_preferences_macos.podspec +++ /dev/null @@ -1,21 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'shared_preferences_macos' - s.version = '0.0.1' - s.summary = 'No-op implementation of shared_preferences desktop plugin to avoid build issues on iOS' - s.description = <<-DESC - No-op implementation of shared_preferences to avoid build issues on iOS. - DESC - - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_macos' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.ios.deployment_target = '8.0' -end diff --git a/packages/shared_preferences/shared_preferences_macos/macos/Classes/SharedPreferencesPlugin.swift b/packages/shared_preferences/shared_preferences_macos/macos/Classes/SharedPreferencesPlugin.swift index 8f7f58ece635..2cf345cf0b04 100644 --- a/packages/shared_preferences/shared_preferences_macos/macos/Classes/SharedPreferencesPlugin.swift +++ b/packages/shared_preferences/shared_preferences_macos/macos/Classes/SharedPreferencesPlugin.swift @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/shared_preferences/shared_preferences_macos/pubspec.yaml b/packages/shared_preferences/shared_preferences_macos/pubspec.yaml index 5657b9e3200a..5eddba2d51ad 100644 --- a/packages/shared_preferences/shared_preferences_macos/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_macos/pubspec.yaml @@ -1,25 +1,24 @@ name: shared_preferences_macos description: macOS implementation of the shared_preferences plugin. -# 0.0.y+z is compatible with 1.0.0, if you land a breaking change bump -# the version to 2.0.0. -# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.0.1+10 -homepage: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_macos +repository: https://github.com/flutter/plugins/tree/master/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.1 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: + implements: shared_preferences platforms: macos: pluginClass: SharedPreferencesPlugin -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" - dependencies: - shared_preferences_platform_interface: ^1.0.0 flutter: sdk: flutter -dev_dependencies: - pedantic: ^1.8.0 + shared_preferences_platform_interface: ^2.0.0 +dev_dependencies: + pedantic: ^1.10.0 diff --git a/packages/shared_preferences/shared_preferences_platform_interface/AUTHORS b/packages/shared_preferences/shared_preferences_platform_interface/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_platform_interface/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md b/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md index 5fe7b18160ba..b402f6e57e88 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_platform_interface/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.0.0 + +* Migrate to null safety. + +## 1.0.5 + +* Update Flutter SDK constraint. + ## 1.0.4 * Update lower bound of dart dependency to 2.1.0. diff --git a/packages/shared_preferences/shared_preferences_platform_interface/LICENSE b/packages/shared_preferences/shared_preferences_platform_interface/LICENSE index a6d6c0749818..c6823b81eb84 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/LICENSE +++ b/packages/shared_preferences/shared_preferences_platform_interface/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017 The Chromium Authors. All rights reserved. +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: diff --git a/packages/shared_preferences/shared_preferences_platform_interface/lib/method_channel_shared_preferences.dart b/packages/shared_preferences/shared_preferences_platform_interface/lib/method_channel_shared_preferences.dart index 66009a5caf14..fa1bdc097b8d 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -18,39 +18,32 @@ const MethodChannel _kChannel = class MethodChannelSharedPreferencesStore extends SharedPreferencesStorePlatform { @override - Future remove(String key) { - return _invokeBoolMethod('remove', { - 'key': key, - }); + Future remove(String key) async { + return (await _kChannel.invokeMethod( + 'remove', + {'key': key}, + ))!; } @override - Future setValue(String valueType, String key, Object value) { - return _invokeBoolMethod('set$valueType', { - 'key': key, - 'value': value, - }); - } - - Future _invokeBoolMethod(String method, Map params) { - return _kChannel - .invokeMethod(method, params) - // TODO(yjbanov): I copied this from the original - // shared_preferences.dart implementation, but I - // actually do not know why it's necessary to pipe the - // result through an identity function. - // - // Source: https://github.com/flutter/plugins/blob/3a87296a40a2624d200917d58f036baa9fb18df8/packages/shared_preferences/lib/shared_preferences.dart#L134 - .then((dynamic result) => result); + Future setValue(String valueType, String key, Object value) async { + return (await _kChannel.invokeMethod( + 'set$valueType', + {'key': key, 'value': value}, + ))!; } @override - Future clear() { - return _kChannel.invokeMethod('clear'); + Future clear() async { + return (await _kChannel.invokeMethod('clear'))!; } @override - Future> getAll() { - return _kChannel.invokeMapMethod('getAll'); + Future> getAll() async { + final Map? preferences = + await _kChannel.invokeMapMethod('getAll'); + + if (preferences == null) return {}; + return preferences; } } diff --git a/packages/shared_preferences/shared_preferences_platform_interface/lib/shared_preferences_platform_interface.dart b/packages/shared_preferences/shared_preferences_platform_interface/lib/shared_preferences_platform_interface.dart index 5a2b99ca69b1..8023c864a399 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/lib/shared_preferences_platform_interface.dart +++ b/packages/shared_preferences/shared_preferences_platform_interface/lib/shared_preferences_platform_interface.dart @@ -1,10 +1,10 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; -import 'package:meta/meta.dart'; +import 'package:flutter/foundation.dart'; import 'method_channel_shared_preferences.dart'; diff --git a/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml b/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml index 3b8f2bfcada4..39f5a3ce42e9 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_platform_interface/pubspec.yaml @@ -1,18 +1,18 @@ name: shared_preferences_platform_interface description: A common platform interface for the shared_preferences plugin. -homepage: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_platform_interface -version: 1.0.4 +repository: https://github.com/flutter/plugins/tree/master/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" dependencies: - meta: ^1.0.4 flutter: sdk: flutter dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" + pedantic: ^1.10.0 diff --git a/packages/shared_preferences/shared_preferences_platform_interface/test/method_channel_shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_platform_interface/test/method_channel_shared_preferences_test.dart index 4cc79b058675..3b43062c2be3 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -15,7 +15,7 @@ void main() { 'plugins.flutter.io/shared_preferences', ); - const Map kTestValues = { + const Map kTestValues = { 'flutter.String': 'hello world', 'flutter.Bool': true, 'flutter.Int': 42, @@ -23,10 +23,10 @@ void main() { 'flutter.StringList': ['foo', 'bar'], }; - InMemorySharedPreferencesStore testData; + late InMemorySharedPreferencesStore testData; final List log = []; - MethodChannelSharedPreferencesStore store; + late MethodChannelSharedPreferencesStore store; setUp(() async { testData = InMemorySharedPreferencesStore.empty(); @@ -44,9 +44,9 @@ void main() { return await testData.clear(); } final RegExp setterRegExp = RegExp(r'set(.*)'); - final Match match = setterRegExp.matchAsPrefix(methodCall.method); - if (match.groupCount == 1) { - final String valueType = match.group(1); + final Match? match = setterRegExp.matchAsPrefix(methodCall.method); + if (match?.groupCount == 1) { + final String valueType = match!.group(1)!; final String key = methodCall.arguments['key']; final Object value = methodCall.arguments['value']; return await testData.setValue(valueType, key, value); @@ -59,8 +59,6 @@ void main() { tearDown(() async { await testData.clear(); - store = null; - testData = null; }); test('getAll', () async { diff --git a/packages/shared_preferences/shared_preferences_platform_interface/test/shared_preferences_platform_interface_test.dart b/packages/shared_preferences/shared_preferences_platform_interface/test/shared_preferences_platform_interface_test.dart index 9e957734d174..8efe885c122c 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/test/shared_preferences_platform_interface_test.dart +++ b/packages/shared_preferences/shared_preferences_platform_interface/test/shared_preferences_platform_interface_test.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/shared_preferences/shared_preferences_web/AUTHORS b/packages/shared_preferences/shared_preferences_web/AUTHORS new file mode 100644 index 000000000000..dbf9d190931b --- /dev/null +++ b/packages/shared_preferences/shared_preferences_web/AUTHORS @@ -0,0 +1,65 @@ +# 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. +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/shared_preferences/shared_preferences_web/CHANGELOG.md b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md index b2076dd8c17e..ec08267fe59f 100644 --- a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md @@ -1,3 +1,11 @@ +## 2.0.0 + +* Migrate to null-safety. + +## 0.1.2+8 + +* Update Flutter SDK constraint. + ## 0.1.2+7 * Removed Android folder from `shared_preferences_web`. diff --git a/packages/shared_preferences/shared_preferences_web/LICENSE b/packages/shared_preferences/shared_preferences_web/LICENSE index 507569823f1b..c6823b81eb84 100644 --- a/packages/shared_preferences/shared_preferences_web/LICENSE +++ b/packages/shared_preferences/shared_preferences_web/LICENSE @@ -1,4 +1,4 @@ -Copyright 2019 The Chromium Authors. All rights reserved. +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: diff --git a/packages/shared_preferences/shared_preferences_web/README.md b/packages/shared_preferences/shared_preferences_web/README.md index 2bb8a9316d86..8f9d22d86ef5 100644 --- a/packages/shared_preferences/shared_preferences_web/README.md +++ b/packages/shared_preferences/shared_preferences_web/README.md @@ -2,13 +2,6 @@ The web implementation of [`shared_preferences`][1]. -**Please set your constraint to `shared_preferences_web: '>=0.1.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.1.y+z`. -Please use `shared_preferences_web: '>=0.1.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ## Usage ### Import the package diff --git a/packages/shared_preferences/shared_preferences_web/ios/shared_preferences_web.podspec b/packages/shared_preferences/shared_preferences_web/ios/shared_preferences_web.podspec deleted file mode 100644 index 11f8b73e02d8..000000000000 --- a/packages/shared_preferences/shared_preferences_web/ios/shared_preferences_web.podspec +++ /dev/null @@ -1,20 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'shared_preferences_web' - s.version = '0.0.1' - s.summary = 'No-op implementation of shared_preferences web plugin to avoid build issues on iOS' - s.description = <<-DESC -temp fake shared_preferences_web plugin - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_web' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.ios.deployment_target = '8.0' -end diff --git a/packages/shared_preferences/shared_preferences_web/lib/shared_preferences_web.dart b/packages/shared_preferences/shared_preferences_web/lib/shared_preferences_web.dart index 8a0f137ddcc8..9cff1d448896 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -14,7 +14,7 @@ import 'package:shared_preferences_platform_interface/shared_preferences_platfor /// This class implements the `package:shared_preferences` functionality for the web. class SharedPreferencesPlugin extends SharedPreferencesStorePlatform { /// Registers this class as the default instance of [SharedPreferencesStorePlatform]. - static void registerWith(Registrar registrar) { + static void registerWith(Registrar? registrar) { SharedPreferencesStorePlatform.instance = SharedPreferencesPlugin(); } @@ -31,9 +31,9 @@ class SharedPreferencesPlugin extends SharedPreferencesStorePlatform { @override Future> getAll() async { - final Map allData = {}; + final Map allData = {}; for (String key in _storedFlutterKeys) { - allData[key] = _decodeValue(html.window.localStorage[key]); + allData[key] = _decodeValue(html.window.localStorage[key]!); } return allData; } @@ -46,7 +46,7 @@ class SharedPreferencesPlugin extends SharedPreferencesStorePlatform { } @override - Future setValue(String valueType, String key, Object value) async { + Future setValue(String valueType, String key, Object? value) async { _checkPrefix(key); html.window.localStorage[key] = _encodeValue(value); return true; @@ -62,17 +62,12 @@ class SharedPreferencesPlugin extends SharedPreferencesStorePlatform { } } - List get _storedFlutterKeys { - final List keys = []; - for (String key in html.window.localStorage.keys) { - if (key.startsWith('flutter.')) { - keys.add(key); - } - } - return keys; + Iterable get _storedFlutterKeys { + return html.window.localStorage.keys + .where((key) => key.startsWith('flutter.')); } - String _encodeValue(Object value) { + String _encodeValue(Object? value) { return json.encode(value); } diff --git a/packages/shared_preferences/shared_preferences_web/pubspec.yaml b/packages/shared_preferences/shared_preferences_web/pubspec.yaml index a153135b6da7..cd2e063fe6b3 100644 --- a/packages/shared_preferences/shared_preferences_web/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_web/pubspec.yaml @@ -1,10 +1,12 @@ name: shared_preferences_web description: Web platform implementation of shared_preferences -homepage: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_web -# 0.1.y+z is compatible with 1.0.0, if you land a breaking change bump -# the version to 2.0.0. -# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.1.2+7 +repository: https://github.com/flutter/plugins/tree/master/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.0 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: @@ -14,18 +16,14 @@ flutter: fileName: shared_preferences_web.dart dependencies: - shared_preferences_platform_interface: ^1.0.0 flutter: sdk: flutter flutter_web_plugins: sdk: flutter - meta: ^1.1.7 + meta: ^1.3.0 + shared_preferences_platform_interface: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4 <2.0.0" + pedantic: ^1.10.0 diff --git a/packages/shared_preferences/shared_preferences_web/test/shared_preferences_web_test.dart b/packages/shared_preferences/shared_preferences_web/test/shared_preferences_web_test.dart index 951f04cbce5a..6e49fb47f755 100644 --- a/packages/shared_preferences/shared_preferences_web/test/shared_preferences_web_test.dart +++ b/packages/shared_preferences/shared_preferences_web/test/shared_preferences_web_test.dart @@ -1,13 +1,13 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -@TestOn('chrome') // Uses web-only Flutter SDK - +@TestOn('chrome') import 'dart:convert' show json; import 'dart:html' as html; import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences_platform_interface/method_channel_shared_preferences.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; import 'package:shared_preferences_web/shared_preferences_web.dart'; @@ -26,6 +26,8 @@ void main() { }); test('registers itself', () { + SharedPreferencesStorePlatform.instance = + MethodChannelSharedPreferencesStore(); expect(SharedPreferencesStorePlatform.instance, isNot(isA())); SharedPreferencesPlugin.registerWith(null); diff --git a/packages/shared_preferences/shared_preferences_windows/AUTHORS b/packages/shared_preferences/shared_preferences_windows/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md index 7f82e5ba12f1..34c48f37af48 100644 --- a/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md @@ -1,3 +1,36 @@ +## 2.0.1 + +* Add `implements` to pubspec.yaml. +* Add `registerWith` to the Dart main class. + +## 2.0.0 + +* Migrate to null-safety. + +## 0.0.2+3 + +* Remove 'ffi' dependency. + +## 0.0.2+2 + +* Relax 'ffi' version constraint. + +## 0.0.2+1 + +* Update Flutter SDK constraint. + +## 0.0.2 + +* Update integration test examples to use `testWidgets` instead of `test`. + +## 0.0.1+3 + +* Remove unused `test` dependency. + +## 0.0.1+2 + +* Check in windows/ directory for example/ + ## 0.0.1+1 * Add iOS stub for compatibility with 1.17 and earlier. diff --git a/packages/shared_preferences/shared_preferences_windows/LICENSE b/packages/shared_preferences/shared_preferences_windows/LICENSE index c89293372cf3..c6823b81eb84 100644 --- a/packages/shared_preferences/shared_preferences_windows/LICENSE +++ b/packages/shared_preferences/shared_preferences_windows/LICENSE @@ -1,27 +1,25 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +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_windows/example/AUTHORS b/packages/shared_preferences/shared_preferences_windows/example/AUTHORS new file mode 100644 index 000000000000..dbf9d190931b --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/example/AUTHORS @@ -0,0 +1,65 @@ +# 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. +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/shared_preferences/shared_preferences_windows/example/CHANGELOG.md b/packages/shared_preferences/shared_preferences_windows/example/CHANGELOG.md deleted file mode 100644 index 41cc7d8192ec..000000000000 --- a/packages/shared_preferences/shared_preferences_windows/example/CHANGELOG.md +++ /dev/null @@ -1,3 +0,0 @@ -## 0.0.1 - -* TODO: Describe initial release. diff --git a/packages/shared_preferences/shared_preferences_windows/example/LICENSE b/packages/shared_preferences/shared_preferences_windows/example/LICENSE index c89293372cf3..c6823b81eb84 100644 --- a/packages/shared_preferences/shared_preferences_windows/example/LICENSE +++ b/packages/shared_preferences/shared_preferences_windows/example/LICENSE @@ -1,27 +1,25 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following disclaimer -// in the documentation and/or other materials provided with the -// distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived from -// this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +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_windows/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_windows/example/integration_test/shared_preferences_test.dart index 44c8129e9405..207d712650a7 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 @@ -1,14 +1,14 @@ -// Copyright 2017, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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:shared_preferences_windows/shared_preferences_windows.dart'; -import 'package:e2e/e2e.dart'; +import 'package:integration_test/integration_test.dart'; void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); group('SharedPreferencesWindows', () { const Map kTestValues = { @@ -27,7 +27,7 @@ void main() { 'flutter.List': ['baz', 'quox'], }; - SharedPreferencesWindows preferences; + late SharedPreferencesWindows preferences; setUp(() async { preferences = SharedPreferencesWindows.instance; @@ -37,7 +37,7 @@ void main() { preferences.clear(); }); - test('reading', () async { + testWidgets('reading', (WidgetTester _) async { final Map values = await preferences.getAll(); expect(values['String'], isNull); expect(values['bool'], isNull); @@ -46,7 +46,7 @@ void main() { expect(values['List'], isNull); }); - test('writing', () async { + testWidgets('writing', (WidgetTester _) async { await Future.wait(>[ preferences.setValue( 'String', 'String', kTestValues2['flutter.String']), @@ -64,7 +64,7 @@ void main() { expect(values['List'], kTestValues2['flutter.List']); }); - test('removing', () async { + testWidgets('removing', (WidgetTester _) async { const String key = 'testKey'; await preferences.setValue('String', key, kTestValues['flutter.String']); await preferences.setValue('Bool', key, kTestValues['flutter.bool']); @@ -77,7 +77,7 @@ void main() { expect(values[key], isNull); }); - test('clearing', () async { + testWidgets('clearing', (WidgetTester _) async { await preferences.setValue( 'String', 'String', kTestValues['flutter.String']); await preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']); diff --git a/packages/shared_preferences/shared_preferences_windows/example/lib/main.dart b/packages/shared_preferences/shared_preferences_windows/example/lib/main.dart index 140851c90504..0cdd37394706 100644 --- a/packages/shared_preferences/shared_preferences_windows/example/lib/main.dart +++ b/packages/shared_preferences/shared_preferences_windows/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -24,7 +24,7 @@ class MyApp extends StatelessWidget { } class SharedPreferencesDemo extends StatefulWidget { - SharedPreferencesDemo({Key key}) : super(key: key); + SharedPreferencesDemo({Key? key}) : super(key: key); @override SharedPreferencesDemoState createState() => SharedPreferencesDemoState(); @@ -32,14 +32,14 @@ class SharedPreferencesDemo extends StatefulWidget { class SharedPreferencesDemoState extends State { final prefs = SharedPreferencesWindows.instance; - Future _counter; + late Future _counter; Future _incrementCounter() async { final values = await prefs.getAll(); - final int counter = (values['counter'] as int ?? 0) + 1; + final int counter = (values['counter'] as int? ?? 0) + 1; setState(() { - _counter = prefs.setValue(null, "counter", counter).then((bool success) { + _counter = prefs.setValue('Int', 'counter', counter).then((bool success) { return counter; }); }); @@ -49,7 +49,7 @@ class SharedPreferencesDemoState extends State { void initState() { super.initState(); _counter = prefs.getAll().then((Map values) { - return (values['counter'] ?? 0); + return (values['counter'] as int? ?? 0); }); } diff --git a/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml index 6dd7841ea477..96762e933a9d 100644 --- a/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml @@ -1,24 +1,28 @@ name: shared_preferences_windows_example description: Demonstrates how to use the shared_preferences_windows plugin. +publish_to: none environment: - sdk: ">=2.7.0 <3.0.0" + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: sdk: flutter - shared_preferences_windows: ^0.0.1 - -dependency_overrides: shared_preferences_windows: + # When depending on this package from a real application you should use: + # shared_preferences_windows: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: flutter_driver: sdk: flutter - test: any - e2e: ^0.2.0 - pedantic: ^1.8.0 + integration_test: + sdk: flutter + pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/shared_preferences/shared_preferences_windows/example/test_driver/integration_test.dart b/packages/shared_preferences/shared_preferences_windows/example/test_driver/integration_test.dart index 102dfdfef708..4f10f2a522f3 100644 --- a/packages/shared_preferences/shared_preferences_windows/example/test_driver/integration_test.dart +++ b/packages/shared_preferences/shared_preferences_windows/example/test_driver/integration_test.dart @@ -1,15 +1,7 @@ -// Copyright 2017, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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_driver/flutter_driver.dart'; +import 'package:integration_test/integration_test_driver.dart'; -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String result = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - await driver.close(); - exit(result == 'pass' ? 0 : 1); -} +Future main() => integrationDriver(); diff --git a/packages/shared_preferences/shared_preferences_windows/example/windows/.gitignore b/packages/shared_preferences/shared_preferences_windows/example/windows/.gitignore new file mode 100644 index 000000000000..d492d0d98c8f --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_windows/example/windows/CMakeLists.txt b/packages/shared_preferences/shared_preferences_windows/example/windows/CMakeLists.txt new file mode 100644 index 000000000000..abf90408efb4 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/example/windows/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.15) +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/shared_preferences/shared_preferences_windows/example/windows/flutter/CMakeLists.txt b/packages/shared_preferences/shared_preferences_windows/example/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000000..c7a8c7607d81 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,101 @@ +cmake_minimum_required(VERSION 3.15) + +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" +) +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 $ +) +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/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 new file mode 100644 index 000000000000..8b6d4680af38 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,11 @@ +// +// 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 new file mode 100644 index 000000000000..dc139d85a931 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// 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/flutter/generated_plugins.cmake b/packages/shared_preferences/shared_preferences_windows/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..4d10c2518654 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,15 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_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) diff --git a/packages/shared_preferences/shared_preferences_windows/example/windows/runner/CMakeLists.txt b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/CMakeLists.txt new file mode 100644 index 000000000000..977e38b5d1d2 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.15) +project(runner LANGUAGES CXX) + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "run_loop.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/shared_preferences/shared_preferences_windows/example/windows/runner/Runner.rc b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/Runner.rc new file mode 100644 index 000000000000..944329afc03a --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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) 2020 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/shared_preferences/shared_preferences_windows/example/windows/runner/flutter_window.cpp b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/flutter_window.cpp new file mode 100644 index 000000000000..8e415602cf3b --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/flutter_window.cpp @@ -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. + +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(RunLoop* run_loop, + const flutter::DartProject& project) + : run_loop_(run_loop), 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()); + run_loop_->RegisterFlutterInstance(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + run_loop_->UnregisterFlutterInstance(flutter_controller_->engine()); + 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 opporutunity 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/shared_preferences/shared_preferences_windows/example/windows/runner/flutter_window.h b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/flutter_window.h new file mode 100644 index 000000000000..8e9c12bbe022 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/flutter_window.h @@ -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. + +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "run_loop.h" +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow driven by the |run_loop|, hosting a + // Flutter view running |project|. + explicit FlutterWindow(RunLoop* run_loop, + 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 run loop driving events for this window. + RunLoop* run_loop_; + + // 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/shared_preferences/shared_preferences_windows/example/windows/runner/main.cpp b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/main.cpp new file mode 100644 index 000000000000..126302b0be18 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/main.cpp @@ -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. + +#include +#include +#include + +#include "flutter_window.h" +#include "run_loop.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); + + RunLoop run_loop; + + flutter::DartProject project(L"data"); + FlutterWindow window(&run_loop, project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + run_loop.Run(); + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/packages/shared_preferences/shared_preferences_windows/example/windows/runner/resource.h b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/resource.h new file mode 100644 index 000000000000..d5d958dc4257 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_windows/example/windows/runner/resources/app_icon.ico b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000000..c04e20caf637 Binary files /dev/null and b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/resources/app_icon.ico differ diff --git a/packages/shared_preferences/shared_preferences_windows/example/windows/runner/run_loop.cpp b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/run_loop.cpp new file mode 100644 index 000000000000..1916500e6440 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/run_loop.cpp @@ -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. + +#include "run_loop.h" + +#include + +#include + +RunLoop::RunLoop() {} + +RunLoop::~RunLoop() {} + +void RunLoop::Run() { + bool keep_running = true; + TimePoint next_flutter_event_time = TimePoint::clock::now(); + while (keep_running) { + std::chrono::nanoseconds wait_duration = + std::max(std::chrono::nanoseconds(0), + next_flutter_event_time - TimePoint::clock::now()); + ::MsgWaitForMultipleObjects( + 0, nullptr, FALSE, static_cast(wait_duration.count() / 1000), + QS_ALLINPUT); + bool processed_events = false; + MSG message; + // All pending Windows messages must be processed; MsgWaitForMultipleObjects + // won't return again for items left in the queue after PeekMessage. + while (::PeekMessage(&message, nullptr, 0, 0, PM_REMOVE)) { + processed_events = true; + if (message.message == WM_QUIT) { + keep_running = false; + break; + } + ::TranslateMessage(&message); + ::DispatchMessage(&message); + // Allow Flutter to process messages each time a Windows message is + // processed, to prevent starvation. + next_flutter_event_time = + std::min(next_flutter_event_time, ProcessFlutterMessages()); + } + // If the PeekMessage loop didn't run, process Flutter messages. + if (!processed_events) { + next_flutter_event_time = + std::min(next_flutter_event_time, ProcessFlutterMessages()); + } + } +} + +void RunLoop::RegisterFlutterInstance( + flutter::FlutterEngine* flutter_instance) { + flutter_instances_.insert(flutter_instance); +} + +void RunLoop::UnregisterFlutterInstance( + flutter::FlutterEngine* flutter_instance) { + flutter_instances_.erase(flutter_instance); +} + +RunLoop::TimePoint RunLoop::ProcessFlutterMessages() { + TimePoint next_event_time = TimePoint::max(); + for (auto instance : flutter_instances_) { + std::chrono::nanoseconds wait_duration = instance->ProcessMessages(); + if (wait_duration != std::chrono::nanoseconds::max()) { + next_event_time = + std::min(next_event_time, TimePoint::clock::now() + wait_duration); + } + } + return next_event_time; +} diff --git a/packages/shared_preferences/shared_preferences_windows/example/windows/runner/run_loop.h b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/run_loop.h new file mode 100644 index 000000000000..819ed3ed4995 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/run_loop.h @@ -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. + +#ifndef RUNNER_RUN_LOOP_H_ +#define RUNNER_RUN_LOOP_H_ + +#include + +#include +#include + +// A runloop that will service events for Flutter instances as well +// as native messages. +class RunLoop { + public: + RunLoop(); + ~RunLoop(); + + // Prevent copying + RunLoop(RunLoop const&) = delete; + RunLoop& operator=(RunLoop const&) = delete; + + // Runs the run loop until the application quits. + void Run(); + + // Registers the given Flutter instance for event servicing. + void RegisterFlutterInstance(flutter::FlutterEngine* flutter_instance); + + // Unregisters the given Flutter instance from event servicing. + void UnregisterFlutterInstance(flutter::FlutterEngine* flutter_instance); + + private: + using TimePoint = std::chrono::steady_clock::time_point; + + // Processes all currently pending messages for registered Flutter instances. + TimePoint ProcessFlutterMessages(); + + std::set flutter_instances_; +}; + +#endif // RUNNER_RUN_LOOP_H_ diff --git a/packages/shared_preferences/shared_preferences_windows/example/windows/runner/runner.exe.manifest b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/runner.exe.manifest new file mode 100644 index 000000000000..c977c4a42589 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + 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 new file mode 100644 index 000000000000..537728149601 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/utils.cpp @@ -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. + +#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(); + } +} diff --git a/packages/shared_preferences/shared_preferences_windows/example/windows/runner/utils.h b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/utils.h new file mode 100644 index 000000000000..16b3f0794597 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/utils.h @@ -0,0 +1,12 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/shared_preferences/shared_preferences_windows/example/windows/runner/win32_window.cpp b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/win32_window.cpp new file mode 100644 index 000000000000..a609a2002bb3 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_windows/example/windows/runner/win32_window.h b/packages/shared_preferences/shared_preferences_windows/example/windows/runner/win32_window.h new file mode 100644 index 000000000000..d2a730052223 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_windows/ios/shared_preferences_windows.podspec b/packages/shared_preferences/shared_preferences_windows/ios/shared_preferences_windows.podspec deleted file mode 100644 index 2e239e607493..000000000000 --- a/packages/shared_preferences/shared_preferences_windows/ios/shared_preferences_windows.podspec +++ /dev/null @@ -1,22 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# Run `pod lib lint shared_preferences_windows.podspec' to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'shared_preferences_windows' - s.version = '0.0.1' - s.summary = 'shared_preferences_windows iOS stub' - s.description = <<-DESC - No-op implementation of the windows shared_preferences plugin to avoid build issues on iOS - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_windows' } - s.dependency 'Flutter' - s.platform = :ios, '8.0' - - # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } - s.swift_version = '5.0' -end diff --git a/packages/shared_preferences/shared_preferences_windows/lib/shared_preferences_windows.dart b/packages/shared_preferences/shared_preferences_windows/lib/shared_preferences_windows.dart index dd9ab8a0c38f..b8cd3702b837 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 @@ -1,4 +1,4 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -16,8 +16,15 @@ import 'package:path_provider_windows/path_provider_windows.dart'; /// 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 static SharedPreferencesWindows instance = SharedPreferencesWindows(); + /// Registers the Windows implementation. + static void registerWith() { + SharedPreferencesStorePlatform.instance = instance; + } + /// File system used to store to disk. Exposed for testing only. @visibleForTesting FileSystem fs = LocalFileSystem(); @@ -27,42 +34,51 @@ class SharedPreferencesWindows extends SharedPreferencesStorePlatform { PathProviderWindows pathProvider = PathProviderWindows(); /// Local copy of preferences - Map _cachedPreferences; + Map? _cachedPreferences; /// Cached file for storing preferences. - File _localDataFilePath; + File? _localDataFilePath; /// Gets the file where the preferences are stored. - Future _getLocalDataFile() async { - if (_localDataFilePath == null) { - final directory = await pathProvider.getApplicationSupportPath(); - _localDataFilePath = - fs.file(path.join(directory, 'shared_preferences.json')); + Future _getLocalDataFile() async { + if (_localDataFilePath != null) { + return _localDataFilePath!; + } + final directory = await pathProvider.getApplicationSupportPath(); + if (directory == null) { + return null; } - return _localDataFilePath; + return _localDataFilePath = + fs.file(path.join(directory, 'shared_preferences.json')); } /// Gets the preferences from the stored file. Once read, the preferences are /// maintained in memory. Future> _readPreferences() async { - if (_cachedPreferences == null) { - _cachedPreferences = {}; - File localDataFile = await _getLocalDataFile(); - if (localDataFile.existsSync()) { - String stringMap = localDataFile.readAsStringSync(); - if (stringMap.isNotEmpty) { - _cachedPreferences = json.decode(stringMap) as Map; - } + if (_cachedPreferences != null) { + return _cachedPreferences!; + } + Map preferences = {}; + final File? localDataFile = await _getLocalDataFile(); + if (localDataFile != null && localDataFile.existsSync()) { + String stringMap = localDataFile.readAsStringSync(); + if (stringMap.isNotEmpty) { + preferences = json.decode(stringMap).cast(); } } - return _cachedPreferences; + _cachedPreferences = preferences; + return preferences; } /// Writes the cached preferences to disk. Returns [true] if the operation /// succeeded. Future _writePreferences(Map preferences) async { try { - File localDataFile = await _getLocalDataFile(); + final File? localDataFile = await _getLocalDataFile(); + if (localDataFile == null) { + print("Unable to determine where to write preferences."); + return false; + } if (!localDataFile.existsSync()) { localDataFile.createSync(recursive: true); } diff --git a/packages/shared_preferences/shared_preferences_windows/pubspec.yaml b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml index f1ce8ecf48d7..2cc59d5aa635 100644 --- a/packages/shared_preferences/shared_preferences_windows/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml @@ -1,31 +1,32 @@ name: shared_preferences_windows description: Windows implementation of shared_preferences -homepage: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_windows -version: 0.0.1+1 +repository: https://github.com/flutter/plugins/tree/master/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.1 + +environment: + sdk: '>=2.12.0 <3.0.0' + flutter: ">=2.0.0" flutter: plugin: + implements: shared_preferences platforms: windows: dartPluginClass: SharedPreferencesWindows pluginClass: none -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" - dependencies: - shared_preferences_platform_interface: ^1.0.0 flutter: sdk: flutter - ffi: ^0.1.3 - file: ">=5.1.0 <7.0.0" - meta: ^1.1.7 - path: ^1.6.4 - path_provider_platform_interface: ^1.0.3 - path_provider_windows: ^0.0.2 + file: ^6.0.0 + meta: ^1.3.0 + path: ^1.8.0 + path_provider_platform_interface: ^2.0.0 + path_provider_windows: ^2.0.0 + shared_preferences_platform_interface: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 diff --git a/packages/shared_preferences/shared_preferences_windows/test/shared_preferences_windows_test.dart b/packages/shared_preferences/shared_preferences_windows/test/shared_preferences_windows_test.dart index b0827ca3b36b..6bb21b814e07 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 @@ -1,4 +1,4 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -7,22 +7,21 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:path/path.dart' as path; import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; import 'package:path_provider_windows/path_provider_windows.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; import 'package:shared_preferences_windows/shared_preferences_windows.dart'; void main() { - MemoryFileSystem fileSystem; - PathProviderWindows pathProvider; + late MemoryFileSystem fileSystem; + late PathProviderWindows pathProvider; setUp(() { fileSystem = MemoryFileSystem.test(); pathProvider = FakePathProviderWindows(); }); - tearDown(() {}); - Future _getFilePath() async { final directory = await pathProvider.getApplicationSupportPath(); - return path.join(directory, 'shared_preferences.json'); + return path.join(directory!, 'shared_preferences.json'); } _writeTestFile(String value) async { @@ -42,6 +41,12 @@ void main() { return prefs; } + test('registered instance', () { + SharedPreferencesWindows.registerWith(); + expect(SharedPreferencesStorePlatform.instance, + isA()); + }); + test('getAll', () async { await _writeTestFile('{"key1": "one", "key2": 2}'); var prefs = _getPreferences(); @@ -87,23 +92,23 @@ void main() { /// path it returns is a root path that does not actually exist on Windows. class FakePathProviderWindows extends PathProviderPlatform implements PathProviderWindows { - VersionInfoQuerier versionInfoQuerier; + late VersionInfoQuerier versionInfoQuerier; @override - Future getApplicationSupportPath() async => r'C:\appsupport'; + Future getApplicationSupportPath() async => r'C:\appsupport'; @override - Future getTemporaryPath() async => null; + Future getTemporaryPath() async => null; @override - Future getLibraryPath() async => null; + Future getLibraryPath() async => null; @override - Future getApplicationDocumentsPath() async => null; + Future getApplicationDocumentsPath() async => null; @override - Future getDownloadsPath() async => null; + Future getDownloadsPath() async => null; @override - Future getPath(String folderID) async => null; + Future getPath(String folderID) async => ''; } diff --git a/packages/url_launcher/analysis_options.yaml b/packages/url_launcher/analysis_options.yaml new file mode 100644 index 000000000000..cda4f6e153e6 --- /dev/null +++ b/packages/url_launcher/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options_legacy.yaml diff --git a/packages/url_launcher/url_launcher/AUTHORS b/packages/url_launcher/url_launcher/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/url_launcher/url_launcher/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index 2b499a55427d..03a3f6fb0b76 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,3 +1,83 @@ +## 6.0.6 + +* Require `url_launcher_platform_interface` 2.0.3. This fixes an issue + where 6.0.5 could fail to compile in some projects due to internal + changes in that version that were not compatible with earlier versions + of `url_launcher_platform_interface`. + +## 6.0.5 + +* Add iOS unit and UI integration test targets. +* Add a `Link` widget to the example app. + +## 6.0.4 + +* Migrate maven repository from jcenter to mavenCentral. + +## 6.0.3 + +* Update README notes about URL schemes on iOS + +## 6.0.2 + +* Update platform_plugin_interface version requirement. + +## 6.0.1 + +* Update result to `True` on iOS when the url was loaded successfully. +* Added a README note about required applications. + +## 6.0.0 + +* Migrate to null safety. +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. +* Correct statement in description about which platforms url_launcher supports. + +## 5.7.13 + +* Update Flutter SDK constraint. + +## 5.7.12 + +* Updated code sample in `README.md` + +## 5.7.11 + +* Update integration test examples to use `testWidgets` instead of `test`. + +## 5.7.10 + +* Update Dart SDK constraint in example. + +## 5.7.9 + +* Check in windows/ directory for example/ + +## 5.7.8 + +* Fixed a situation where an app would crash if the url_launcher’s `launch` method can’t find an app to open the provided url. It will now throw a clear Dart PlatformException. + +## 5.7.7 + +* Introduce the Link widget with an implementation for native platforms. + +## 5.7.6 + +* Suppress deprecation warning on the `shouldOverrideUrlLoading` method on Android of the `FlutterWebChromeClient` class. + +## 5.7.5 + +* Improved documentation of the `headers` parameter. + +## 5.7.4 + +* Update android compileSdkVersion to 29. + +## 5.7.3 + +* Check in linux/ directory for example/ + ## 5.7.2 * Add API documentation explaining the [canLaunch] method returns `false` if package visibility (Android API 30) is not managed correctly. diff --git a/packages/url_launcher/url_launcher/LICENSE b/packages/url_launcher/url_launcher/LICENSE index a6d6c0749818..c6823b81eb84 100644 --- a/packages/url_launcher/url_launcher/LICENSE +++ b/packages/url_launcher/url_launcher/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017 The Chromium Authors. All rights reserved. +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: diff --git a/packages/url_launcher/url_launcher/README.md b/packages/url_launcher/url_launcher/README.md index 811dcd5b4ea1..31fed9a833f1 100644 --- a/packages/url_launcher/url_launcher/README.md +++ b/packages/url_launcher/url_launcher/README.md @@ -1,44 +1,57 @@ # url_launcher -[![pub package](https://img.shields.io/pub/v/url_launcher.svg)](https://pub.dartlang.org/packages/url_launcher) +[![pub package](https://img.shields.io/pub/v/url_launcher.svg)](https://pub.dev/packages/url_launcher) -A Flutter plugin for launching a URL in the mobile platform. Supports +A Flutter plugin for launching a URL. Supports iOS, Android, web, Windows, macOS, and Linux. ## Usage To use this plugin, add `url_launcher` as a [dependency in your pubspec.yaml file](https://flutter.dev/platform-plugins/). +## Installation + +### iOS +Add any URL schemes passed to `canLaunch` as `LSApplicationQueriesSchemes` entries in your Info.plist file. + +Example: +``` +LSApplicationQueriesSchemes + + https + http + +``` + +See [`-[UIApplication canOpenURL:]`](https://developer.apple.com/documentation/uikit/uiapplication/1622952-canopenurl) for more details. + ### Example ``` dart import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; -void main() { - runApp(Scaffold( - body: Center( - child: RaisedButton( - onPressed: _launchURL, - child: Text('Show Flutter homepage'), +const _url = 'https://flutter.dev'; + +void main() => runApp( + const MaterialApp( + home: Material( + child: Center( + child: RaisedButton( + onPressed: _launchURL, + child: Text('Show Flutter homepage'), + ), + ), + ), ), - ), - )); -} - -_launchURL() async { - const url = 'https://flutter.dev'; - if (await canLaunch(url)) { - await launch(url); - } else { - throw 'Could not launch $url'; - } -} + ); +void _launchURL() async => + await canLaunch(_url) ? await launch(_url) : throw 'Could not launch $_url'; ``` ## Supported URL schemes -The [`launch`](https://www.dartdocs.org/documentation/url_launcher/latest/url_launcher/launch.html) method +The [`launch`](https://pub.dev/documentation/url_launcher/latest/url_launcher/launch.html) method takes a string argument containing a URL. This URL can be formatted using a number of different URL schemes. The supported URL schemes depend on the underlying platform and installed apps. @@ -54,6 +67,10 @@ Common schemes supported by both iOS and Android: More details can be found here for [iOS](https://developer.apple.com/library/content/featuredarticles/iPhoneURLScheme_Reference/Introduction/Introduction.html) and [Android](https://developer.android.com/guide/components/intents-common.html) +**Note**: URL schemes are only supported if there are apps installed on the device that can +support them. For example, iOS simulators don't have a default email or phone +apps installed, so can't open `tel:` or `mailto:` links. + ### Encoding URLs URLs must be properly encoded, especially when including spaces or other special characters. This can be done using the [`Uri` class](https://api.dart.dev/stable/2.7.1/dart-core/Uri-class.html): @@ -83,7 +100,7 @@ launching a URL using the `sms` scheme, or a device may not have an email app and thus no support for launching a URL using the `email` scheme. We recommend checking which URL schemes are supported using the -[`canLaunch`](https://www.dartdocs.org/documentation/url_launcher/latest/url_launcher/canLaunch.html) +[`canLaunch`](https://pub.dev/documentation/url_launcher/latest/url_launcher/canLaunch.html) method prior to calling `launch`. If the `canLaunch` method returns false, as a best practice we suggest adjusting the application UI so that the unsupported URL is never triggered; for example, if the `email` scheme is not supported, a @@ -96,4 +113,4 @@ By default, Android opens up a browser when handling URLs. You can pass 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. +else is redirected to the app handler. \ No newline at end of file diff --git a/packages/url_launcher/url_launcher/android/build.gradle b/packages/url_launcher/url_launcher/android/build.gradle index c02b29a5814f..a0225af4491b 100644 --- a/packages/url_launcher/url_launcher/android/build.gradle +++ b/packages/url_launcher/url_launcher/android/build.gradle @@ -4,7 +4,7 @@ version '1.0-SNAPSHOT' buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -15,14 +15,14 @@ buildscript { rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } apply plugin: 'com.android.library' android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { minSdkVersion 16 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 index 0b90dfaf32bc..9e798abcdbb5 100644 --- 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 @@ -1,3 +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. + package io.flutter.plugins.urllauncher; import android.os.Bundle; @@ -92,6 +96,11 @@ private void onLaunch(MethodCall call, Result result, String url) { 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); } 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/src/main/java/io/flutter/plugins/urllauncher/UrlLauncher.java index 40f2a51f1db2..07f7ef3ee7dc 100644 --- a/packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncher.java +++ b/packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncher.java @@ -1,6 +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. + package io.flutter.plugins.urllauncher; import android.app.Activity; +import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -48,7 +53,8 @@ boolean canLaunch(String url) { * @param enableJavaScript Only used if {@param useWebView} is true. Enables JS in the WebView. * @param enableDomStorage Only used if {@param useWebView} is true. Enables DOM storage in the * @return {@link LaunchStatus#NO_ACTIVITY} if there's no available {@code applicationContext}. - * {@link LaunchStatus#OK} otherwise. + * {@link LaunchStatus#ACTIVITY_NOT_FOUND} if there's no activity found to handle {@code + * launchIntent}. {@link LaunchStatus#OK} otherwise. */ LaunchStatus launch( String url, @@ -72,7 +78,12 @@ LaunchStatus launch( .putExtra(Browser.EXTRA_HEADERS, headersBundle); } - activity.startActivity(launchIntent); + try { + activity.startActivity(launchIntent); + } catch (ActivityNotFoundException e) { + return LaunchStatus.ACTIVITY_NOT_FOUND; + } + return LaunchStatus.OK; } @@ -87,5 +98,7 @@ enum LaunchStatus { OK, /** No activity was found to launch. */ NO_ACTIVITY, + /** No Activity found that can handle given intent. */ + ACTIVITY_NOT_FOUND, } } 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/src/main/java/io/flutter/plugins/urllauncher/UrlLauncherPlugin.java index 17af0abda23c..3c9db478e14b 100644 --- a/packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncherPlugin.java +++ b/packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/UrlLauncherPlugin.java @@ -1,3 +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. + package io.flutter.plugins.urllauncher; import android.util.Log; 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/src/main/java/io/flutter/plugins/urllauncher/WebViewActivity.java index 98c5613e6c62..7f26c18740ec 100644 --- a/packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/WebViewActivity.java +++ b/packages/url_launcher/url_launcher/android/src/main/java/io/flutter/plugins/urllauncher/WebViewActivity.java @@ -1,3 +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. + package io.flutter.plugins.urllauncher; import android.annotation.TargetApi; @@ -86,6 +90,11 @@ public boolean shouldOverrideUrlLoading( return true; } + /* + * This method is deprecated in API 24. Still overridden to support + * earlier Android versions. + */ + @SuppressWarnings("deprecation") @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { webview.loadUrl(url); 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 index 63ce46f6d0cb..5e0811399ac6 100644 --- 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 @@ -1,3 +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. + package io.flutter.plugins.urllauncher; import static org.mockito.Matchers.any; @@ -8,6 +12,7 @@ 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; @@ -105,6 +110,95 @@ public void onMethodCall_canLaunchReturnsFalse() { 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); diff --git a/packages/url_launcher/url_launcher/example/README.md b/packages/url_launcher/url_launcher/example/README.md index 28dd90d71700..c200da8974d1 100644 --- a/packages/url_launcher/url_launcher/example/README.md +++ b/packages/url_launcher/url_launcher/example/README.md @@ -5,4 +5,4 @@ Demonstrates how to use the url_launcher plugin. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). diff --git a/packages/url_launcher/url_launcher/example/android/app/build.gradle b/packages/url_launcher/url_launcher/example/android/app/build.gradle index 7a6cf5df0d33..4620c8963b47 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 28 + compileSdkVersion 29 lintOptions { disable 'InvalidPackage' diff --git a/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/EmbeddingV1ActivityTest.java b/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/EmbeddingV1ActivityTest.java index b144786fe925..4fb52708b9eb 100644 --- a/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/EmbeddingV1ActivityTest.java +++ b/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/EmbeddingV1ActivityTest.java @@ -1,3 +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. + package io.flutter.plugins.urllauncherexample; import androidx.test.rule.ActivityTestRule; 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/androidTestDebug/java/io/flutter/plugins/urllauncherexample/MainActivityTest.java index 5b50523f7f40..9e343b82a193 100644 --- 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/androidTestDebug/java/io/flutter/plugins/urllauncherexample/MainActivityTest.java @@ -1,3 +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. + package io.flutter.plugins.urllauncherexample; import androidx.test.rule.ActivityTestRule; diff --git a/packages/url_launcher/url_launcher/example/android/app/src/main/java/io/flutter/plugins/urllauncherexample/EmbeddingV1Activity.java b/packages/url_launcher/url_launcher/example/android/app/src/main/java/io/flutter/plugins/urllauncherexample/EmbeddingV1Activity.java index 9fd4871c0711..969b4713dc16 100644 --- a/packages/url_launcher/url_launcher/example/android/app/src/main/java/io/flutter/plugins/urllauncherexample/EmbeddingV1Activity.java +++ b/packages/url_launcher/url_launcher/example/android/app/src/main/java/io/flutter/plugins/urllauncherexample/EmbeddingV1Activity.java @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/packages/url_launcher/url_launcher/example/android/build.gradle b/packages/url_launcher/url_launcher/example/android/build.gradle index 6b1a639efd76..4d553dd548c7 100644 --- a/packages/url_launcher/url_launcher/example/android/build.gradle +++ b/packages/url_launcher/url_launcher/example/android/build.gradle @@ -1,7 +1,7 @@ buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -12,7 +12,7 @@ buildscript { allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/packages/url_launcher/url_launcher/example/integration_test/url_launcher_test.dart b/packages/url_launcher/url_launcher/example/integration_test/url_launcher_test.dart index 9fb5eaa34b4c..14136996b692 100644 --- a/packages/url_launcher/url_launcher/example/integration_test/url_launcher_test.dart +++ b/packages/url_launcher/url_launcher/example/integration_test/url_launcher_test.dart @@ -1,6 +1,6 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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' show Platform; @@ -12,7 +12,7 @@ import 'package:url_launcher/url_launcher.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - test('canLaunch', () async { + testWidgets('canLaunch', (WidgetTester _) async { expect(await canLaunch('randomstring'), false); // Generally all devices should have some default browser. diff --git a/packages/url_launcher/url_launcher/example/ios/Podfile b/packages/url_launcher/url_launcher/example/ios/Podfile new file mode 100644 index 000000000000..3924e59aa0f9 --- /dev/null +++ b/packages/url_launcher/url_launcher/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/example/ios/Runner.xcodeproj/project.pbxproj b/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/project.pbxproj index db72809a6169..ffc37abef072 100644 --- a/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/project.pbxproj @@ -10,17 +10,33 @@ 2D92223F1EC1DA93007564B0 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D92223E1EC1DA93007564B0 /* GeneratedPluginRegistrant.m */; }; 2E37D9A274B2EACB147AC51B /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 856D0913184F79C678A42603 /* libPods-Runner.a */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 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; @@ -28,8 +44,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -40,7 +54,8 @@ 2D92223D1EC1DA93007564B0 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GeneratedPluginRegistrant.h; path = Runner/GeneratedPluginRegistrant.h; sourceTree = ""; }; 2D92223E1EC1DA93007564B0 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = GeneratedPluginRegistrant.m; path = Runner/GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; + 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 = ""; }; @@ -48,7 +63,6 @@ 856D0913184F79C678A42603 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -56,6 +70,13 @@ 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 */ @@ -63,12 +84,25 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 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 */ @@ -77,6 +111,8 @@ children = ( 836316F9AEA584411312E29F /* Pods-Runner.debug.xcconfig */, A84BFEE343F54B983D1B67EB /* Pods-Runner.release.xcconfig */, + 666BCD7C181C34F8BE58929B /* Pods-RunnerTests.debug.xcconfig */, + D25C434271ACF6555E002440 /* Pods-RunnerTests.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -84,9 +120,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, @@ -101,6 +135,8 @@ 2D92223E1EC1DA93007564B0 /* GeneratedPluginRegistrant.m */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, + F7151F4926604CFB0028CB91 /* RunnerTests */, + F7151F5726604D060028CB91 /* RunnerUITests */, 97C146EF1CF9000F007C117D /* Products */, 840012C8B5EDBCF56B0E4AC1 /* Pods */, CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, @@ -111,6 +147,8 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + F7151F4826604CFB0028CB91 /* RunnerTests.xctest */, + F7151F5626604D060028CB91 /* RunnerUITests.xctest */, ); name = Products; sourceTree = ""; @@ -141,10 +179,29 @@ 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 */ @@ -158,7 +215,6 @@ 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, - 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, ); buildRules = ( @@ -170,6 +226,43 @@ 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 */ @@ -177,10 +270,21 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; + 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; }; }; }; @@ -198,6 +302,8 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + F7151F4726604CFB0028CB91 /* RunnerTests */, + F7151F5526604D060028CB91 /* RunnerUITests */, ); }; /* End PBXProject section */ @@ -214,6 +320,20 @@ ); 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 */ @@ -229,49 +349,56 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "[CP] Embed Pods Frameworks"; + name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - 9740EEB61CF901F6004384FC /* Run Script */ = { + AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", ); - name = "Run Script"; + name = "[CP] Check Pods Manifest.lock"; 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\" build"; + 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; }; - AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */ = { + 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-Runner-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -291,8 +418,37 @@ ); 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; @@ -315,7 +471,6 @@ /* Begin XCBuildConfiguration section */ 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -372,7 +527,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -437,7 +591,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.urlLauncher; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.urlLauncher; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -458,11 +612,67 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.urlLauncher; + 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 */ @@ -484,6 +694,24 @@ 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.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 21a3cc14c74e..919434a6254f 100644 --- a/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,9 +2,6 @@ - - + location = "self:"> diff --git a/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 3bb3697ef41c..c5f1a9de4a30 100644 --- a/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/url_launcher/url_launcher/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -37,6 +37,26 @@ + + + + + + + + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/url_launcher/url_launcher/example/ios/Runner/AppDelegate.h b/packages/url_launcher/url_launcher/example/ios/Runner/AppDelegate.h index d9e18e990f2e..0681d288bb70 100644 --- a/packages/url_launcher/url_launcher/example/ios/Runner/AppDelegate.h +++ b/packages/url_launcher/url_launcher/example/ios/Runner/AppDelegate.h @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/packages/url_launcher/url_launcher/example/ios/Runner/AppDelegate.m b/packages/url_launcher/url_launcher/example/ios/Runner/AppDelegate.m index 9cf1c7796c6a..83f0621aceba 100644 --- a/packages/url_launcher/url_launcher/example/ios/Runner/AppDelegate.m +++ b/packages/url_launcher/url_launcher/example/ios/Runner/AppDelegate.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/packages/url_launcher/url_launcher/example/ios/Runner/main.m b/packages/url_launcher/url_launcher/example/ios/Runner/main.m index bec320c0bee0..f97b9ef5c8a1 100644 --- a/packages/url_launcher/url_launcher/example/ios/Runner/main.m +++ b/packages/url_launcher/url_launcher/example/ios/Runner/main.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/packages/url_launcher/url_launcher/example/ios/RunnerTests/Info.plist b/packages/url_launcher/url_launcher/example/ios/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/url_launcher/url_launcher/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/url_launcher/url_launcher/example/ios/RunnerTests/URLLauncherTests.m b/packages/url_launcher/url_launcher/example/ios/RunnerTests/URLLauncherTests.m new file mode 100644 index 000000000000..746089425f7a --- /dev/null +++ b/packages/url_launcher/url_launcher/example/ios/RunnerTests/URLLauncherTests.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 url_launcher; +@import XCTest; + +@interface URLLauncherTests : XCTestCase +@end + +@implementation URLLauncherTests + +- (void)testPlugin { + FLTURLLauncherPlugin* plugin = [[FLTURLLauncherPlugin alloc] init]; + XCTAssertNotNil(plugin); +} + +@end diff --git a/packages/url_launcher/url_launcher/example/ios/RunnerUITests/Info.plist b/packages/url_launcher/url_launcher/example/ios/RunnerUITests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/url_launcher/url_launcher/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/example/ios/RunnerUITests/URLLauncherUITests.m new file mode 100644 index 000000000000..f7ae5d9250da --- /dev/null +++ b/packages/url_launcher/url_launcher/example/ios/RunnerUITests/URLLauncherUITests.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 XCTest; +@import os.log; + +@interface URLLauncherUITests : XCTestCase +@property(nonatomic, strong) XCUIApplication* app; +@end + +@implementation URLLauncherUITests + +- (void)setUp { + self.continueAfterFailure = NO; + + self.app = [[XCUIApplication alloc] init]; + [self.app launch]; +} + +- (void)testLaunch { + XCUIApplication* app = self.app; + + 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]; + if (![button waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find %@ button", buttonName); + } + XCTAssertEqual(app.webViews.count, 0); + [button tap]; + XCUIElement* webView = app.webViews.firstMatch; + if (![webView waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find webview"); + } + XCTAssertTrue(app.buttons[@"ForwardButton"].exists); + XCTAssertTrue(app.buttons[@"ShareButton"].exists); + XCTAssertTrue(app.buttons[@"OpenInSafariButton"].exists); + [app.buttons[@"Done"] tap]; + } +} + +@end diff --git a/packages/url_launcher/url_launcher/example/lib/main.dart b/packages/url_launcher/url_launcher/example/lib/main.dart index f7d90c4bef65..d593e6d5e001 100644 --- a/packages/url_launcher/url_launcher/example/lib/main.dart +++ b/packages/url_launcher/url_launcher/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -7,6 +7,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:url_launcher/link.dart'; import 'package:url_launcher/url_launcher.dart'; void main() { @@ -27,7 +28,7 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); + MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override @@ -35,7 +36,7 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - Future _launched; + Future? _launched; String _phone = ''; Future _launchInBrowser(String url) async { @@ -141,7 +142,7 @@ class _MyHomePageState extends State { decoration: const InputDecoration( hintText: 'Input the phone number to launch')), ), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _makePhoneCall('tel:$_phone'); }), @@ -151,33 +152,33 @@ class _MyHomePageState extends State { padding: EdgeInsets.all(16.0), child: Text(toLaunch), ), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchInBrowser(toLaunch); }), child: const Text('Launch in browser'), ), const Padding(padding: EdgeInsets.all(16.0)), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchInWebViewOrVC(toLaunch); }), child: const Text('Launch in app'), ), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchInWebViewWithJavaScript(toLaunch); }), child: const Text('Launch in app(JavaScript ON)'), ), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchInWebViewWithDomStorage(toLaunch); }), child: const Text('Launch in app(DOM storage ON)'), ), const Padding(padding: EdgeInsets.all(16.0)), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchUniversalLinkIos(toLaunch); }), @@ -185,7 +186,7 @@ class _MyHomePageState extends State { 'Launch a universal link in a native app, fallback to Safari.(Youtube)'), ), const Padding(padding: EdgeInsets.all(16.0)), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchInWebViewOrVC(toLaunch); Timer(const Duration(seconds: 5), () { @@ -196,6 +197,19 @@ class _MyHomePageState extends State { child: const Text('Launch in app + close after 5 seconds'), ), const Padding(padding: EdgeInsets.all(16.0)), + Link( + uri: Uri.parse( + 'https://pub.dev/documentation/url_launcher/latest/link/link-library.html'), + target: LinkTarget.blank, + builder: (ctx, openLink) { + return TextButton.icon( + onPressed: openLink, + label: Text('Link Widget documentation'), + icon: Icon(Icons.read_more), + ); + }, + ), + const Padding(padding: EdgeInsets.all(16.0)), FutureBuilder(future: _launched, builder: _launchStatus), ], ), diff --git a/packages/url_launcher/url_launcher/example/linux/.gitignore b/packages/url_launcher/url_launcher/example/linux/.gitignore new file mode 100644 index 000000000000..d3896c98444f --- /dev/null +++ b/packages/url_launcher/url_launcher/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral 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 index 36185a63f2fd..f6f23bfe970f 100644 --- 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 @@ -2,13 +2,14 @@ // 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"); + 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 index 9bf7478940c1..e0f0a47bc08f 100644 --- 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 @@ -2,6 +2,8 @@ // Generated file. Do not edit. // +// clang-format off + #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/url_launcher/url_launcher/example/linux/main.cc b/packages/url_launcher/url_launcher/example/linux/main.cc index e7c5c5437037..1507d02825e7 100644 --- a/packages/url_launcher/url_launcher/example/linux/main.cc +++ b/packages/url_launcher/url_launcher/example/linux/main.cc @@ -1,3 +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. + #include "my_application.h" int main(int argc, char** argv) { diff --git a/packages/url_launcher/url_launcher/example/linux/my_application.cc b/packages/url_launcher/url_launcher/example/linux/my_application.cc index f079e19eb396..878cd973d997 100644 --- a/packages/url_launcher/url_launcher/example/linux/my_application.cc +++ b/packages/url_launcher/url_launcher/example/linux/my_application.cc @@ -1,3 +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. + #include "my_application.h" #include diff --git a/packages/url_launcher/url_launcher/example/linux/my_application.h b/packages/url_launcher/url_launcher/example/linux/my_application.h index 72271d5e4170..6e9f0c3ff665 100644 --- a/packages/url_launcher/url_launcher/example/linux/my_application.h +++ b/packages/url_launcher/url_launcher/example/linux/my_application.h @@ -1,3 +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. + #ifndef FLUTTER_MY_APPLICATION_H_ #define FLUTTER_MY_APPLICATION_H_ diff --git a/packages/url_launcher/url_launcher/example/macos/Podfile b/packages/url_launcher/url_launcher/example/macos/Podfile new file mode 100644 index 000000000000..dade8dfad0dc --- /dev/null +++ b/packages/url_launcher/url_launcher/example/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.11' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/url_launcher/url_launcher/example/macos/Runner/AppDelegate.swift b/packages/url_launcher/url_launcher/example/macos/Runner/AppDelegate.swift index d53ef6437726..5cec4c48f620 100644 --- a/packages/url_launcher/url_launcher/example/macos/Runner/AppDelegate.swift +++ b/packages/url_launcher/url_launcher/example/macos/Runner/AppDelegate.swift @@ -1,3 +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 Cocoa import FlutterMacOS diff --git a/packages/url_launcher/url_launcher/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/url_launcher/url_launcher/example/macos/Runner/Configs/AppInfo.xcconfig index eddfd3e0bab0..f19f849dea77 100644 --- a/packages/url_launcher/url_launcher/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/packages/url_launcher/url_launcher/example/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = url_launcher_example_example // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.urlLauncherExample +PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.urlLauncherExample // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2019 io.flutter.plugins. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2019 The Flutter Authors. All rights reserved. diff --git a/packages/url_launcher/url_launcher/example/macos/Runner/MainFlutterWindow.swift b/packages/url_launcher/url_launcher/example/macos/Runner/MainFlutterWindow.swift index 2722837ec918..32aaeedceb1f 100644 --- a/packages/url_launcher/url_launcher/example/macos/Runner/MainFlutterWindow.swift +++ b/packages/url_launcher/url_launcher/example/macos/Runner/MainFlutterWindow.swift @@ -1,3 +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 Cocoa import FlutterMacOS diff --git a/packages/url_launcher/url_launcher/example/pubspec.yaml b/packages/url_launcher/url_launcher/example/pubspec.yaml index b48445c3a7e9..643c53f9a6cb 100644 --- a/packages/url_launcher/url_launcher/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher/example/pubspec.yaml @@ -1,20 +1,30 @@ name: url_launcher_example description: Demonstrates how to use the url_launcher plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.12.13+hotfix.5" dependencies: flutter: sdk: flutter url_launcher: + # When depending on this package from a real application you should use: + # url_launcher: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: integration_test: - path: ../../../integration_test + sdk: flutter flutter_driver: sdk: flutter - pedantic: ^1.8.0 - mockito: ^4.1.1 - plugin_platform_interface: ^1.0.0 + pedantic: ^1.10.0 + mockito: ^5.0.0 + plugin_platform_interface: ^2.0.0 flutter: uses-material-design: true diff --git a/packages/url_launcher/url_launcher/example/test/url_launcher_example_test.dart b/packages/url_launcher/url_launcher/example/test/url_launcher_example_test.dart deleted file mode 100644 index 41b9f6f5ec6c..000000000000 --- a/packages/url_launcher/url_launcher/example/test/url_launcher_example_test.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter/material.dart'; -import 'package:mockito/mockito.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; -import 'package:url_launcher_example/main.dart'; - -void main() { - final MockUrlLauncher mock = MockUrlLauncher(); - UrlLauncherPlatform.instance = mock; - - testWidgets('Can open URLs', (WidgetTester tester) async { - await tester.pumpWidget(MyApp()); - const String defaultUrl = 'https://www.cylog.org/headers/'; - when(mock.canLaunch(defaultUrl)).thenAnswer((_) => Future.value(true)); - const Map defaultHeaders = { - 'my_header_key': 'my_header_value' - }; - verifyNever(mock.launch(defaultUrl, - useSafariVC: false, - useWebView: false, - enableDomStorage: false, - enableJavaScript: false, - universalLinksOnly: false, - headers: defaultHeaders)); - - Finder browserlaunchBtn = - find.widgetWithText(RaisedButton, 'Launch in browser'); - expect(browserlaunchBtn, findsOneWidget); - await tester.tap(browserlaunchBtn); - - verify(mock.launch(defaultUrl, - useSafariVC: false, - useWebView: false, - enableDomStorage: false, - enableJavaScript: false, - universalLinksOnly: false, - headers: defaultHeaders)) - .called(1); - }); -} - -class MockUrlLauncher extends Mock - with MockPlatformInterfaceMixin - implements UrlLauncherPlatform {} diff --git a/packages/url_launcher/url_launcher/example/test_driver/integration_test.dart b/packages/url_launcher/url_launcher/example/test_driver/integration_test.dart index 7a2c21338786..4f10f2a522f3 100644 --- a/packages/url_launcher/url_launcher/example/test_driver/integration_test.dart +++ b/packages/url_launcher/url_launcher/example/test_driver/integration_test.dart @@ -1,17 +1,7 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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'; +import 'package:integration_test/integration_test_driver.dart'; -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String data = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - await driver.close(); - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} +Future main() => integrationDriver(); diff --git a/packages/url_launcher/url_launcher/example/web/index.html b/packages/url_launcher/url_launcher/example/web/index.html index 3d1872c20298..c3d22621fc4f 100644 --- a/packages/url_launcher/url_launcher/example/web/index.html +++ b/packages/url_launcher/url_launcher/example/web/index.html @@ -1,4 +1,7 @@ + diff --git a/packages/url_launcher/url_launcher/example/windows/.gitignore b/packages/url_launcher/url_launcher/example/windows/.gitignore new file mode 100644 index 000000000000..d492d0d98c8f --- /dev/null +++ b/packages/url_launcher/url_launcher/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/url_launcher/url_launcher/example/windows/CMakeLists.txt b/packages/url_launcher/url_launcher/example/windows/CMakeLists.txt new file mode 100644 index 000000000000..abf90408efb4 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/windows/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.15) +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/url_launcher/url_launcher/example/windows/flutter/CMakeLists.txt b/packages/url_launcher/url_launcher/example/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000000..c7a8c7607d81 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,101 @@ +cmake_minimum_required(VERSION 3.15) + +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" +) +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 $ +) +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/url_launcher/url_launcher/example/windows/flutter/generated_plugin_registrant.cc b/packages/url_launcher/url_launcher/example/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 000000000000..d9fdd53925c5 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,14 @@ +// +// 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 new file mode 100644 index 000000000000..dc139d85a931 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// 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 new file mode 100644 index 000000000000..411af46dd721 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,16 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_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/url_launcher/url_launcher/example/windows/runner/CMakeLists.txt b/packages/url_launcher/url_launcher/example/windows/runner/CMakeLists.txt new file mode 100644 index 000000000000..977e38b5d1d2 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/windows/runner/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.15) +project(runner LANGUAGES CXX) + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "run_loop.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/url_launcher/url_launcher/example/windows/runner/Runner.rc b/packages/url_launcher/url_launcher/example/windows/runner/Runner.rc new file mode 100644 index 000000000000..dbda44723259 --- /dev/null +++ b/packages/url_launcher/url_launcher/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", "Flutter Dev" "\0" + VALUE "FileDescription", "A new Flutter project." "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2020 The Flutter Authors. 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/url_launcher/url_launcher/example/windows/runner/flutter_window.cpp b/packages/url_launcher/url_launcher/example/windows/runner/flutter_window.cpp new file mode 100644 index 000000000000..8e415602cf3b --- /dev/null +++ b/packages/url_launcher/url_launcher/example/windows/runner/flutter_window.cpp @@ -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. + +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(RunLoop* run_loop, + const flutter::DartProject& project) + : run_loop_(run_loop), 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()); + run_loop_->RegisterFlutterInstance(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + run_loop_->UnregisterFlutterInstance(flutter_controller_->engine()); + 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 opporutunity 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/url_launcher/url_launcher/example/windows/runner/flutter_window.h b/packages/url_launcher/url_launcher/example/windows/runner/flutter_window.h new file mode 100644 index 000000000000..8e9c12bbe022 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/windows/runner/flutter_window.h @@ -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. + +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "run_loop.h" +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow driven by the |run_loop|, hosting a + // Flutter view running |project|. + explicit FlutterWindow(RunLoop* run_loop, + 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 run loop driving events for this window. + RunLoop* run_loop_; + + // 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/url_launcher/url_launcher/example/windows/runner/main.cpp b/packages/url_launcher/url_launcher/example/windows/runner/main.cpp new file mode 100644 index 000000000000..126302b0be18 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/windows/runner/main.cpp @@ -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. + +#include +#include +#include + +#include "flutter_window.h" +#include "run_loop.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); + + RunLoop run_loop; + + flutter::DartProject project(L"data"); + FlutterWindow window(&run_loop, project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + run_loop.Run(); + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/packages/url_launcher/url_launcher/example/windows/runner/resource.h b/packages/url_launcher/url_launcher/example/windows/runner/resource.h new file mode 100644 index 000000000000..d5d958dc4257 --- /dev/null +++ b/packages/url_launcher/url_launcher/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/url_launcher/url_launcher/example/windows/runner/resources/app_icon.ico b/packages/url_launcher/url_launcher/example/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000000..c04e20caf637 Binary files /dev/null and b/packages/url_launcher/url_launcher/example/windows/runner/resources/app_icon.ico differ diff --git a/packages/url_launcher/url_launcher/example/windows/runner/run_loop.cpp b/packages/url_launcher/url_launcher/example/windows/runner/run_loop.cpp new file mode 100644 index 000000000000..1916500e6440 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/windows/runner/run_loop.cpp @@ -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. + +#include "run_loop.h" + +#include + +#include + +RunLoop::RunLoop() {} + +RunLoop::~RunLoop() {} + +void RunLoop::Run() { + bool keep_running = true; + TimePoint next_flutter_event_time = TimePoint::clock::now(); + while (keep_running) { + std::chrono::nanoseconds wait_duration = + std::max(std::chrono::nanoseconds(0), + next_flutter_event_time - TimePoint::clock::now()); + ::MsgWaitForMultipleObjects( + 0, nullptr, FALSE, static_cast(wait_duration.count() / 1000), + QS_ALLINPUT); + bool processed_events = false; + MSG message; + // All pending Windows messages must be processed; MsgWaitForMultipleObjects + // won't return again for items left in the queue after PeekMessage. + while (::PeekMessage(&message, nullptr, 0, 0, PM_REMOVE)) { + processed_events = true; + if (message.message == WM_QUIT) { + keep_running = false; + break; + } + ::TranslateMessage(&message); + ::DispatchMessage(&message); + // Allow Flutter to process messages each time a Windows message is + // processed, to prevent starvation. + next_flutter_event_time = + std::min(next_flutter_event_time, ProcessFlutterMessages()); + } + // If the PeekMessage loop didn't run, process Flutter messages. + if (!processed_events) { + next_flutter_event_time = + std::min(next_flutter_event_time, ProcessFlutterMessages()); + } + } +} + +void RunLoop::RegisterFlutterInstance( + flutter::FlutterEngine* flutter_instance) { + flutter_instances_.insert(flutter_instance); +} + +void RunLoop::UnregisterFlutterInstance( + flutter::FlutterEngine* flutter_instance) { + flutter_instances_.erase(flutter_instance); +} + +RunLoop::TimePoint RunLoop::ProcessFlutterMessages() { + TimePoint next_event_time = TimePoint::max(); + for (auto instance : flutter_instances_) { + std::chrono::nanoseconds wait_duration = instance->ProcessMessages(); + if (wait_duration != std::chrono::nanoseconds::max()) { + next_event_time = + std::min(next_event_time, TimePoint::clock::now() + wait_duration); + } + } + return next_event_time; +} diff --git a/packages/url_launcher/url_launcher/example/windows/runner/run_loop.h b/packages/url_launcher/url_launcher/example/windows/runner/run_loop.h new file mode 100644 index 000000000000..819ed3ed4995 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/windows/runner/run_loop.h @@ -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. + +#ifndef RUNNER_RUN_LOOP_H_ +#define RUNNER_RUN_LOOP_H_ + +#include + +#include +#include + +// A runloop that will service events for Flutter instances as well +// as native messages. +class RunLoop { + public: + RunLoop(); + ~RunLoop(); + + // Prevent copying + RunLoop(RunLoop const&) = delete; + RunLoop& operator=(RunLoop const&) = delete; + + // Runs the run loop until the application quits. + void Run(); + + // Registers the given Flutter instance for event servicing. + void RegisterFlutterInstance(flutter::FlutterEngine* flutter_instance); + + // Unregisters the given Flutter instance from event servicing. + void UnregisterFlutterInstance(flutter::FlutterEngine* flutter_instance); + + private: + using TimePoint = std::chrono::steady_clock::time_point; + + // Processes all currently pending messages for registered Flutter instances. + TimePoint ProcessFlutterMessages(); + + std::set flutter_instances_; +}; + +#endif // RUNNER_RUN_LOOP_H_ diff --git a/packages/url_launcher/url_launcher/example/windows/runner/runner.exe.manifest b/packages/url_launcher/url_launcher/example/windows/runner/runner.exe.manifest new file mode 100644 index 000000000000..c977c4a42589 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/packages/url_launcher/url_launcher/example/windows/runner/utils.cpp b/packages/url_launcher/url_launcher/example/windows/runner/utils.cpp new file mode 100644 index 000000000000..537728149601 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/windows/runner/utils.cpp @@ -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. + +#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(); + } +} diff --git a/packages/url_launcher/url_launcher/example/windows/runner/utils.h b/packages/url_launcher/url_launcher/example/windows/runner/utils.h new file mode 100644 index 000000000000..16b3f0794597 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/windows/runner/utils.h @@ -0,0 +1,12 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/url_launcher/url_launcher/example/windows/runner/win32_window.cpp b/packages/url_launcher/url_launcher/example/windows/runner/win32_window.cpp new file mode 100644 index 000000000000..a609a2002bb3 --- /dev/null +++ b/packages/url_launcher/url_launcher/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/url_launcher/url_launcher/example/windows/runner/win32_window.h b/packages/url_launcher/url_launcher/example/windows/runner/win32_window.h new file mode 100644 index 000000000000..d2a730052223 --- /dev/null +++ b/packages/url_launcher/url_launcher/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/url_launcher/url_launcher/ios/Classes/FLTURLLauncherPlugin.h b/packages/url_launcher/url_launcher/ios/Classes/FLTURLLauncherPlugin.h index 7ce28f598082..73589d2a0b7d 100644 --- a/packages/url_launcher/url_launcher/ios/Classes/FLTURLLauncherPlugin.h +++ b/packages/url_launcher/url_launcher/ios/Classes/FLTURLLauncherPlugin.h @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/packages/url_launcher/url_launcher/ios/Classes/FLTURLLauncherPlugin.m b/packages/url_launcher/url_launcher/ios/Classes/FLTURLLauncherPlugin.m index 39013b3ca039..6fbc3a522f36 100644 --- a/packages/url_launcher/url_launcher/ios/Classes/FLTURLLauncherPlugin.m +++ b/packages/url_launcher/url_launcher/ios/Classes/FLTURLLauncherPlugin.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -34,7 +34,7 @@ - (instancetype)initWithUrl:url withFlutterResult:result { - (void)safariViewController:(SFSafariViewController *)controller didCompleteInitialLoad:(BOOL)didLoadSuccessfully API_AVAILABLE(ios(9.0)) { if (didLoadSuccessfully) { - self.flutterResult(nil); + self.flutterResult(@YES); } else { self.flutterResult([FlutterError errorWithCode:@"Error" diff --git a/packages/url_launcher/url_launcher/lib/link.dart b/packages/url_launcher/url_launcher/lib/link.dart new file mode 100644 index 000000000000..12a213b62761 --- /dev/null +++ b/packages/url_launcher/url_launcher/lib/link.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/link.dart' show Link; +export 'package:url_launcher_platform_interface/link.dart' + show FollowLink, LinkTarget, LinkWidgetBuilder; diff --git a/packages/url_launcher/url_launcher/lib/src/link.dart b/packages/url_launcher/url_launcher/lib/src/link.dart new file mode 100644 index 000000000000..72d6e247c970 --- /dev/null +++ b/packages/url_launcher/url_launcher/lib/src/link.dart @@ -0,0 +1,138 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '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'; + +/// The function used to push routes to the Flutter framework. +@visibleForTesting +Future Function(Object?, String) pushRouteToFrameworkFunction = + pushRouteNameToFramework; + +/// A widget that renders a real link on the web, and uses WebViews in native +/// platforms to open links. +/// +/// Example link to an external URL: +/// +/// ```dart +/// Link( +/// uri: Uri.parse('https://flutter.dev'), +/// builder: (BuildContext context, FollowLink followLink) => ElevatedButton( +/// onPressed: followLink, +/// // ... other properties here ... +/// )}, +/// ); +/// ``` +/// +/// Example link to a route name within the app: +/// +/// ```dart +/// Link( +/// uri: Uri.parse('/home'), +/// builder: (BuildContext context, FollowLink followLink) => ElevatedButton( +/// onPressed: followLink, +/// // ... other properties here ... +/// )}, +/// ); +/// ``` +class Link extends StatelessWidget implements LinkInfo { + /// Called at build time to construct the widget tree under the link. + final LinkWidgetBuilder builder; + + /// The destination that this link leads to. + final Uri? uri; + + /// The target indicating where to open the link. + final LinkTarget target; + + /// Whether the link is disabled or not. + 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; + } + + @override + Widget build(BuildContext context) { + return _effectiveDelegate(this); + } +} + +/// The default delegate used on non-web platforms. +/// +/// For external URIs, it uses url_launche APIs. For app route names, it uses +/// 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); + + /// Given a [link], creates an instance of [DefaultLinkDelegate]. + /// + /// This is a static method so it can be used as a tear-off. + static DefaultLinkDelegate create(LinkInfo link) { + return DefaultLinkDelegate(link); + } + + /// Information about the link built by the app. + final LinkInfo link; + + bool get _useWebView { + 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) { + // A uri that doesn't have a scheme is an internal route name. In this + // case, we push it via Flutter's navigation system instead of letting the + // browser handle it. + final String routeName = link.uri.toString(); + await pushRouteToFrameworkFunction(context, routeName); + 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, + ); + } else { + FlutterError.reportError(FlutterErrorDetails( + exception: 'Could not launch link $urlString', + stack: StackTrace.current, + library: 'url_launcher', + context: ErrorDescription('during launching a link'), + )); + } + } + + @override + Widget build(BuildContext context) { + return link.builder( + context, + link.isDisabled ? null : () => _followLink(context), + ); + } +} diff --git a/packages/url_launcher/url_launcher/lib/url_launcher.dart b/packages/url_launcher/url_launcher/lib/url_launcher.dart index ccc6833f0941..b59c91d02a1a 100644 --- a/packages/url_launcher/url_launcher/lib/url_launcher.dart +++ b/packages/url_launcher/url_launcher/lib/url_launcher.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -44,6 +44,9 @@ import 'package:url_launcher_platform_interface/url_launcher_platform_interface. /// [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. @@ -59,16 +62,15 @@ import 'package:url_launcher_platform_interface/url_launcher_platform_interface. /// is set to true and the universal link failed to launch. Future launch( String urlString, { - bool forceSafariVC, - bool forceWebView, - bool enableJavaScript, - bool enableDomStorage, - bool universalLinksOnly, - Map headers, - Brightness statusBarBrightness, - String webOnlyWindowName, + bool? forceSafariVC, + bool forceWebView = false, + bool enableJavaScript = false, + bool enableDomStorage = false, + bool universalLinksOnly = false, + Map headers = const {}, + Brightness? statusBarBrightness, + String? webOnlyWindowName, }) async { - assert(urlString != null); final Uri url = Uri.parse(urlString.trimLeft()); final bool isWebURL = url.scheme == 'http' || url.scheme == 'https'; if ((forceSafariVC == true || forceWebView == true) && !isWebURL) { @@ -81,29 +83,32 @@ Future launch( /// [true] so that ui is automatically computed if [statusBarBrightness] is set. bool previousAutomaticSystemUiAdjustment = true; if (statusBarBrightness != null && - defaultTargetPlatform == TargetPlatform.iOS) { + defaultTargetPlatform == TargetPlatform.iOS && + WidgetsBinding.instance != null) { previousAutomaticSystemUiAdjustment = - WidgetsBinding.instance.renderView.automaticSystemUiAdjustment; - WidgetsBinding.instance.renderView.automaticSystemUiAdjustment = false; + WidgetsBinding.instance!.renderView.automaticSystemUiAdjustment; + WidgetsBinding.instance!.renderView.automaticSystemUiAdjustment = false; SystemChrome.setSystemUIOverlayStyle(statusBarBrightness == Brightness.light ? SystemUiOverlayStyle.dark : SystemUiOverlayStyle.light); } + final bool result = await UrlLauncherPlatform.instance.launch( urlString, useSafariVC: forceSafariVC ?? isWebURL, - useWebView: forceWebView ?? false, - enableJavaScript: enableJavaScript ?? false, - enableDomStorage: enableDomStorage ?? false, - universalLinksOnly: universalLinksOnly ?? false, - headers: headers ?? {}, + useWebView: forceWebView, + enableJavaScript: enableJavaScript, + enableDomStorage: enableDomStorage, + universalLinksOnly: universalLinksOnly, + headers: headers, webOnlyWindowName: webOnlyWindowName, ); - assert(previousAutomaticSystemUiAdjustment != null); - if (statusBarBrightness != null) { - WidgetsBinding.instance.renderView.automaticSystemUiAdjustment = + + if (statusBarBrightness != null && WidgetsBinding.instance != null) { + WidgetsBinding.instance!.renderView.automaticSystemUiAdjustment = previousAutomaticSystemUiAdjustment; } + return result; } @@ -115,9 +120,6 @@ Future launch( /// For more information see the [Managing package visibility](https://developer.android.com/training/basics/intents/package-visibility) /// article in the Android docs. Future canLaunch(String urlString) async { - if (urlString == null) { - return false; - } return await UrlLauncherPlatform.instance.canLaunch(urlString); } diff --git a/packages/url_launcher/url_launcher/macos/url_launcher.podspec b/packages/url_launcher/url_launcher/macos/url_launcher.podspec deleted file mode 100644 index 2ddd8ced06d1..000000000000 --- a/packages/url_launcher/url_launcher/macos/url_launcher.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 = 'url_launcher' - s.version = '0.0.1' - s.summary = 'No-op implementation of the macos url_launcher to avoid build issues on macos' - s.description = <<-DESC - No-op implementation of the url_launcher plugin to avoid build issues on macos. - https://github.com/flutter/flutter/issues/46618 - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/url_launcher' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - - s.platform = :osx - s.osx.deployment_target = '10.11' -end - diff --git a/packages/url_launcher/url_launcher/pubspec.yaml b/packages/url_launcher/url_launcher/pubspec.yaml index 0b6a50f8b8fd..00cfc218ce9e 100644 --- a/packages/url_launcher/url_launcher/pubspec.yaml +++ b/packages/url_launcher/url_launcher/pubspec.yaml @@ -1,8 +1,13 @@ name: url_launcher -description: Flutter plugin for launching a URL on Android and iOS. Supports +description: Flutter plugin for launching a URL. Supports web, phone, SMS, and email schemes. -homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher -version: 5.7.2 +repository: https://github.com/flutter/plugins/tree/master/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.6 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: @@ -12,37 +17,34 @@ flutter: pluginClass: UrlLauncherPlugin ios: pluginClass: FLTURLLauncherPlugin - web: - default_package: url_launcher_web linux: default_package: url_laucher_linux macos: default_package: url_laucher_macos + web: + default_package: url_launcher_web windows: default_package: url_laucher_windows dependencies: flutter: sdk: flutter - url_launcher_platform_interface: ^1.0.8 + meta: ^1.3.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 + # 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). + # TODO(amirh): Revisit this (either update this part in the design or the pub tool). # https://github.com/flutter/flutter/issues/46264 - url_launcher_web: ^0.1.3 - url_launcher_linux: ^0.0.1 - url_launcher_macos: ^0.0.1 - url_launcher_windows: ^0.0.1 + url_launcher_linux: ^2.0.0 + url_launcher_macos: ^2.0.0 + url_launcher_platform_interface: ^2.0.3 + url_launcher_web: ^2.0.0 + url_launcher_windows: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - test: ^1.3.0 - mockito: ^4.1.1 - plugin_platform_interface: ^1.0.0 - pedantic: ^1.8.0 - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + 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 new file mode 100644 index 000000000000..819f6a370e30 --- /dev/null +++ b/packages/url_launcher/url_launcher/test/link_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/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter/services.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'; + +void main() { + late MockUrlLauncher mock; + + setUp(() { + mock = MockUrlLauncher(); + UrlLauncherPlatform.instance = mock; + }); + + group('$Link', () { + testWidgets('handles null uri correctly', (WidgetTester tester) async { + bool isBuilt = false; + FollowLink? followLink; + + final Link link = Link( + uri: null, + builder: (BuildContext context, FollowLink? followLink2) { + isBuilt = true; + followLink = followLink2; + return Container(); + }, + ); + await tester.pumpWidget(link); + + expect(link.isDisabled, isTrue); + expect(isBuilt, isTrue); + expect(followLink, isNull); + }); + + testWidgets('calls url_launcher for external URLs with blank target', + (WidgetTester tester) async { + FollowLink? followLink; + + await tester.pumpWidget(Link( + uri: Uri.parse('http://example.com/foobar'), + target: LinkTarget.blank, + builder: (BuildContext context, FollowLink? followLink2) { + followLink = followLink2; + return Container(); + }, + )); + + mock + ..setLaunchExpectations( + url: 'http://example.com/foobar', + useSafariVC: false, + useWebView: false, + universalLinksOnly: false, + enableJavaScript: false, + enableDomStorage: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + await followLink!(); + expect(mock.canLaunchCalled, isTrue); + expect(mock.launchCalled, isTrue); + }); + + testWidgets('calls url_launcher for external URLs with self target', + (WidgetTester tester) async { + FollowLink? followLink; + + await tester.pumpWidget(Link( + uri: Uri.parse('http://example.com/foobar'), + target: LinkTarget.self, + builder: (BuildContext context, FollowLink? followLink2) { + followLink = followLink2; + return Container(); + }, + )); + + mock + ..setLaunchExpectations( + url: 'http://example.com/foobar', + useSafariVC: true, + useWebView: true, + universalLinksOnly: false, + enableJavaScript: false, + enableDomStorage: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + await followLink!(); + expect(mock.canLaunchCalled, isTrue); + expect(mock.launchCalled, isTrue); + }); + + testWidgets('pushes to framework for internal route names', + (WidgetTester tester) async { + final Uri uri = Uri.parse('/foo/bar'); + FollowLink? followLink; + + await tester.pumpWidget(MaterialApp( + routes: { + '/': (BuildContext context) => Link( + uri: uri, + builder: (BuildContext context, FollowLink? followLink2) { + followLink = followLink2; + return Container(); + }, + ), + '/foo/bar': (BuildContext context) => Container(), + }, + )); + + bool frameworkCalled = false; + Future Function(Object?, String) originalPushFunction = + pushRouteToFrameworkFunction; + pushRouteToFrameworkFunction = (Object? _, String __) { + frameworkCalled = true; + return Future.value(ByteData(0)); + }; + + await followLink!(); + + // Shouldn't use url_launcher when uri is an internal route name. + expect(mock.canLaunchCalled, isFalse); + expect(mock.launchCalled, isFalse); + + // A route should have been pushed to the framework. + expect(frameworkCalled, true); + + // Restore the original function. + pushRouteToFrameworkFunction = originalPushFunction; + }); + }); +} diff --git a/packages/url_launcher/url_launcher/test/mock_url_launcher_platform.dart b/packages/url_launcher/url_launcher/test/mock_url_launcher_platform.dart new file mode 100644 index 000000000000..789c1435df80 --- /dev/null +++ b/packages/url_launcher/url_launcher/test/mock_url_launcher_platform.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. + +import 'package:flutter_test/flutter_test.dart'; +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'; + +class MockUrlLauncher extends Fake + with MockPlatformInterfaceMixin + implements UrlLauncherPlatform { + String? url; + bool? useSafariVC; + bool? useWebView; + bool? enableJavaScript; + bool? enableDomStorage; + bool? universalLinksOnly; + Map? headers; + String? webOnlyWindowName; + + bool? response; + + bool closeWebViewCalled = false; + bool canLaunchCalled = false; + bool launchCalled = false; + + void setCanLaunchExpectations(String url) { + this.url = url; + } + + void setLaunchExpectations({ + required String url, + required bool? useSafariVC, + required bool useWebView, + required bool enableJavaScript, + required bool enableDomStorage, + required bool universalLinksOnly, + required Map headers, + required String? webOnlyWindowName, + }) { + this.url = url; + this.useSafariVC = useSafariVC; + this.useWebView = useWebView; + this.enableJavaScript = enableJavaScript; + this.enableDomStorage = enableDomStorage; + this.universalLinksOnly = universalLinksOnly; + this.headers = headers; + this.webOnlyWindowName = webOnlyWindowName; + } + + void setResponse(bool response) { + this.response = response; + } + + @override + LinkDelegate? get linkDelegate => null; + + @override + Future canLaunch(String url) async { + expect(url, this.url); + canLaunchCalled = true; + return response!; + } + + @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 { + expect(url, this.url); + expect(useSafariVC, this.useSafariVC); + expect(useWebView, this.useWebView); + expect(enableJavaScript, this.enableJavaScript); + expect(enableDomStorage, this.enableDomStorage); + expect(universalLinksOnly, this.universalLinksOnly); + expect(headers, this.headers); + expect(webOnlyWindowName, this.webOnlyWindowName); + launchCalled = true; + return response!; + } + + @override + Future closeWebView() async { + closeWebViewCalled = true; + } +} diff --git a/packages/url_launcher/url_launcher/test/url_launcher_test.dart b/packages/url_launcher/url_launcher/test/url_launcher_test.dart index 9d01b43d8e72..9b2d167483cd 100644 --- a/packages/url_launcher/url_launcher/test/url_launcher_test.dart +++ b/packages/url_launcher/url_launcher/test/url_launcher_test.dart @@ -1,29 +1,32 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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:mockito/mockito.dart'; import 'package:flutter/foundation.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.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(); - verify(mock.closeWebView()); + expect(mock.closeWebViewCalled, isTrue); }); group('canLaunch', () { test('returns true', () async { - when(mock.canLaunch('foo')).thenAnswer((_) => Future.value(true)); + mock + ..setCanLaunchExpectations('foo') + ..setResponse(true); final bool result = await canLaunch('foo'); @@ -31,7 +34,9 @@ void main() { }); test('returns false', () async { - when(mock.canLaunch('foo')).thenAnswer((_) => Future.value(false)); + mock + ..setCanLaunchExpectations('foo') + ..setResponse(false); final bool result = await canLaunch('foo'); @@ -39,151 +44,146 @@ void main() { }); }); group('launch', () { - test('requires a non-null urlString', () { - expect(() => launch(null), throwsAssertionError); - }); - test('default behavior', () async { - await launch('http://flutter.dev/'); - expect( - verify(mock.launch( - captureAny, - useSafariVC: captureAnyNamed('useSafariVC'), - useWebView: captureAnyNamed('useWebView'), - enableJavaScript: captureAnyNamed('enableJavaScript'), - enableDomStorage: captureAnyNamed('enableDomStorage'), - universalLinksOnly: captureAnyNamed('universalLinksOnly'), - headers: captureAnyNamed('headers'), - )).captured, - [ - 'http://flutter.dev/', - true, - false, - false, - false, - false, - {}, - ], - ); + 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 { - await launch( - 'http://flutter.dev/', - headers: {'key': 'value'}, - ); + mock + ..setLaunchExpectations( + url: 'http://flutter.dev/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: {'key': 'value'}, + webOnlyWindowName: null, + ) + ..setResponse(true); expect( - verify(mock.launch( - any, - useSafariVC: anyNamed('useSafariVC'), - useWebView: anyNamed('useWebView'), - enableJavaScript: anyNamed('enableJavaScript'), - enableDomStorage: anyNamed('enableDomStorage'), - universalLinksOnly: anyNamed('universalLinksOnly'), - headers: captureAnyNamed('headers'), - )).captured.single, - {'key': 'value'}, - ); + await launch( + 'http://flutter.dev/', + headers: {'key': 'value'}, + ), + isTrue); }); test('force SafariVC', () async { - await launch('http://flutter.dev/', forceSafariVC: true); - expect( - verify(mock.launch( - any, - useSafariVC: captureAnyNamed('useSafariVC'), - useWebView: anyNamed('useWebView'), - enableJavaScript: anyNamed('enableJavaScript'), - enableDomStorage: anyNamed('enableDomStorage'), - universalLinksOnly: anyNamed('universalLinksOnly'), - headers: anyNamed('headers'), - )).captured.single, - true, - ); + 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 { - await launch('http://flutter.dev/', - forceSafariVC: false, universalLinksOnly: true); + mock + ..setLaunchExpectations( + url: 'http://flutter.dev/', + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: true, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); expect( - verify(mock.launch( - any, - useSafariVC: captureAnyNamed('useSafariVC'), - useWebView: anyNamed('useWebView'), - enableJavaScript: anyNamed('enableJavaScript'), - enableDomStorage: anyNamed('enableDomStorage'), - universalLinksOnly: captureAnyNamed('universalLinksOnly'), - headers: anyNamed('headers'), - )).captured, - [false, true], - ); + await launch('http://flutter.dev/', + forceSafariVC: false, universalLinksOnly: true), + isTrue); }); test('force WebView', () async { - await launch('http://flutter.dev/', forceWebView: true); - expect( - verify(mock.launch( - any, - useSafariVC: anyNamed('useSafariVC'), - useWebView: captureAnyNamed('useWebView'), - enableJavaScript: anyNamed('enableJavaScript'), - enableDomStorage: anyNamed('enableDomStorage'), - universalLinksOnly: anyNamed('universalLinksOnly'), - headers: anyNamed('headers'), - )).captured.single, - true, - ); + 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 { - await launch('http://flutter.dev/', - forceWebView: true, enableJavaScript: true); + mock + ..setLaunchExpectations( + url: 'http://flutter.dev/', + useSafariVC: true, + useWebView: true, + enableJavaScript: true, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); expect( - verify(mock.launch( - any, - useSafariVC: anyNamed('useSafariVC'), - useWebView: captureAnyNamed('useWebView'), - enableJavaScript: captureAnyNamed('enableJavaScript'), - enableDomStorage: anyNamed('enableDomStorage'), - universalLinksOnly: anyNamed('universalLinksOnly'), - headers: anyNamed('headers'), - )).captured, - [true, true], - ); + await launch('http://flutter.dev/', + forceWebView: true, enableJavaScript: true), + isTrue); }); test('force WebView enable DOM storage', () async { - await launch('http://flutter.dev/', - forceWebView: true, enableDomStorage: true); + mock + ..setLaunchExpectations( + url: 'http://flutter.dev/', + useSafariVC: true, + useWebView: true, + enableJavaScript: false, + enableDomStorage: true, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); expect( - verify(mock.launch( - any, - useSafariVC: anyNamed('useSafariVC'), - useWebView: captureAnyNamed('useWebView'), - enableJavaScript: anyNamed('enableJavaScript'), - enableDomStorage: captureAnyNamed('enableDomStorage'), - universalLinksOnly: anyNamed('universalLinksOnly'), - headers: anyNamed('headers'), - )).captured, - [true, true], - ); + await launch('http://flutter.dev/', + forceWebView: true, enableDomStorage: true), + isTrue); }); test('force SafariVC to false', () async { - await launch('http://flutter.dev/', forceSafariVC: false); - expect( - // ignore: missing_required_param - verify(mock.launch( - any, - useSafariVC: captureAnyNamed('useSafariVC'), - useWebView: anyNamed('useWebView'), - enableJavaScript: anyNamed('enableJavaScript'), - enableDomStorage: anyNamed('enableDomStorage'), - universalLinksOnly: anyNamed('universalLinksOnly'), - headers: anyNamed('headers'), - )).captured.single, - false, - ); + 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 { @@ -191,9 +191,56 @@ void main() { 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 = - TestWidgetsFlutterBinding.ensureInitialized(); + TestWidgetsFlutterBinding.ensureInitialized() + as TestWidgetsFlutterBinding; debugDefaultTargetPlatformOverride = TargetPlatform.iOS; binding.renderView.automaticSystemUiAdjustment = true; final Future launchResult = @@ -207,8 +254,22 @@ void main() { }); 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 = - TestWidgetsFlutterBinding.ensureInitialized(); + TestWidgetsFlutterBinding.ensureInitialized() + as TestWidgetsFlutterBinding; debugDefaultTargetPlatformOverride = TargetPlatform.android; expect(binding.renderView.automaticSystemUiAdjustment, true); final Future launchResult = @@ -222,7 +283,3 @@ void main() { }); }); } - -class MockUrlLauncher extends Mock - with MockPlatformInterfaceMixin - implements UrlLauncherPlatform {} diff --git a/packages/url_launcher/url_launcher_linux/AUTHORS b/packages/url_launcher/url_launcher_linux/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/url_launcher/url_launcher_linux/CHANGELOG.md b/packages/url_launcher/url_launcher_linux/CHANGELOG.md index e03b1d387c55..ec9fad53437c 100644 --- a/packages/url_launcher/url_launcher_linux/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_linux/CHANGELOG.md @@ -1,3 +1,30 @@ +## 2.0.0 + +* Migrate to null safety. +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) +* Set `implementation` in pubspec.yaml + +## 0.0.2+1 + +* Update Flutter SDK constraint. + +## 0.0.2 + +* Update integration test examples to use `testWidgets` instead of `test`. + +## 0.0.1+4 + +* Update Dart SDK constraint in example. + +## 0.0.1+3 + +* Add a missing include. + +## 0.0.1+2 + +* Check in linux/ directory for example/ + # 0.0.1+1 * README update for endorsement by url_launcher. diff --git a/packages/url_launcher/url_launcher_linux/LICENSE b/packages/url_launcher/url_launcher_linux/LICENSE index d7412e0a1e0c..c6823b81eb84 100644 --- a/packages/url_launcher/url_launcher_linux/LICENSE +++ b/packages/url_launcher/url_launcher_linux/LICENSE @@ -1,4 +1,4 @@ -Copyright 2020 The Chromium Authors. All rights reserved. +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: diff --git a/packages/url_launcher/url_launcher_linux/example/README.md b/packages/url_launcher/url_launcher_linux/example/README.md index 28dd90d71700..c200da8974d1 100644 --- a/packages/url_launcher/url_launcher_linux/example/README.md +++ b/packages/url_launcher/url_launcher_linux/example/README.md @@ -5,4 +5,4 @@ Demonstrates how to use the url_launcher plugin. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). diff --git a/packages/url_launcher/url_launcher_linux/example/integration_test/url_launcher_test.dart b/packages/url_launcher/url_launcher_linux/example/integration_test/url_launcher_test.dart index 0b25516c2a29..ae9a9148f9d7 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 @@ -1,23 +1,20 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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/url_launcher.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - test('canLaunch', () async { - expect(await canLaunch('randomstring'), false); + testWidgets('canLaunch', (WidgetTester _) async { + UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; - // Generally all devices should have some default browser. - expect(await canLaunch('http://flutter.dev'), true); - - // Desktop will not necessarily support sms:. + expect(await launcher.canLaunch('randomstring'), false); - // tel: and mailto: links may not be openable on every device. iOS - // simulators notably can't open these link types. + // Generally all devices should have some default browser. + expect(await launcher.canLaunch('http://flutter.dev'), true); }); } diff --git a/packages/url_launcher/url_launcher_linux/example/lib/main.dart b/packages/url_launcher/url_launcher_linux/example/lib/main.dart index b5cce7482d07..86e06f3fafed 100644 --- a/packages/url_launcher/url_launcher_linux/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_linux/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -6,7 +6,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; void main() { runApp(MyApp()); @@ -26,7 +26,7 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); + MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override @@ -34,77 +34,24 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - Future _launched; - String _phone = ''; + Future? _launched; Future _launchInBrowser(String url) async { - if (await canLaunch(url)) { - await launch( + if (await UrlLauncherPlatform.instance.canLaunch(url)) { + await UrlLauncherPlatform.instance.launch( url, - forceSafariVC: false, - forceWebView: false, - headers: {'my_header_key': 'my_header_value'}, + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, ); } else { throw 'Could not launch $url'; } } - Future _launchInWebViewOrVC(String url) async { - if (await canLaunch(url)) { - await launch( - url, - forceSafariVC: true, - forceWebView: true, - headers: {'my_header_key': 'my_header_value'}, - ); - } else { - throw 'Could not launch $url'; - } - } - - Future _launchInWebViewWithJavaScript(String url) async { - if (await canLaunch(url)) { - await launch( - url, - forceSafariVC: true, - forceWebView: true, - enableJavaScript: true, - ); - } else { - throw 'Could not launch $url'; - } - } - - Future _launchInWebViewWithDomStorage(String url) async { - if (await canLaunch(url)) { - await launch( - url, - forceSafariVC: true, - forceWebView: true, - enableDomStorage: true, - ); - } else { - throw 'Could not launch $url'; - } - } - - Future _launchUniversalLinkIos(String url) async { - if (await canLaunch(url)) { - final bool nativeAppLaunchSucceeded = await launch( - url, - forceSafariVC: false, - universalLinksOnly: true, - ); - if (!nativeAppLaunchSucceeded) { - await launch( - url, - forceSafariVC: true, - ); - } - } - } - Widget _launchStatus(BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); @@ -113,14 +60,6 @@ class _MyHomePageState extends State { } } - Future _makePhoneCall(String url) async { - if (await canLaunch(url)) { - await launch(url); - } else { - throw 'Could not launch $url'; - } - } - @override Widget build(BuildContext context) { const String toLaunch = 'https://www.cylog.org/headers/'; @@ -133,57 +72,17 @@ class _MyHomePageState extends State { Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: TextField( - onChanged: (String text) => _phone = text, - decoration: const InputDecoration( - hintText: 'Input the phone number to launch')), - ), - RaisedButton( - onPressed: () => setState(() { - _launched = _makePhoneCall('tel:$_phone'); - }), - child: const Text('Make phone call'), - ), const Padding( padding: EdgeInsets.all(16.0), child: Text(toLaunch), ), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchInBrowser(toLaunch); }), child: const Text('Launch in browser'), ), const Padding(padding: EdgeInsets.all(16.0)), - RaisedButton( - onPressed: () => setState(() { - _launched = _launchInWebViewOrVC(toLaunch); - }), - child: const Text('Launch in app'), - ), - RaisedButton( - onPressed: () => setState(() { - _launched = _launchInWebViewWithJavaScript(toLaunch); - }), - child: const Text('Launch in app(JavaScript ON)'), - ), - RaisedButton( - onPressed: () => setState(() { - _launched = _launchInWebViewWithDomStorage(toLaunch); - }), - child: const Text('Launch in app(DOM storage ON)'), - ), - const Padding(padding: EdgeInsets.all(16.0)), - RaisedButton( - onPressed: () => setState(() { - _launched = _launchUniversalLinkIos(toLaunch); - }), - child: const Text( - 'Launch a universal link in a native app, fallback to Safari.(Youtube)'), - ), - const Padding(padding: EdgeInsets.all(16.0)), FutureBuilder(future: _launched, builder: _launchStatus), ], ), diff --git a/packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugin_registrant.cc b/packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugin_registrant.cc index 36185a63f2fd..f6f23bfe970f 100644 --- 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 @@ -2,13 +2,14 @@ // 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"); + 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 index 9bf7478940c1..e0f0a47bc08f 100644 --- 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 @@ -2,6 +2,8 @@ // Generated file. Do not edit. // +// clang-format off + #ifndef GENERATED_PLUGIN_REGISTRANT_ #define GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/url_launcher/url_launcher_linux/example/linux/main.cc b/packages/url_launcher/url_launcher_linux/example/linux/main.cc index e7c5c5437037..1507d02825e7 100644 --- a/packages/url_launcher/url_launcher_linux/example/linux/main.cc +++ b/packages/url_launcher/url_launcher_linux/example/linux/main.cc @@ -1,3 +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. + #include "my_application.h" int main(int argc, char** argv) { diff --git a/packages/url_launcher/url_launcher_linux/example/linux/my_application.cc b/packages/url_launcher/url_launcher_linux/example/linux/my_application.cc index f079e19eb396..878cd973d997 100644 --- a/packages/url_launcher/url_launcher_linux/example/linux/my_application.cc +++ b/packages/url_launcher/url_launcher_linux/example/linux/my_application.cc @@ -1,3 +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. + #include "my_application.h" #include diff --git a/packages/url_launcher/url_launcher_linux/example/linux/my_application.h b/packages/url_launcher/url_launcher_linux/example/linux/my_application.h index 72271d5e4170..6e9f0c3ff665 100644 --- a/packages/url_launcher/url_launcher_linux/example/linux/my_application.h +++ b/packages/url_launcher/url_launcher_linux/example/linux/my_application.h @@ -1,3 +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. + #ifndef FLUTTER_MY_APPLICATION_H_ #define FLUTTER_MY_APPLICATION_H_ diff --git a/packages/url_launcher/url_launcher_linux/example/pubspec.yaml b/packages/url_launcher/url_launcher_linux/example/pubspec.yaml index c9d0c32f5cb6..b0ef2e6eddbf 100644 --- a/packages/url_launcher/url_launcher_linux/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_linux/example/pubspec.yaml @@ -1,19 +1,29 @@ name: url_launcher_example description: Demonstrates how to use the url_launcher plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: sdk: flutter - url_launcher: any url_launcher_linux: + # When depending on this package from a real application you should use: + # url_launcher_linux: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ + url_launcher_platform_interface: ^2.0.0 dev_dependencies: integration_test: - path: ../../../integration_test + sdk: flutter flutter_driver: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/url_launcher/url_launcher_linux/example/test_driver/integration_test.dart b/packages/url_launcher/url_launcher_linux/example/test_driver/integration_test.dart index 7a2c21338786..4f10f2a522f3 100644 --- a/packages/url_launcher/url_launcher_linux/example/test_driver/integration_test.dart +++ b/packages/url_launcher/url_launcher_linux/example/test_driver/integration_test.dart @@ -1,17 +1,7 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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'; +import 'package:integration_test/integration_test_driver.dart'; -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String data = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - await driver.close(); - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} +Future main() => integrationDriver(); diff --git a/packages/url_launcher/url_launcher_linux/ios/.gitignore b/packages/url_launcher/url_launcher_linux/ios/.gitignore deleted file mode 100644 index aa479fd3ce8a..000000000000 --- a/packages/url_launcher/url_launcher_linux/ios/.gitignore +++ /dev/null @@ -1,37 +0,0 @@ -.idea/ -.vagrant/ -.sconsign.dblite -.svn/ - -.DS_Store -*.swp -profile - -DerivedData/ -build/ -GeneratedPluginRegistrant.h -GeneratedPluginRegistrant.m - -.generated/ - -*.pbxuser -*.mode1v3 -*.mode2v3 -*.perspectivev3 - -!default.pbxuser -!default.mode1v3 -!default.mode2v3 -!default.perspectivev3 - -xcuserdata - -*.moved-aside - -*.pyc -*sync/ -Icon? -.tags* - -/Flutter/Generated.xcconfig -/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/packages/url_launcher/url_launcher_linux/ios/url_launcher_linux.podspec b/packages/url_launcher/url_launcher_linux/ios/url_launcher_linux.podspec deleted file mode 100644 index 1359fd403d8d..000000000000 --- a/packages/url_launcher/url_launcher_linux/ios/url_launcher_linux.podspec +++ /dev/null @@ -1,22 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint url_launcher_linux.podspec' to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'url_launcher_linux' - s.version = '0.0.1' - s.summary = 'url_launcher_linux iOS stub' - s.description = <<-DESC - No-op implementation of the Linux url_launcher plugin to avoid build issues on iOS - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_linux' } - s.dependency 'Flutter' - s.platform = :ios, '8.0' - - # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } - s.swift_version = '5.0' -end diff --git a/packages/url_launcher/url_launcher_linux/lib/url_launcher_linux.dart b/packages/url_launcher/url_launcher_linux/lib/url_launcher_linux.dart deleted file mode 100644 index 18f7af1836ce..000000000000 --- a/packages/url_launcher/url_launcher_linux/lib/url_launcher_linux.dart +++ /dev/null @@ -1,3 +0,0 @@ -// The url_launcher_platform_interface defaults to MethodChannelUrlLauncher -// as its instance, which is all the Linux implementation needs. This file -// is here to silence warnings when publishing to pub. diff --git a/packages/url_launcher/url_launcher_linux/linux/include/url_launcher_linux/url_launcher_plugin.h b/packages/url_launcher/url_launcher_linux/linux/include/url_launcher_linux/url_launcher_plugin.h index efcfe62e706a..f4d19395e37f 100644 --- a/packages/url_launcher/url_launcher_linux/linux/include/url_launcher_linux/url_launcher_plugin.h +++ b/packages/url_launcher/url_launcher_linux/linux/include/url_launcher_linux/url_launcher_plugin.h @@ -1,4 +1,4 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/packages/url_launcher/url_launcher_linux/linux/url_launcher_plugin.cc b/packages/url_launcher/url_launcher_linux/linux/url_launcher_plugin.cc index 592bb965e83f..6e10607dd14e 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 @@ -1,4 +1,4 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -7,6 +7,8 @@ #include #include +#include + // See url_launcher_channel.dart for documentation. const char kChannelName[] = "plugins.flutter.io/url_launcher"; const char kBadArgumentsError[] = "Bad Arguments"; diff --git a/packages/url_launcher/url_launcher_linux/pubspec.yaml b/packages/url_launcher/url_launcher_linux/pubspec.yaml index 74c2968aba69..a5d6ddd24ff4 100644 --- a/packages/url_launcher/url_launcher_linux/pubspec.yaml +++ b/packages/url_launcher/url_launcher_linux/pubspec.yaml @@ -1,19 +1,20 @@ name: url_launcher_linux description: Linux implementation of the url_launcher plugin. -version: 0.0.1+1 -homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_linux +repository: https://github.com/flutter/plugins/tree/master/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.0 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: + implements: url_launcher platforms: linux: pluginClass: UrlLauncherPlugin -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" - dependencies: flutter: sdk: flutter - diff --git a/packages/url_launcher/url_launcher_macos/AUTHORS b/packages/url_launcher/url_launcher_macos/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/url_launcher/url_launcher_macos/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/url_launcher/url_launcher_macos/CHANGELOG.md b/packages/url_launcher/url_launcher_macos/CHANGELOG.md index d52bf8c249e6..976f7719329b 100644 --- a/packages/url_launcher/url_launcher_macos/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_macos/CHANGELOG.md @@ -1,3 +1,25 @@ +## NEXT + +* Add native unit tests. + +## 2.0.0 + +* Migrate to null safety. +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. +* Set `implementation` in pubspec.yaml + +## 0.0.2+1 + +* Update Flutter SDK constraint. + +## 0.0.2 + +* Update integration test examples to use `testWidgets` instead of `test`. + +# 0.0.1+9 + +* Update Dart SDK constraint in example. + # 0.0.1+8 * Remove no-op android folder in the example app. @@ -34,4 +56,3 @@ # 0.0.1 * Initial open source release. - diff --git a/packages/url_launcher/url_launcher_macos/LICENSE b/packages/url_launcher/url_launcher_macos/LICENSE index 507569823f1b..c6823b81eb84 100644 --- a/packages/url_launcher/url_launcher_macos/LICENSE +++ b/packages/url_launcher/url_launcher_macos/LICENSE @@ -1,4 +1,4 @@ -Copyright 2019 The Chromium Authors. All rights reserved. +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: diff --git a/packages/url_launcher/url_launcher_macos/README.md b/packages/url_launcher/url_launcher_macos/README.md index e0c326ba86bf..28aa18817d6c 100644 --- a/packages/url_launcher/url_launcher_macos/README.md +++ b/packages/url_launcher/url_launcher_macos/README.md @@ -2,13 +2,6 @@ The macos implementation of [`url_launcher`][1]. -**Please set your constraint to `url_launcher_macos: '>=0.0.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.0.y+z`. -Please use `url_launcher_macos: '>=0.0.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ## Usage ### Import the package diff --git a/packages/url_launcher/url_launcher_macos/example/README.md b/packages/url_launcher/url_launcher_macos/example/README.md index 28dd90d71700..c200da8974d1 100644 --- a/packages/url_launcher/url_launcher_macos/example/README.md +++ b/packages/url_launcher/url_launcher_macos/example/README.md @@ -5,4 +5,4 @@ Demonstrates how to use the url_launcher plugin. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). diff --git a/packages/url_launcher/url_launcher_macos/example/integration_test/url_launcher_test.dart b/packages/url_launcher/url_launcher_macos/example/integration_test/url_launcher_test.dart index 676b78c3bbfb..897b22f89392 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 @@ -1,24 +1,23 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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/url_launcher.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - test('canLaunch', () async { - expect(await canLaunch('randomstring'), false); + testWidgets('canLaunch', (WidgetTester _) async { + UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; + + expect(await launcher.canLaunch('randomstring'), false); // Generally all devices should have some default browser. - expect(await canLaunch('http://flutter.dev'), true); + expect(await launcher.canLaunch('http://flutter.dev'), true); // Generally all devices should have some default SMS app. - expect(await canLaunch('sms:5555555555'), true); - - // tel: and mailto: links may not be openable on every device. iOS - // simulators notably can't open these link types. + expect(await launcher.canLaunch('sms:5555555555'), true); }); } diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Flutter/AppFrameworkInfo.plist b/packages/url_launcher/url_launcher_macos/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6c2de8086bcd..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - UIRequiredDeviceCapabilities - - arm64 - - MinimumOSVersion - 8.0 - - diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Flutter/Debug.xcconfig b/packages/url_launcher/url_launcher_macos/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index 9803018ca79d..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Generated.xcconfig" -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Flutter/Release.xcconfig b/packages/url_launcher/url_launcher_macos/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index a4a8c604e13d..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Generated.xcconfig" -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner.xcodeproj/project.pbxproj b/packages/url_launcher/url_launcher_macos/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index db72809a6169..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,490 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 2D92223F1EC1DA93007564B0 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D92223E1EC1DA93007564B0 /* GeneratedPluginRegistrant.m */; }; - 2E37D9A274B2EACB147AC51B /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 856D0913184F79C678A42603 /* libPods-Runner.a */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 2D92223D1EC1DA93007564B0 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = GeneratedPluginRegistrant.h; path = Runner/GeneratedPluginRegistrant.h; sourceTree = ""; }; - 2D92223E1EC1DA93007564B0 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = GeneratedPluginRegistrant.m; path = Runner/GeneratedPluginRegistrant.m; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 836316F9AEA584411312E29F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 856D0913184F79C678A42603 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - A84BFEE343F54B983D1B67EB /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - 2E37D9A274B2EACB147AC51B /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 840012C8B5EDBCF56B0E4AC1 /* Pods */ = { - isa = PBXGroup; - children = ( - 836316F9AEA584411312E29F /* Pods-Runner.debug.xcconfig */, - A84BFEE343F54B983D1B67EB /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B80C3931E831B6300D905FE /* App.framework */, - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 2D92223D1EC1DA93007564B0 /* GeneratedPluginRegistrant.h */, - 2D92223E1EC1DA93007564B0 /* GeneratedPluginRegistrant.m */, - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 840012C8B5EDBCF56B0E4AC1 /* Pods */, - CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - CF3B75C9A7D2FA2A4C99F110 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 856D0913184F79C678A42603 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; - }; - 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; - AB1344B0443C71CD721E1BB7 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 2D92223F1EC1DA93007564B0 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.urlLauncher; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.urlLauncher; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/url_launcher/url_launcher_macos/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 3bb3697ef41c..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/AppDelegate.h b/packages/url_launcher/url_launcher_macos/example/ios/Runner/AppDelegate.h deleted file mode 100644 index d9e18e990f2e..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,10 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/AppDelegate.m b/packages/url_launcher/url_launcher_macos/example/ios/Runner/AppDelegate.m deleted file mode 100644 index 9cf1c7796c6a..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - [super application:application didFinishLaunchingWithOptions:launchOptions]; - return YES; -} - -@end diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d22f10b2ab63..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,116 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf03016f..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca8..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cde12118dda..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb8..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index dcdc2306c285..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a9..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 6a84f41e14e2..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index d0e1f5853602..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index ebf48f603974..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Base.lproj/Main.storyboard b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516fb38..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Info.plist b/packages/url_launcher/url_launcher_macos/example/ios/Runner/Info.plist deleted file mode 100644 index 80aec052fa79..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - url_launcher_example - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/url_launcher/url_launcher_macos/example/ios/Runner/main.m b/packages/url_launcher/url_launcher_macos/example/ios/Runner/main.m deleted file mode 100644 index bec320c0bee0..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -#import -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/url_launcher/url_launcher_macos/example/lib/main.dart b/packages/url_launcher/url_launcher_macos/example/lib/main.dart index b5cce7482d07..86e06f3fafed 100644 --- a/packages/url_launcher/url_launcher_macos/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_macos/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -6,7 +6,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; void main() { runApp(MyApp()); @@ -26,7 +26,7 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); + MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override @@ -34,77 +34,24 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - Future _launched; - String _phone = ''; + Future? _launched; Future _launchInBrowser(String url) async { - if (await canLaunch(url)) { - await launch( + if (await UrlLauncherPlatform.instance.canLaunch(url)) { + await UrlLauncherPlatform.instance.launch( url, - forceSafariVC: false, - forceWebView: false, - headers: {'my_header_key': 'my_header_value'}, + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, ); } else { throw 'Could not launch $url'; } } - Future _launchInWebViewOrVC(String url) async { - if (await canLaunch(url)) { - await launch( - url, - forceSafariVC: true, - forceWebView: true, - headers: {'my_header_key': 'my_header_value'}, - ); - } else { - throw 'Could not launch $url'; - } - } - - Future _launchInWebViewWithJavaScript(String url) async { - if (await canLaunch(url)) { - await launch( - url, - forceSafariVC: true, - forceWebView: true, - enableJavaScript: true, - ); - } else { - throw 'Could not launch $url'; - } - } - - Future _launchInWebViewWithDomStorage(String url) async { - if (await canLaunch(url)) { - await launch( - url, - forceSafariVC: true, - forceWebView: true, - enableDomStorage: true, - ); - } else { - throw 'Could not launch $url'; - } - } - - Future _launchUniversalLinkIos(String url) async { - if (await canLaunch(url)) { - final bool nativeAppLaunchSucceeded = await launch( - url, - forceSafariVC: false, - universalLinksOnly: true, - ); - if (!nativeAppLaunchSucceeded) { - await launch( - url, - forceSafariVC: true, - ); - } - } - } - Widget _launchStatus(BuildContext context, AsyncSnapshot snapshot) { if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); @@ -113,14 +60,6 @@ class _MyHomePageState extends State { } } - Future _makePhoneCall(String url) async { - if (await canLaunch(url)) { - await launch(url); - } else { - throw 'Could not launch $url'; - } - } - @override Widget build(BuildContext context) { const String toLaunch = 'https://www.cylog.org/headers/'; @@ -133,57 +72,17 @@ class _MyHomePageState extends State { Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: TextField( - onChanged: (String text) => _phone = text, - decoration: const InputDecoration( - hintText: 'Input the phone number to launch')), - ), - RaisedButton( - onPressed: () => setState(() { - _launched = _makePhoneCall('tel:$_phone'); - }), - child: const Text('Make phone call'), - ), const Padding( padding: EdgeInsets.all(16.0), child: Text(toLaunch), ), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchInBrowser(toLaunch); }), child: const Text('Launch in browser'), ), const Padding(padding: EdgeInsets.all(16.0)), - RaisedButton( - onPressed: () => setState(() { - _launched = _launchInWebViewOrVC(toLaunch); - }), - child: const Text('Launch in app'), - ), - RaisedButton( - onPressed: () => setState(() { - _launched = _launchInWebViewWithJavaScript(toLaunch); - }), - child: const Text('Launch in app(JavaScript ON)'), - ), - RaisedButton( - onPressed: () => setState(() { - _launched = _launchInWebViewWithDomStorage(toLaunch); - }), - child: const Text('Launch in app(DOM storage ON)'), - ), - const Padding(padding: EdgeInsets.all(16.0)), - RaisedButton( - onPressed: () => setState(() { - _launched = _launchUniversalLinkIos(toLaunch); - }), - child: const Text( - 'Launch a universal link in a native app, fallback to Safari.(Youtube)'), - ), - const Padding(padding: EdgeInsets.all(16.0)), FutureBuilder(future: _launched, builder: _launchStatus), ], ), diff --git a/packages/url_launcher/url_launcher_macos/example/macos/Podfile b/packages/url_launcher/url_launcher_macos/example/macos/Podfile new file mode 100644 index 000000000000..e8da8332969a --- /dev/null +++ b/packages/url_launcher/url_launcher_macos/example/macos/Podfile @@ -0,0 +1,44 @@ +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__)) + + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/packages/url_launcher/url_launcher_macos/example/macos/Runner.xcodeproj/project.pbxproj b/packages/url_launcher/url_launcher_macos/example/macos/Runner.xcodeproj/project.pbxproj index a95e62daada1..88c678b4a15d 100644 --- a/packages/url_launcher/url_launcher_macos/example/macos/Runner.xcodeproj/project.pbxproj +++ b/packages/url_launcher/url_launcher_macos/example/macos/Runner.xcodeproj/project.pbxproj @@ -26,10 +26,8 @@ 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, ); }; }; + 33EBD3B9267296CB0013E557 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33EBD3B8267296CB0013E557 /* RunnerTests.swift */; }; + B0E8018BA137CF3E1D668F89 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5A7581585AB49438450A8105 /* Pods_RunnerTests.framework */; }; DD4A1B9DEDBB72C87CD7AE27 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5067D74CB28D28AE3B3DD05B /* Pods_Runner.framework */; }; /* End PBXBuildFile section */ @@ -41,6 +39,13 @@ remoteGlobalIDString = 33CC111A2044C6BA0003C045; remoteInfo = FLX; }; + 33EBD3BB267296CB0013E557 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -50,8 +55,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - D73912F222F3801D000D13A0 /* App.framework in Bundle Framework */, - 33D1A10522148B93006C7A3E /* FlutterMacOS.framework in Bundle Framework */, ); name = "Bundle Framework"; runOnlyForDeploymentPostprocessing = 0; @@ -59,6 +62,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 220FFDB920A73FF04EA40119 /* 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 = ""; }; 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 /* url_launcher_example_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = url_launcher_example_example.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -70,17 +74,21 @@ 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 = ""; }; + 33EBD3B6267296CB0013E557 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 33EBD3B8267296CB0013E557 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 33EBD3BA267296CB0013E557 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 5067D74CB28D28AE3B3DD05B /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 53F020549CA1E801ACA3428F /* 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 = ""; }; + 5A7581585AB49438450A8105 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5CFCAA4A883B5A0C4BD62DCF /* 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 = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 899489AD6AA35AECA4E2BEA6 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; B36FDC1D769C9045B8821207 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - D73912EF22F37F9E000D13A0 /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/ephemeral/App.framework; sourceTree = SOURCE_ROOT; }; + FD671DB5E266C257DCC5AD6A /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -88,12 +96,18 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - D73912F022F37F9E000D13A0 /* App.framework in Frameworks */, - 33D1A10422148B71006C7A3E /* FlutterMacOS.framework in Frameworks */, DD4A1B9DEDBB72C87CD7AE27 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; + 33EBD3B3267296CB0013E557 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + B0E8018BA137CF3E1D668F89 /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -113,6 +127,7 @@ children = ( 33FAB671232836740065AC1E /* Runner */, 33CEB47122A05771004F2AC0 /* Flutter */, + 33EBD3B7267296CB0013E557 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, 96C1F6D923BD5787E8EBE8FC /* Pods */, @@ -123,6 +138,7 @@ isa = PBXGroup; children = ( 33CC10ED2044A3C60003C045 /* url_launcher_example_example.app */, + 33EBD3B6267296CB0013E557 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -145,12 +161,19 @@ 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - D73912EF22F37F9E000D13A0 /* App.framework */, - 33D1A10322148B71006C7A3E /* FlutterMacOS.framework */, ); path = Flutter; sourceTree = ""; }; + 33EBD3B7267296CB0013E557 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 33EBD3B8267296CB0013E557 /* RunnerTests.swift */, + 33EBD3BA267296CB0013E557 /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; 33FAB671232836740065AC1E /* Runner */ = { isa = PBXGroup; children = ( @@ -170,8 +193,10 @@ 899489AD6AA35AECA4E2BEA6 /* Pods-Runner.debug.xcconfig */, B36FDC1D769C9045B8821207 /* Pods-Runner.release.xcconfig */, 53F020549CA1E801ACA3428F /* Pods-Runner.profile.xcconfig */, + 220FFDB920A73FF04EA40119 /* Pods-RunnerTests.debug.xcconfig */, + 5CFCAA4A883B5A0C4BD62DCF /* Pods-RunnerTests.release.xcconfig */, + FD671DB5E266C257DCC5AD6A /* Pods-RunnerTests.profile.xcconfig */, ); - name = Pods; path = Pods; sourceTree = ""; }; @@ -179,6 +204,7 @@ isa = PBXGroup; children = ( 5067D74CB28D28AE3B3DD05B /* Pods_Runner.framework */, + 5A7581585AB49438450A8105 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -208,13 +234,32 @@ productReference = 33CC10ED2044A3C60003C045 /* url_launcher_example_example.app */; productType = "com.apple.product-type.application"; }; + 33EBD3B5267296CB0013E557 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33EBD3C0267296CB0013E557 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 460A36EFD54BEB8122DDAC6D /* [CP] Check Pods Manifest.lock */, + 33EBD3B2267296CB0013E557 /* Sources */, + 33EBD3B3267296CB0013E557 /* Frameworks */, + 33EBD3B4267296CB0013E557 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 33EBD3BC267296CB0013E557 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 33EBD3B6267296CB0013E557 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 33CC10E52044A3C60003C045 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 0920; + LastSwiftUpdateCheck = 1250; LastUpgradeCheck = 0930; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { @@ -232,6 +277,10 @@ CreatedOnToolsVersion = 9.2; ProvisioningStyle = Manual; }; + 33EBD3B5267296CB0013E557 = { + CreatedOnToolsVersion = 12.5; + TestTargetID = 33CC10EC2044A3C60003C045; + }; }; }; buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; @@ -249,6 +298,7 @@ targets = ( 33CC10EC2044A3C60003C045 /* Runner */, 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + 33EBD3B5267296CB0013E557 /* RunnerTests */, ); }; /* End PBXProject section */ @@ -263,6 +313,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 33EBD3B4267296CB0013E557 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -281,7 +338,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename\n"; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; }; 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; @@ -303,16 +360,41 @@ shellPath = /bin/sh; shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh\ntouch Flutter/ephemeral/tripwire\n"; }; - 50C74DCD840D9B569BE3D48F /* [CP] Embed Pods Frameworks */ = { + 460A36EFD54BEB8122DDAC6D /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); - name = "[CP] Embed Pods Frameworks"; + 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; + }; + 50C74DCD840D9B569BE3D48F /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/url_launcher_macos/url_launcher_macos.framework", + ); + name = "[CP] Embed Pods Frameworks"; + outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/url_launcher_macos.framework", + ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; @@ -353,6 +435,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 33EBD3B2267296CB0013E557 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33EBD3B9267296CB0013E557 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -361,6 +451,11 @@ target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; }; + 33EBD3BC267296CB0013E557 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 33EBD3BB267296CB0013E557 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -615,6 +710,63 @@ }; name = Release; }; + 33EBD3BD267296CB0013E557 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 220FFDB920A73FF04EA40119 /* Pods-RunnerTests.debug.xcconfig */; + 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)/url_launcher_example_example.app/Contents/MacOS/url_launcher_example_example"; + }; + name = Debug; + }; + 33EBD3BE267296CB0013E557 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 5CFCAA4A883B5A0C4BD62DCF /* Pods-RunnerTests.release.xcconfig */; + 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)/url_launcher_example_example.app/Contents/MacOS/url_launcher_example_example"; + }; + name = Release; + }; + 33EBD3BF267296CB0013E557 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FD671DB5E266C257DCC5AD6A /* Pods-RunnerTests.profile.xcconfig */; + 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)/url_launcher_example_example.app/Contents/MacOS/url_launcher_example_example"; + }; + name = Profile; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -648,6 +800,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 33EBD3C0267296CB0013E557 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33EBD3BD267296CB0013E557 /* Debug */, + 33EBD3BE267296CB0013E557 /* Release */, + 33EBD3BF267296CB0013E557 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 33CC10E52044A3C60003C045 /* Project object */; diff --git a/packages/url_launcher/url_launcher_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/url_launcher/url_launcher_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 660c47db95c3..323d07b817b1 100644 --- a/packages/url_launcher/url_launcher_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/packages/url_launcher/url_launcher_macos/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -27,6 +27,15 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> + + + + @@ -38,18 +47,17 @@ ReferencedContainer = "container:Runner.xcodeproj"> + + + + - - - - - - - - + + diff --git a/packages/url_launcher/url_launcher_macos/example/macos/Runner/AppDelegate.swift b/packages/url_launcher/url_launcher_macos/example/macos/Runner/AppDelegate.swift index d53ef6437726..5cec4c48f620 100644 --- a/packages/url_launcher/url_launcher_macos/example/macos/Runner/AppDelegate.swift +++ b/packages/url_launcher/url_launcher_macos/example/macos/Runner/AppDelegate.swift @@ -1,3 +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 Cocoa import FlutterMacOS diff --git a/packages/url_launcher/url_launcher_macos/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/url_launcher/url_launcher_macos/example/macos/Runner/Configs/AppInfo.xcconfig index eddfd3e0bab0..f19f849dea77 100644 --- a/packages/url_launcher/url_launcher_macos/example/macos/Runner/Configs/AppInfo.xcconfig +++ b/packages/url_launcher/url_launcher_macos/example/macos/Runner/Configs/AppInfo.xcconfig @@ -8,7 +8,7 @@ PRODUCT_NAME = url_launcher_example_example // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.urlLauncherExample +PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.urlLauncherExample // The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2019 io.flutter.plugins. All rights reserved. +PRODUCT_COPYRIGHT = Copyright © 2019 The Flutter Authors. All rights reserved. diff --git a/packages/url_launcher/url_launcher_macos/example/macos/Runner/MainFlutterWindow.swift b/packages/url_launcher/url_launcher_macos/example/macos/Runner/MainFlutterWindow.swift index 2722837ec918..32aaeedceb1f 100644 --- a/packages/url_launcher/url_launcher_macos/example/macos/Runner/MainFlutterWindow.swift +++ b/packages/url_launcher/url_launcher_macos/example/macos/Runner/MainFlutterWindow.swift @@ -1,3 +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 Cocoa import FlutterMacOS diff --git a/packages/url_launcher/url_launcher_macos/example/macos/RunnerTests/Info.plist b/packages/url_launcher/url_launcher_macos/example/macos/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/url_launcher/url_launcher_macos/example/macos/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/url_launcher/url_launcher_macos/example/macos/RunnerTests/RunnerTests.swift b/packages/url_launcher/url_launcher_macos/example/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 000000000000..d08f66464454 --- /dev/null +++ b/packages/url_launcher/url_launcher_macos/example/macos/RunnerTests/RunnerTests.swift @@ -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 FlutterMacOS +import XCTest +import url_launcher_macos + +class RunnerTests: XCTestCase { + func testCanLaunch() throws { + let plugin = UrlLauncherPlugin() + let call = FlutterMethodCall( + methodName: "canLaunch", + arguments: ["url": "https://flutter.dev"]) + var canLaunch: Bool? + plugin.handle( + call, + result: { (result: Any?) -> Void in + canLaunch = result as? Bool + }) + + XCTAssertTrue(canLaunch == true) + } +} diff --git a/packages/url_launcher/url_launcher_macos/example/pubspec.yaml b/packages/url_launcher/url_launcher_macos/example/pubspec.yaml index 0fab67e8af1b..6d12b75819b0 100644 --- a/packages/url_launcher/url_launcher_macos/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_macos/example/pubspec.yaml @@ -1,19 +1,29 @@ name: url_launcher_example description: Demonstrates how to use the url_launcher plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: sdk: flutter - url_launcher: any url_launcher_macos: + # When depending on this package from a real application you should use: + # url_launcher_macos: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ + url_launcher_platform_interface: ^2.0.0 dev_dependencies: integration_test: - path: ../../../integration_test + sdk: flutter flutter_driver: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/url_launcher/url_launcher_macos/example/test_driver/integration_test.dart b/packages/url_launcher/url_launcher_macos/example/test_driver/integration_test.dart index 7a2c21338786..4f10f2a522f3 100644 --- a/packages/url_launcher/url_launcher_macos/example/test_driver/integration_test.dart +++ b/packages/url_launcher/url_launcher_macos/example/test_driver/integration_test.dart @@ -1,17 +1,7 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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'; +import 'package:integration_test/integration_test_driver.dart'; -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String data = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - await driver.close(); - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} +Future main() => integrationDriver(); diff --git a/packages/url_launcher/url_launcher_macos/ios/url_launcher_macos.podspec b/packages/url_launcher/url_launcher_macos/ios/url_launcher_macos.podspec deleted file mode 100644 index 2bfe79708555..000000000000 --- a/packages/url_launcher/url_launcher_macos/ios/url_launcher_macos.podspec +++ /dev/null @@ -1,21 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'url_launcher_macos' - s.version = '0.0.1' - s.summary = 'No-op implementation of the macos url_launcher plugin to avoid build issues on iOS' - s.description = <<-DESC - No-op implementation of the macos url_launcher plugin to avoid build issues on iOS - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_macos' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.ios.deployment_target = '8.0' -end - diff --git a/packages/url_launcher/url_launcher_macos/lib/url_launcher_macos.dart b/packages/url_launcher/url_launcher_macos/lib/url_launcher_macos.dart deleted file mode 100644 index 5a1956c9a9c1..000000000000 --- a/packages/url_launcher/url_launcher_macos/lib/url_launcher_macos.dart +++ /dev/null @@ -1,3 +0,0 @@ -// The url_launcher_platform_interface defaults to MethodChannelUrlLauncher -// as its instance, which is all the macOS implementation needs. This file -// is here to silence warnings when publishing to pub. 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 916f5c9aa22f..ab89038fa01d 100644 --- a/packages/url_launcher/url_launcher_macos/macos/Classes/UrlLauncherPlugin.swift +++ b/packages/url_launcher/url_launcher_macos/macos/Classes/UrlLauncherPlugin.swift @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/packages/url_launcher/url_launcher_macos/pubspec.yaml b/packages/url_launcher/url_launcher_macos/pubspec.yaml index 4775f23fe36a..4a0eac109ab5 100644 --- a/packages/url_launcher/url_launcher_macos/pubspec.yaml +++ b/packages/url_launcher/url_launcher_macos/pubspec.yaml @@ -1,22 +1,21 @@ name: url_launcher_macos description: macOS implementation of the url_launcher plugin. -# 0.0.y+z is compatible with 1.0.0, if you land a breaking change bump -# the version to 2.0.0. -# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.0.1+8 -homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_macos +repository: https://github.com/flutter/plugins/tree/master/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.0 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: + implements: url_launcher platforms: macos: pluginClass: UrlLauncherPlugin fileName: url_launcher_macos.dart -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" - dependencies: flutter: sdk: flutter diff --git a/packages/url_launcher/url_launcher_platform_interface/AUTHORS b/packages/url_launcher/url_launcher_platform_interface/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/url_launcher/url_launcher_platform_interface/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md index 768042be4cef..06a2efecc500 100644 --- a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md @@ -1,3 +1,27 @@ +## 2.0.3 + +* Migrate `pushRouteNameToFramework` to use ChannelBuffers API. + +## 2.0.2 + +* Update platform_plugin_interface version requirement. + +## 2.0.1 + +* Fix SDK range. + +## 2.0.0 + +* Migrate to null safety. + +## 1.0.10 + +* Update Flutter SDK constraint. + +## 1.0.9 + +* Laid the groundwork for introducing a Link widget. + ## 1.0.8 * Added webOnlyWindowName parameter diff --git a/packages/url_launcher/url_launcher_platform_interface/LICENSE b/packages/url_launcher/url_launcher_platform_interface/LICENSE index a6d6c0749818..c6823b81eb84 100644 --- a/packages/url_launcher/url_launcher_platform_interface/LICENSE +++ b/packages/url_launcher/url_launcher_platform_interface/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017 The Chromium Authors. All rights reserved. +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: diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/link.dart b/packages/url_launcher/url_launcher_platform_interface/lib/link.dart new file mode 100644 index 000000000000..4a414ae78f1f --- /dev/null +++ b/packages/url_launcher/url_launcher_platform_interface/lib/link.dart @@ -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. + +import 'dart:async'; +import 'dart:ui' as ui; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; + +/// Signature for a function provided by the [Link] widget that instructs it to +/// follow the link. +typedef FollowLink = Future Function(); + +/// Signature for a builder function passed to the [Link] widget to construct +/// the widget tree under it. +typedef LinkWidgetBuilder = Widget Function( + BuildContext context, + FollowLink? followLink, +); + +/// Signature for a delegate function to build the [Link] widget. +typedef LinkDelegate = Widget Function(LinkInfo linkWidget); + +final MethodCodec _codec = const JSONMethodCodec(); + +/// Defines where a Link URL should be open. +/// +/// This is a class instead of an enum to allow future customizability e.g. +/// opening a link in a specific iframe. +class LinkTarget { + /// Const private constructor with a [debugLabel] to allow the creation of + /// multiple distinct const instances. + const LinkTarget._({required this.debugLabel}); + + /// Used to distinguish multiple const instances of [LinkTarget]. + final String debugLabel; + + /// Use the default target for each platform. + /// + /// On Android, the default is [blank]. On the web, the default is [self]. + /// + /// iOS, on the other hand, defaults to [self] for web URLs, and [blank] for + /// non-web URLs. + static const 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'); + + /// 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'); +} + +/// Encapsulates all the information necessary to build a Link widget. +abstract class LinkInfo { + /// Called at build time to construct the widget tree under the link. + LinkWidgetBuilder get builder; + + /// The destination that this link leads to. + Uri? get uri; + + /// The target indicating where to open the link. + LinkTarget get target; + + /// Whether the link is disabled or not. + bool get isDisabled; +} + +typedef _SendMessage = Function(String, ByteData?, void Function(ByteData?)); + +/// Pushes the [routeName] into Flutter's navigation system via a platform +/// message. +/// +/// The platform is notified using [SystemNavigator.routeInformationUpdated]. On +/// older versions of Flutter, this means it will not work unless the +/// application uses a [Router] (e.g. using [MaterialApp.router]). +/// +/// Returns the raw data returned by the framework. +// TODO(ianh): Remove the first argument. +Future pushRouteNameToFramework(Object? _, String routeName) { + final Completer completer = Completer(); + SystemNavigator.routeInformationUpdated(location: routeName); + final _SendMessage sendMessage = + WidgetsBinding.instance?.platformDispatcher.onPlatformMessage ?? + ui.channelBuffers.push; + sendMessage( + 'flutter/navigation', + _codec.encodeMethodCall( + MethodCall('pushRouteInformation', { + 'location': routeName, + 'state': null, + }), + ), + completer.complete, + ); + return completer.future; +} 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 f87630ee3045..e75e283eeca7 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 @@ -1,24 +1,27 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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' show required; +import 'link.dart'; import 'url_launcher_platform_interface.dart'; const MethodChannel _channel = MethodChannel('plugins.flutter.io/url_launcher'); /// An implementation of [UrlLauncherPlatform] that uses method channels. class MethodChannelUrlLauncher extends UrlLauncherPlatform { + @override + final LinkDelegate? linkDelegate = null; + @override Future canLaunch(String url) { return _channel.invokeMethod( 'canLaunch', {'url': url}, - ); + ).then((value) => value ?? false); } @override @@ -29,13 +32,13 @@ class MethodChannelUrlLauncher extends UrlLauncherPlatform { @override Future launch( String url, { - @required bool useSafariVC, - @required bool useWebView, - @required bool enableJavaScript, - @required bool enableDomStorage, - @required bool universalLinksOnly, - @required Map headers, - String webOnlyWindowName, + required bool useSafariVC, + required bool useWebView, + required bool enableJavaScript, + required bool enableDomStorage, + required bool universalLinksOnly, + required Map headers, + String? webOnlyWindowName, }) { return _channel.invokeMethod( 'launch', @@ -48,6 +51,6 @@ class MethodChannelUrlLauncher extends UrlLauncherPlatform { 'universalLinksOnly': universalLinksOnly, 'headers': headers, }, - ); + ).then((value) => value ?? false); } } diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart b/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart index 1de5742c1f6f..e9435b8dc4e3 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 @@ -1,11 +1,11 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:async'; -import 'package:meta/meta.dart' show required; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:url_launcher_platform_interface/link.dart'; import 'method_channel_url_launcher.dart'; @@ -38,6 +38,9 @@ abstract class UrlLauncherPlatform extends PlatformInterface { _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.'); @@ -49,13 +52,13 @@ abstract class UrlLauncherPlatform extends PlatformInterface { /// in `package:url_launcher/url_launcher.dart`. Future launch( String url, { - @required bool useSafariVC, - @required bool useWebView, - @required bool enableJavaScript, - @required bool enableDomStorage, - @required bool universalLinksOnly, - @required Map headers, - String webOnlyWindowName, + required bool useSafariVC, + required bool useWebView, + required bool enableJavaScript, + required bool enableDomStorage, + required bool universalLinksOnly, + required Map headers, + String? webOnlyWindowName, }) { throw UnimplementedError('launch() has not been implemented.'); } diff --git a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml index 0c4096278bcb..9bb30c60f405 100644 --- a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml +++ b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml @@ -1,22 +1,22 @@ name: url_launcher_platform_interface description: A common platform interface for the url_launcher plugin. -homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_platform_interface +repository: https://github.com/flutter/plugins/tree/master/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: 1.0.8 +version: 2.0.3 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" dependencies: flutter: sdk: flutter - meta: ^1.0.5 - plugin_platform_interface: ^1.0.1 + plugin_platform_interface: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - mockito: ^4.1.1 - pedantic: ^1.8.0 - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.9.1+hotfix.4 <2.0.0" + 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 new file mode 100644 index 000000000000..75a14b2e11a6 --- /dev/null +++ b/packages/url_launcher/url_launcher_platform_interface/test/link_test.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:collection'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +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')), + routes: { + '/a': (BuildContext context) => Placeholder(key: Key('a')), + }, + )); + expect(find.byKey(Key('home')), findsOneWidget); + expect(find.byKey(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); + }); + + testWidgets('Link with Navigator', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp.router( + routeInformationParser: _RouteInformationParser(), + routerDelegate: _RouteDelegate(), + )); + expect(find.byKey(Key('/')), findsOneWidget); + expect(find.byKey(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); + }); +} + +class _RouteInformationParser extends RouteInformationParser { + @override + Future parseRouteInformation( + RouteInformation routeInformation) { + return SynchronousFuture(routeInformation); + } + + @override + RouteInformation? restoreRouteInformation(RouteInformation configuration) { + return configuration; + } +} + +class _RouteDelegate extends RouterDelegate + with ChangeNotifier { + final Queue _history = Queue(); + + @override + Future setNewRoutePath(RouteInformation configuration) { + _history.add(configuration); + return SynchronousFuture(null); + } + + @override + Future popRoute() { + if (_history.isEmpty) { + return SynchronousFuture(false); + } + _history.removeLast(); + return SynchronousFuture(true); + } + + @override + Widget build(BuildContext context) { + if (_history.isEmpty) { + return 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 628ab48498ec..5bfc78c5c5a2 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -7,6 +7,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.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'; @@ -41,6 +42,10 @@ void main() { final List log = []; channel.setMockMethodCallHandler((MethodCall methodCall) async { log.add(methodCall); + + // Return null explicitly instead of relying on the implicit null + // returned by the method channel if no return statement is specified. + return null; }); final MethodChannelUrlLauncher launcher = MethodChannelUrlLauncher(); @@ -61,6 +66,12 @@ void main() { ); }); + test('canLaunch should return false if platform returns null', () async { + final canLaunch = await launcher.canLaunch('http://example.com/'); + + expect(canLaunch, false); + }); + test('launch', () async { await launcher.launch( 'http://example.com/', @@ -269,6 +280,20 @@ void main() { ); }); + test('launch should return false if platform returns null', () async { + final launched = await launcher.launch( + 'http://example.com/', + useSafariVC: true, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: const {}, + ); + + expect(launched, false); + }); + test('closeWebView default behavior', () async { await launcher.closeWebView(); expect( @@ -286,4 +311,7 @@ class UrlLauncherPlatformMock extends Mock class ImplementsUrlLauncherPlatform extends Mock implements UrlLauncherPlatform {} -class ExtendsUrlLauncherPlatform extends UrlLauncherPlatform {} +class ExtendsUrlLauncherPlatform extends UrlLauncherPlatform { + @override + final LinkDelegate? linkDelegate = null; +} diff --git a/packages/url_launcher/url_launcher_web/AUTHORS b/packages/url_launcher/url_launcher_web/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/url_launcher/url_launcher_web/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/url_launcher/url_launcher_web/CHANGELOG.md b/packages/url_launcher/url_launcher_web/CHANGELOG.md index 456d458834bf..488c3387cb68 100644 --- a/packages/url_launcher/url_launcher_web/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_web/CHANGELOG.md @@ -1,3 +1,27 @@ +# 2.0.1 + +- Change sizing code of `Link` widget's `HtmlElementView` so it works well when slotted. + +# 2.0.0 + +- Migrate to null safety. + +# 0.1.5+3 + +- Fix Link misalignment [issue](https://github.com/flutter/flutter/issues/70053). + +# 0.1.5+2 + +- Update Flutter SDK constraint. + +# 0.1.5+1 + +- Substitute `undefined_prefixed_name: ignore` analyzer setting by a `dart:ui` shim with conditional exports. [Issue](https://github.com/flutter/flutter/issues/69309). + +# 0.1.5 + +- Added the web implementation of the Link widget. + # 0.1.4+2 - Move `lib/third_party` to `lib/src/third_party`. @@ -29,7 +53,7 @@ # 0.1.2 -- Adds "tel" and "sms" support +- Adds "tel" and "sms" support # 0.1.1+6 diff --git a/packages/url_launcher/url_launcher_web/LICENSE b/packages/url_launcher/url_launcher_web/LICENSE index 46b9e8c222d7..dd4ac737fc37 100644 --- a/packages/url_launcher/url_launcher_web/LICENSE +++ b/packages/url_launcher/url_launcher_web/LICENSE @@ -1,6 +1,6 @@ url_launcher_web -Copyright 2019 The Chromium Authors. All rights reserved. +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: diff --git a/packages/url_launcher/url_launcher_web/example/README.md b/packages/url_launcher/url_launcher_web/example/README.md new file mode 100644 index 000000000000..b75df09c33f1 --- /dev/null +++ b/packages/url_launcher/url_launcher_web/example/README.md @@ -0,0 +1,31 @@ +# Testing + +This package utilizes the `integration_test` package to run its tests in a web browser. + +See [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) for more info. + +## Running the tests + +Make sure you have updated to the latest Flutter master. + +1. Check what version of Chrome is running on the machine you're running tests on. + +2. Download and install driver for that version from here: + * + +3. Start the driver using `chromedriver --port=4444` + +4. Run tests: `flutter drive -d web-server --browser-name=chrome --driver=test_driver/integration_test_driver.dart --target=integration_test/TEST_NAME.dart`, or (in Linux): + + * Single: `./run_test.sh integration_test/TEST_NAME.dart` + * All: `./run_test.sh` + +## Mocks + +There's `.mocks.dart` files next to the test files that use them. + +They're [generated by Mockito](https://github.com/dart-lang/mockito/blob/master/NULL_SAFETY_README.md#code-generation). + +Mocks might be manually re-generated with the following command: `flutter pub run build_runner build`. If there are any changes in the mocks, feel free to commit them. + +(Mocks will be auto-generated by the `run_test.sh` script as well.) diff --git a/packages/url_launcher/url_launcher_web/example/build.yaml b/packages/url_launcher/url_launcher_web/example/build.yaml new file mode 100644 index 000000000000..db3104bb04c6 --- /dev/null +++ b/packages/url_launcher/url_launcher_web/example/build.yaml @@ -0,0 +1,6 @@ +targets: + $default: + sources: + - integration_test/*.dart + - lib/$lib$ + - $package$ diff --git a/packages/url_launcher/url_launcher_web/example/integration_test/link_widget_test.dart b/packages/url_launcher/url_launcher_web/example/integration_test/link_widget_test.dart new file mode 100644 index 000000000000..0487aca1c2dd --- /dev/null +++ b/packages/url_launcher/url_launcher_web/example/integration_test/link_widget_test.dart @@ -0,0 +1,148 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:html' as html; +import 'dart:js_util'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:url_launcher_platform_interface/link.dart'; +import 'package:url_launcher_web/src/link.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('Link Widget', () { + testWidgets('creates anchor with correct attributes', + (WidgetTester tester) async { + final Uri uri = Uri.parse('http://foobar/example?q=1'); + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: WebLinkDelegate(TestLinkInfo( + uri: uri, + target: LinkTarget.blank, + builder: (BuildContext context, FollowLink? followLink) { + return Container(width: 100, height: 100); + }, + )), + )); + // Platform view creation happens asynchronously. + await tester.pumpAndSettle(); + + final html.Element anchor = _findSingleAnchor(); + expect(anchor.getAttribute('href'), uri.toString()); + expect(anchor.getAttribute('target'), '_blank'); + + final Uri uri2 = Uri.parse('http://foobar2/example?q=2'); + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: WebLinkDelegate(TestLinkInfo( + uri: uri2, + target: LinkTarget.self, + builder: (BuildContext context, FollowLink? followLink) { + return Container(width: 100, height: 100); + }, + )), + )); + await tester.pumpAndSettle(); + + // Check that the same anchor has been updated. + expect(anchor.getAttribute('href'), uri2.toString()); + expect(anchor.getAttribute('target'), '_self'); + }); + + testWidgets('sizes itself correctly', (WidgetTester tester) async { + final Key containerKey = GlobalKey(); + final Uri uri = Uri.parse('http://foobar'); + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints.tight(Size(100.0, 100.0)), + child: WebLinkDelegate(TestLinkInfo( + uri: uri, + target: LinkTarget.blank, + builder: (BuildContext context, FollowLink? followLink) { + return Container( + key: containerKey, + child: SizedBox(width: 50.0, height: 50.0), + ); + }, + )), + ), + ), + )); + await tester.pumpAndSettle(); + + final Size containerSize = tester.getSize(find.byKey(containerKey)); + // The Stack widget inserted by the `WebLinkDelegate` shouldn't loosen the + // constraints before passing them to the inner container. So the inner + // container should respect the tight constraints given by the ancestor + // `ConstrainedBox` widget. + expect(containerSize.width, 100.0); + expect(containerSize.height, 100.0); + }); + + // See: https://github.com/flutter/plugins/pull/3522#discussion_r574703724 + testWidgets('uri can be null', (WidgetTester tester) async { + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: WebLinkDelegate(TestLinkInfo( + uri: null, + target: LinkTarget.defaultTarget, + builder: (BuildContext context, FollowLink? followLink) { + return Container(width: 100, height: 100); + }, + )), + )); + // Platform view creation happens asynchronously. + await tester.pumpAndSettle(); + + final html.Element anchor = _findSingleAnchor(); + expect(anchor.hasAttribute('href'), false); + }); + }); +} + +html.Element _findSingleAnchor() { + final List foundAnchors = []; + for (final html.Element anchor in html.document.querySelectorAll('a')) { + if (hasProperty(anchor, linkViewIdProperty)) { + foundAnchors.add(anchor); + } + } + + // Search inside the shadow DOM as well. + final html.ShadowRoot? shadowRoot = + html.document.querySelector('flt-glass-pane')?.shadowRoot; + if (shadowRoot != null) { + for (final html.Element anchor in shadowRoot.querySelectorAll('a')) { + if (hasProperty(anchor, linkViewIdProperty)) { + foundAnchors.add(anchor); + } + } + } + + return foundAnchors.single; +} + +class TestLinkInfo extends LinkInfo { + @override + final LinkWidgetBuilder builder; + + @override + final Uri? uri; + + @override + final LinkTarget target; + + @override + bool get isDisabled => uri == null; + + TestLinkInfo({ + required this.uri, + required this.target, + required this.builder, + }); +} diff --git a/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.dart b/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.dart new file mode 100644 index 000000000000..0b53a1ffb1dd --- /dev/null +++ b/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.dart @@ -0,0 +1,202 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:html' as html; +import 'package:flutter_test/flutter_test.dart'; +import 'package: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 'url_launcher_web_test.mocks.dart'; + +@GenerateMocks([html.Window, html.Navigator]) +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('UrlLauncherPlugin', () { + late MockWindow mockWindow; + late MockNavigator mockNavigator; + + late UrlLauncherPlugin plugin; + + setUp(() { + mockWindow = MockWindow(); + mockNavigator = MockNavigator(); + when(mockWindow.navigator).thenReturn(mockNavigator); + + // Simulate that window.open does something. + when(mockWindow.open(any, any)).thenReturn(MockWindow()); + + when(mockNavigator.vendor).thenReturn('Google LLC'); + when(mockNavigator.appVersion).thenReturn('Mock version!'); + + plugin = UrlLauncherPlugin(debugWindow: mockWindow); + }); + + group('canLaunch', () { + testWidgets('"http" URLs -> true', (WidgetTester _) async { + expect(plugin.canLaunch('http://google.com'), completion(isTrue)); + }); + + testWidgets('"https" URLs -> true', (WidgetTester _) async { + expect( + plugin.canLaunch('https://go, (Widogle.com'), completion(isTrue)); + }); + + testWidgets('"mailto" URLs -> true', (WidgetTester _) async { + expect( + plugin.canLaunch('mailto:name@mydomain.com'), completion(isTrue)); + }); + + testWidgets('"tel" URLs -> true', (WidgetTester _) async { + expect(plugin.canLaunch('tel:5551234567'), completion(isTrue)); + }); + + testWidgets('"sms" URLs -> true', (WidgetTester _) async { + expect(plugin.canLaunch('sms:+19725551212?body=hello%20there'), + completion(isTrue)); + }); + }); + + group('launch', () { + testWidgets('launching a URL returns true', (WidgetTester _) async { + expect( + plugin.launch( + 'https://www.google.com', + ), + completion(isTrue)); + }); + + testWidgets('launching a "mailto" returns true', (WidgetTester _) async { + expect( + plugin.launch( + 'mailto:name@mydomain.com', + ), + completion(isTrue)); + }); + + testWidgets('launching a "tel" returns true', (WidgetTester _) async { + expect( + plugin.launch( + 'tel:5551234567', + ), + completion(isTrue)); + }); + + testWidgets('launching a "sms" returns true', (WidgetTester _) async { + expect( + plugin.launch( + 'sms:+19725551212?body=hello%20there', + ), + completion(isTrue)); + }); + }); + + group('openNewWindow', () { + testWidgets('http urls should be launched in a new window', + (WidgetTester _) async { + plugin.openNewWindow('http://www.google.com'); + + verify(mockWindow.open('http://www.google.com', '')); + }); + + testWidgets('https urls should be launched in a new window', + (WidgetTester _) async { + plugin.openNewWindow('https://www.google.com'); + + verify(mockWindow.open('https://www.google.com', '')); + }); + + testWidgets('mailto urls should be launched on a new window', + (WidgetTester _) async { + plugin.openNewWindow('mailto:name@mydomain.com'); + + verify(mockWindow.open('mailto:name@mydomain.com', '')); + }); + + testWidgets('tel urls should be launched on a new window', + (WidgetTester _) async { + plugin.openNewWindow('tel:5551234567'); + + verify(mockWindow.open('tel:5551234567', '')); + }); + + testWidgets('sms urls should be launched on a new window', + (WidgetTester _) async { + plugin.openNewWindow('sms:+19725551212?body=hello%20there'); + + verify(mockWindow.open('sms:+19725551212?body=hello%20there', '')); + }); + testWidgets( + 'setting webOnlyLinkTarget as _self opens the url in the same tab', + (WidgetTester _) async { + plugin.openNewWindow('https://www.google.com', + webOnlyWindowName: '_self'); + verify(mockWindow.open('https://www.google.com', '_self')); + }); + + testWidgets( + 'setting webOnlyLinkTarget as _blank opens the url in a new tab', + (WidgetTester _) async { + plugin.openNewWindow('https://www.google.com', + webOnlyWindowName: '_blank'); + verify(mockWindow.open('https://www.google.com', '_blank')); + }); + + group('Safari', () { + setUp(() { + when(mockNavigator.vendor).thenReturn('Apple Computer, Inc.'); + when(mockNavigator.appVersion).thenReturn( + '5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/605.1.15'); + // Recreate the plugin, so it grabs the overrides from this group + plugin = UrlLauncherPlugin(debugWindow: mockWindow); + }); + + testWidgets('http urls should be launched in a new window', + (WidgetTester _) async { + plugin.openNewWindow('http://www.google.com'); + + verify(mockWindow.open('http://www.google.com', '')); + }); + + testWidgets('https urls should be launched in a new window', + (WidgetTester _) async { + plugin.openNewWindow('https://www.google.com'); + + verify(mockWindow.open('https://www.google.com', '')); + }); + + testWidgets('mailto urls should be launched on the same window', + (WidgetTester _) async { + plugin.openNewWindow('mailto:name@mydomain.com'); + + verify(mockWindow.open('mailto:name@mydomain.com', '_top')); + }); + + testWidgets('tel urls should be launched on the same window', + (WidgetTester _) async { + plugin.openNewWindow('tel:5551234567'); + + verify(mockWindow.open('tel:5551234567', '_top')); + }); + + testWidgets('sms urls should be launched on the same window', + (WidgetTester _) async { + plugin.openNewWindow('sms:+19725551212?body=hello%20there'); + + verify( + mockWindow.open('sms:+19725551212?body=hello%20there', '_top')); + }); + testWidgets( + 'mailto urls should use _blank if webOnlyWindowName is set as _blank', + (WidgetTester _) async { + plugin.openNewWindow('mailto:name@mydomain.com', + webOnlyWindowName: '_blank'); + verify(mockWindow.open('mailto:name@mydomain.com', '_blank')); + }); + }); + }); + }); +} diff --git a/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.mocks.dart b/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.mocks.dart new file mode 100644 index 000000000000..9cd0196f51db --- /dev/null +++ b/packages/url_launcher/url_launcher_web/example/integration_test/url_launcher_web_test.mocks.dart @@ -0,0 +1,726 @@ +// Copyright 2013 The Flutter 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 +// in regular_integration_tests/integration_test/url_launcher_web_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i4; +import 'dart:html' as _i2; +import 'dart:math' as _i5; +import 'dart:web_sql' as _i3; + +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: comment_references +// ignore_for_file: unnecessary_parenthesis + +class _FakeDocument extends _i1.Fake implements _i2.Document {} + +class _FakeLocation extends _i1.Fake implements _i2.Location {} + +class _FakeConsole extends _i1.Fake implements _i2.Console {} + +class _FakeHistory extends _i1.Fake implements _i2.History {} + +class _FakeStorage extends _i1.Fake implements _i2.Storage {} + +class _FakeNavigator extends _i1.Fake implements _i2.Navigator {} + +class _FakePerformance extends _i1.Fake implements _i2.Performance {} + +class _FakeEvents extends _i1.Fake implements _i2.Events {} + +class _FakeType extends _i1.Fake implements Type {} + +class _FakeWindowBase extends _i1.Fake implements _i2.WindowBase {} + +class _FakeFileSystem extends _i1.Fake implements _i2.FileSystem {} + +class _FakeStylePropertyMapReadonly extends _i1.Fake + implements _i2.StylePropertyMapReadonly {} + +class _FakeMediaQueryList extends _i1.Fake implements _i2.MediaQueryList {} + +class _FakeEntry extends _i1.Fake implements _i2.Entry {} + +class _FakeSqlDatabase extends _i1.Fake implements _i3.SqlDatabase {} + +class _FakeGeolocation extends _i1.Fake implements _i2.Geolocation {} + +class _FakeMediaStream extends _i1.Fake implements _i2.MediaStream {} + +class _FakeRelatedApplication extends _i1.Fake + implements _i2.RelatedApplication {} + +/// A class which mocks [Window]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWindow extends _i1.Mock implements _i2.Window { + MockWindow() { + _i1.throwOnMissingStub(this); + } + + @override + _i4.Future get animationFrame => + (super.noSuchMethod(Invocation.getter(#animationFrame), + returnValue: Future.value(0)) as _i4.Future); + @override + _i2.Document get document => (super.noSuchMethod(Invocation.getter(#document), + returnValue: _FakeDocument()) as _i2.Document); + @override + _i2.Location get location => (super.noSuchMethod(Invocation.getter(#location), + returnValue: _FakeLocation()) 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); + @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); + @override + _i2.Storage get localStorage => + (super.noSuchMethod(Invocation.getter(#localStorage), + returnValue: _FakeStorage()) as _i2.Storage); + @override + _i2.Navigator get navigator => + (super.noSuchMethod(Invocation.getter(#navigator), + returnValue: _FakeNavigator()) as _i2.Navigator); + @override + int get outerHeight => + (super.noSuchMethod(Invocation.getter(#outerHeight), returnValue: 0) + as int); + @override + int get outerWidth => + (super.noSuchMethod(Invocation.getter(#outerWidth), returnValue: 0) + as int); + @override + _i2.Performance get performance => + (super.noSuchMethod(Invocation.getter(#performance), + returnValue: _FakePerformance()) as _i2.Performance); + @override + _i2.Storage get sessionStorage => + (super.noSuchMethod(Invocation.getter(#sessionStorage), + returnValue: _FakeStorage()) as _i2.Storage); + @override + _i4.Stream<_i2.Event> get onContentLoaded => + (super.noSuchMethod(Invocation.getter(#onContentLoaded), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onAbort => + (super.noSuchMethod(Invocation.getter(#onAbort), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onBlur => + (super.noSuchMethod(Invocation.getter(#onBlur), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onCanPlay => + (super.noSuchMethod(Invocation.getter(#onCanPlay), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onCanPlayThrough => + (super.noSuchMethod(Invocation.getter(#onCanPlayThrough), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onChange => + (super.noSuchMethod(Invocation.getter(#onChange), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.MouseEvent> get onClick => + (super.noSuchMethod(Invocation.getter(#onClick), + returnValue: Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onContextMenu => + (super.noSuchMethod(Invocation.getter(#onContextMenu), + returnValue: Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.Event> get onDoubleClick => + (super.noSuchMethod(Invocation.getter(#onDoubleClick), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.DeviceMotionEvent> get onDeviceMotion => + (super.noSuchMethod(Invocation.getter(#onDeviceMotion), + returnValue: Stream<_i2.DeviceMotionEvent>.empty()) + as _i4.Stream<_i2.DeviceMotionEvent>); + @override + _i4.Stream<_i2.DeviceOrientationEvent> get onDeviceOrientation => + (super.noSuchMethod(Invocation.getter(#onDeviceOrientation), + returnValue: Stream<_i2.DeviceOrientationEvent>.empty()) + as _i4.Stream<_i2.DeviceOrientationEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onDrag => + (super.noSuchMethod(Invocation.getter(#onDrag), + returnValue: Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onDragEnd => + (super.noSuchMethod(Invocation.getter(#onDragEnd), + returnValue: Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onDragEnter => + (super.noSuchMethod(Invocation.getter(#onDragEnter), + returnValue: Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onDragLeave => + (super.noSuchMethod(Invocation.getter(#onDragLeave), + returnValue: Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onDragOver => + (super.noSuchMethod(Invocation.getter(#onDragOver), + returnValue: Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onDragStart => + (super.noSuchMethod(Invocation.getter(#onDragStart), + returnValue: Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onDrop => + (super.noSuchMethod(Invocation.getter(#onDrop), + returnValue: Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.Event> get onDurationChange => + (super.noSuchMethod(Invocation.getter(#onDurationChange), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onEmptied => + (super.noSuchMethod(Invocation.getter(#onEmptied), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onEnded => + (super.noSuchMethod(Invocation.getter(#onEnded), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onError => + (super.noSuchMethod(Invocation.getter(#onError), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onFocus => + (super.noSuchMethod(Invocation.getter(#onFocus), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onHashChange => + (super.noSuchMethod(Invocation.getter(#onHashChange), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onInput => + (super.noSuchMethod(Invocation.getter(#onInput), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onInvalid => + (super.noSuchMethod(Invocation.getter(#onInvalid), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.KeyboardEvent> get onKeyDown => + (super.noSuchMethod(Invocation.getter(#onKeyDown), + returnValue: Stream<_i2.KeyboardEvent>.empty()) + as _i4.Stream<_i2.KeyboardEvent>); + @override + _i4.Stream<_i2.KeyboardEvent> get onKeyPress => + (super.noSuchMethod(Invocation.getter(#onKeyPress), + returnValue: Stream<_i2.KeyboardEvent>.empty()) + as _i4.Stream<_i2.KeyboardEvent>); + @override + _i4.Stream<_i2.KeyboardEvent> get onKeyUp => + (super.noSuchMethod(Invocation.getter(#onKeyUp), + returnValue: Stream<_i2.KeyboardEvent>.empty()) + as _i4.Stream<_i2.KeyboardEvent>); + @override + _i4.Stream<_i2.Event> get onLoad => + (super.noSuchMethod(Invocation.getter(#onLoad), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onLoadedData => + (super.noSuchMethod(Invocation.getter(#onLoadedData), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onLoadedMetadata => + (super.noSuchMethod(Invocation.getter(#onLoadedMetadata), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onLoadStart => + (super.noSuchMethod(Invocation.getter(#onLoadStart), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.MessageEvent> get onMessage => + (super.noSuchMethod(Invocation.getter(#onMessage), + returnValue: Stream<_i2.MessageEvent>.empty()) + as _i4.Stream<_i2.MessageEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onMouseDown => + (super.noSuchMethod(Invocation.getter(#onMouseDown), + returnValue: Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onMouseEnter => + (super.noSuchMethod(Invocation.getter(#onMouseEnter), + returnValue: Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onMouseLeave => + (super.noSuchMethod(Invocation.getter(#onMouseLeave), + returnValue: Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onMouseMove => + (super.noSuchMethod(Invocation.getter(#onMouseMove), + returnValue: Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onMouseOut => + (super.noSuchMethod(Invocation.getter(#onMouseOut), + returnValue: Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onMouseOver => + (super.noSuchMethod(Invocation.getter(#onMouseOver), + returnValue: Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.MouseEvent> get onMouseUp => + (super.noSuchMethod(Invocation.getter(#onMouseUp), + returnValue: Stream<_i2.MouseEvent>.empty()) + as _i4.Stream<_i2.MouseEvent>); + @override + _i4.Stream<_i2.WheelEvent> get onMouseWheel => + (super.noSuchMethod(Invocation.getter(#onMouseWheel), + returnValue: Stream<_i2.WheelEvent>.empty()) + as _i4.Stream<_i2.WheelEvent>); + @override + _i4.Stream<_i2.Event> get onOffline => + (super.noSuchMethod(Invocation.getter(#onOffline), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onOnline => + (super.noSuchMethod(Invocation.getter(#onOnline), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onPageHide => + (super.noSuchMethod(Invocation.getter(#onPageHide), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onPageShow => + (super.noSuchMethod(Invocation.getter(#onPageShow), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onPause => + (super.noSuchMethod(Invocation.getter(#onPause), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onPlay => + (super.noSuchMethod(Invocation.getter(#onPlay), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onPlaying => + (super.noSuchMethod(Invocation.getter(#onPlaying), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.PopStateEvent> get onPopState => + (super.noSuchMethod(Invocation.getter(#onPopState), + returnValue: Stream<_i2.PopStateEvent>.empty()) + as _i4.Stream<_i2.PopStateEvent>); + @override + _i4.Stream<_i2.Event> get onProgress => + (super.noSuchMethod(Invocation.getter(#onProgress), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onRateChange => + (super.noSuchMethod(Invocation.getter(#onRateChange), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onReset => + (super.noSuchMethod(Invocation.getter(#onReset), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onResize => + (super.noSuchMethod(Invocation.getter(#onResize), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onScroll => + (super.noSuchMethod(Invocation.getter(#onScroll), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onSearch => + (super.noSuchMethod(Invocation.getter(#onSearch), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onSeeked => + (super.noSuchMethod(Invocation.getter(#onSeeked), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onSeeking => + (super.noSuchMethod(Invocation.getter(#onSeeking), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onSelect => + (super.noSuchMethod(Invocation.getter(#onSelect), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onStalled => + (super.noSuchMethod(Invocation.getter(#onStalled), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.StorageEvent> get onStorage => + (super.noSuchMethod(Invocation.getter(#onStorage), + returnValue: Stream<_i2.StorageEvent>.empty()) + as _i4.Stream<_i2.StorageEvent>); + @override + _i4.Stream<_i2.Event> get onSubmit => + (super.noSuchMethod(Invocation.getter(#onSubmit), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onSuspend => + (super.noSuchMethod(Invocation.getter(#onSuspend), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onTimeUpdate => + (super.noSuchMethod(Invocation.getter(#onTimeUpdate), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.TouchEvent> get onTouchCancel => + (super.noSuchMethod(Invocation.getter(#onTouchCancel), + returnValue: Stream<_i2.TouchEvent>.empty()) + as _i4.Stream<_i2.TouchEvent>); + @override + _i4.Stream<_i2.TouchEvent> get onTouchEnd => + (super.noSuchMethod(Invocation.getter(#onTouchEnd), + returnValue: Stream<_i2.TouchEvent>.empty()) + as _i4.Stream<_i2.TouchEvent>); + @override + _i4.Stream<_i2.TouchEvent> get onTouchMove => + (super.noSuchMethod(Invocation.getter(#onTouchMove), + returnValue: Stream<_i2.TouchEvent>.empty()) + as _i4.Stream<_i2.TouchEvent>); + @override + _i4.Stream<_i2.TouchEvent> get onTouchStart => + (super.noSuchMethod(Invocation.getter(#onTouchStart), + returnValue: Stream<_i2.TouchEvent>.empty()) + as _i4.Stream<_i2.TouchEvent>); + @override + _i4.Stream<_i2.TransitionEvent> get onTransitionEnd => + (super.noSuchMethod(Invocation.getter(#onTransitionEnd), + returnValue: Stream<_i2.TransitionEvent>.empty()) + as _i4.Stream<_i2.TransitionEvent>); + @override + _i4.Stream<_i2.Event> get onUnload => + (super.noSuchMethod(Invocation.getter(#onUnload), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onVolumeChange => + (super.noSuchMethod(Invocation.getter(#onVolumeChange), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.Event> get onWaiting => + (super.noSuchMethod(Invocation.getter(#onWaiting), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.AnimationEvent> get onAnimationEnd => + (super.noSuchMethod(Invocation.getter(#onAnimationEnd), + returnValue: Stream<_i2.AnimationEvent>.empty()) + as _i4.Stream<_i2.AnimationEvent>); + @override + _i4.Stream<_i2.AnimationEvent> get onAnimationIteration => + (super.noSuchMethod(Invocation.getter(#onAnimationIteration), + returnValue: Stream<_i2.AnimationEvent>.empty()) + as _i4.Stream<_i2.AnimationEvent>); + @override + _i4.Stream<_i2.AnimationEvent> get onAnimationStart => + (super.noSuchMethod(Invocation.getter(#onAnimationStart), + returnValue: Stream<_i2.AnimationEvent>.empty()) + as _i4.Stream<_i2.AnimationEvent>); + @override + _i4.Stream<_i2.Event> get onBeforeUnload => + (super.noSuchMethod(Invocation.getter(#onBeforeUnload), + returnValue: Stream<_i2.Event>.empty()) as _i4.Stream<_i2.Event>); + @override + _i4.Stream<_i2.WheelEvent> get onWheel => + (super.noSuchMethod(Invocation.getter(#onWheel), + returnValue: Stream<_i2.WheelEvent>.empty()) + as _i4.Stream<_i2.WheelEvent>); + @override + int get pageXOffset => + (super.noSuchMethod(Invocation.getter(#pageXOffset), returnValue: 0) + as int); + @override + int get pageYOffset => + (super.noSuchMethod(Invocation.getter(#pageYOffset), returnValue: 0) + as int); + @override + int get scrollX => + (super.noSuchMethod(Invocation.getter(#scrollX), returnValue: 0) as int); + @override + int get scrollY => + (super.noSuchMethod(Invocation.getter(#scrollY), returnValue: 0) as int); + @override + _i2.Events get on => + (super.noSuchMethod(Invocation.getter(#on), returnValue: _FakeEvents()) + 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); + @override + int requestAnimationFrame(_i2.FrameRequestCallback? callback) => + (super.noSuchMethod(Invocation.method(#requestAnimationFrame, [callback]), + returnValue: 0) as int); + @override + void cancelAnimationFrame(int? id) => + super.noSuchMethod(Invocation.method(#cancelAnimationFrame, [id]), + returnValueForMissingStub: null); + @override + _i4.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>); + @override + void cancelIdleCallback(int? handle) => + super.noSuchMethod(Invocation.method(#cancelIdleCallback, [handle]), + 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]) => + (super.noSuchMethod(Invocation.method(#fetch, [input, init]), + returnValue: Future.value(null)) as _i4.Future); + @override + bool find(String? string, bool? caseSensitive, bool? backwards, bool? wrap, + bool? wholeWord, bool? searchInFrames, bool? showDialog) => + (super.noSuchMethod( + Invocation.method(#find, [ + string, + caseSensitive, + backwards, + wrap, + wholeWord, + searchInFrames, + showDialog + ]), + returnValue: false) as bool); + @override + _i2.StylePropertyMapReadonly getComputedStyleMap( + _i2.Element? element, String? pseudoElement) => + (super.noSuchMethod( + Invocation.method(#getComputedStyleMap, [element, pseudoElement]), + returnValue: _FakeStylePropertyMapReadonly()) + as _i2.StylePropertyMapReadonly); + @override + List<_i2.CssRule> getMatchedCssRules( + _i2.Element? element, String? pseudoElement) => + (super.noSuchMethod( + Invocation.method(#getMatchedCssRules, [element, pseudoElement]), + returnValue: <_i2.CssRule>[]) as List<_i2.CssRule>); + @override + _i2.MediaQueryList matchMedia(String? query) => + (super.noSuchMethod(Invocation.method(#matchMedia, [query]), + returnValue: _FakeMediaQueryList()) as _i2.MediaQueryList); + @override + void moveBy(int? x, int? y) => + super.noSuchMethod(Invocation.method(#moveBy, [x, y]), + returnValueForMissingStub: null); + @override + void postMessage(dynamic message, String? targetOrigin, + [List? transfer]) => + super.noSuchMethod( + Invocation.method(#postMessage, [message, targetOrigin, transfer]), + returnValueForMissingStub: null); + @override + int requestIdleCallback(_i2.IdleRequestCallback? callback, + [Map? options]) => + (super.noSuchMethod( + Invocation.method(#requestIdleCallback, [callback, options]), + returnValue: 0) as int); + @override + void resizeBy(int? x, int? y) => + super.noSuchMethod(Invocation.method(#resizeBy, [x, y]), + returnValueForMissingStub: null); + @override + void resizeTo(int? x, int? y) => + super.noSuchMethod(Invocation.method(#resizeTo, [x, y]), + returnValueForMissingStub: null); + @override + _i4.Future<_i2.Entry> resolveLocalFileSystemUrl(String? url) => + (super.noSuchMethod(Invocation.method(#resolveLocalFileSystemUrl, [url]), + returnValue: Future.value(_FakeEntry())) as _i4.Future<_i2.Entry>); + @override + String atob(String? atob) => + (super.noSuchMethod(Invocation.method(#atob, [atob]), returnValue: '') + as String); + @override + String btoa(String? btoa) => + (super.noSuchMethod(Invocation.method(#btoa, [btoa]), returnValue: '') + as String); + @override + void moveTo(_i5.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( + 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 + 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]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockNavigator extends _i1.Mock implements _i2.Navigator { + MockNavigator() { + _i1.throwOnMissingStub(this); + } + + @override + String get language => + (super.noSuchMethod(Invocation.getter(#language), returnValue: '') + as String); + @override + _i2.Geolocation get geolocation => + (super.noSuchMethod(Invocation.getter(#geolocation), + returnValue: _FakeGeolocation()) as _i2.Geolocation); + @override + String get vendor => + (super.noSuchMethod(Invocation.getter(#vendor), returnValue: '') + as String); + @override + String get vendorSub => + (super.noSuchMethod(Invocation.getter(#vendorSub), returnValue: '') + as String); + @override + String get appCodeName => + (super.noSuchMethod(Invocation.getter(#appCodeName), returnValue: '') + as String); + @override + String get appName => + (super.noSuchMethod(Invocation.getter(#appName), returnValue: '') + as String); + @override + String get appVersion => + (super.noSuchMethod(Invocation.getter(#appVersion), returnValue: '') + as String); + @override + String get product => + (super.noSuchMethod(Invocation.getter(#product), returnValue: '') + as String); + @override + String get userAgent => + (super.noSuchMethod(Invocation.getter(#userAgent), returnValue: '') + as String); + @override + 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( + {dynamic audio = false, dynamic video = false}) => + (super.noSuchMethod( + Invocation.method(#getUserMedia, [], {#audio: audio, #video: video}), + returnValue: + Future.value(_FakeMediaStream())) as _i4.Future<_i2.MediaStream>); + @override + _i4.Future getBattery() => + (super.noSuchMethod(Invocation.method(#getBattery, []), + returnValue: Future.value(null)) as _i4.Future); + @override + _i4.Future<_i2.RelatedApplication> getInstalledRelatedApps() => + (super.noSuchMethod(Invocation.method(#getInstalledRelatedApps, []), + returnValue: Future.value(_FakeRelatedApplication())) + as _i4.Future<_i2.RelatedApplication>); + @override + _i4.Future getVRDisplays() => + (super.noSuchMethod(Invocation.method(#getVRDisplays, []), + returnValue: Future.value(null)) as _i4.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]) => + (super.noSuchMethod(Invocation.method(#requestKeyboardLock, [keyCodes]), + returnValue: Future.value(null)) as _i4.Future); + @override + _i4.Future requestMidiAccess([Map? options]) => + (super.noSuchMethod(Invocation.method(#requestMidiAccess, [options]), + returnValue: Future.value(null)) as _i4.Future); + @override + _i4.Future requestMediaKeySystemAccess(String? keySystem, + List>? supportedConfigurations) => + (super.noSuchMethod( + Invocation.method(#requestMediaKeySystemAccess, + [keySystem, supportedConfigurations]), + returnValue: Future.value(null)) as _i4.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]) => + (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); +} diff --git a/packages/url_launcher/url_launcher_web/example/lib/main.dart b/packages/url_launcher/url_launcher_web/example/lib/main.dart new file mode 100644 index 000000000000..e1a38dcdcd46 --- /dev/null +++ b/packages/url_launcher/url_launcher_web/example/lib/main.dart @@ -0,0 +1,25 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +void main() { + runApp(MyApp()); +} + +/// App for testing +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State { + @override + Widget build(BuildContext context) { + return Directionality( + textDirection: TextDirection.ltr, + child: Text('Testing... Look at the console output for results!'), + ); + } +} diff --git a/packages/url_launcher/url_launcher_web/example/pubspec.yaml b/packages/url_launcher/url_launcher_web/example/pubspec.yaml new file mode 100644 index 000000000000..7c00c33550a8 --- /dev/null +++ b/packages/url_launcher/url_launcher_web/example/pubspec.yaml @@ -0,0 +1,22 @@ +name: regular_integration_tests +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.2.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + build_runner: ^1.10.0 + mockito: ^5.0.0 + url_launcher_web: + path: ../ + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter diff --git a/packages/url_launcher/url_launcher_web/example/run_test.sh b/packages/url_launcher/url_launcher_web/example/run_test.sh new file mode 100755 index 000000000000..dabf9a8630e6 --- /dev/null +++ b/packages/url_launcher/url_launcher_web/example/run_test.sh @@ -0,0 +1,27 @@ +#!/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." + + flutter pub get + + echo "(Re)generating mocks." + flutter pub run build_runner build --delete-conflicting-outputs + + if [ $# -eq 0 ]; then + echo "No target specified, running all tests..." + find integration_test/ -iname *_test.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.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/url_launcher/url_launcher_web/example/test_driver/integration_test.dart b/packages/url_launcher/url_launcher_web/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_web/example/web/index.html b/packages/url_launcher/url_launcher_web/example/web/index.html new file mode 100644 index 000000000000..dc9f89762aec --- /dev/null +++ b/packages/url_launcher/url_launcher_web/example/web/index.html @@ -0,0 +1,12 @@ + + + + + Browser Tests + + + + + diff --git a/packages/url_launcher/url_launcher_web/ios/url_launcher_web.podspec b/packages/url_launcher/url_launcher_web/ios/url_launcher_web.podspec deleted file mode 100644 index 161156ef020d..000000000000 --- a/packages/url_launcher/url_launcher_web/ios/url_launcher_web.podspec +++ /dev/null @@ -1,20 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'url_launcher_web' - s.version = '0.0.1' - s.summary = 'No-op implementation of url_launcher_web web plugin to avoid build issues on iOS' - s.description = <<-DESC -temp fake url_launcher_web plugin - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_web' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.ios.deployment_target = '8.0' -end diff --git a/packages/url_launcher/url_launcher_web/lib/src/link.dart b/packages/url_launcher/url_launcher_web/lib/src/link.dart new file mode 100644 index 000000000000..3c556b3950b0 --- /dev/null +++ b/packages/url_launcher/url_launcher_web/lib/src/link.dart @@ -0,0 +1,304 @@ +// Copyright 2013 The Flutter Authors. All 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:js_util'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; + +import 'package:url_launcher_platform_interface/link.dart'; + +/// The unique identifier for the view type to be used for link platform views. +const String linkViewType = '__url_launcher::link'; + +/// The name of the property used to set the viewId on the DOM element. +const String linkViewIdProperty = '__url_launcher::link::viewId'; + +/// Signature for a function that takes a unique [id] and creates an HTML element. +typedef HtmlViewFactory = html.Element Function(int viewId); + +/// Factory that returns the link DOM element for each unique view id. +HtmlViewFactory get linkViewFactory => LinkViewController._viewFactory; + +/// The delegate for building the [Link] widget on the web. +/// +/// 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); + + /// Information about the link built by the app. + final LinkInfo link; + + @override + WebLinkDelegateState createState() => WebLinkDelegateState(); +} + +/// The link delegate used on the web platform. +/// +/// For external URIs, it lets the browser do its thing. For app route names, it +/// pushes the route name to the framework. +class WebLinkDelegateState extends State { + late LinkViewController _controller; + + @override + void didUpdateWidget(WebLinkDelegate oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.link.uri != oldWidget.link.uri) { + _controller.setUri(widget.link.uri); + } + if (widget.link.target != oldWidget.link.target) { + _controller.setTarget(widget.link.target); + } + } + + Future _followLink() { + LinkViewController.registerHitTest(_controller); + return Future.value(); + } + + @override + Widget build(BuildContext context) { + return Stack( + fit: StackFit.passthrough, + children: [ + widget.link.builder( + context, + widget.link.isDisabled ? null : _followLink, + ), + Positioned.fill( + child: PlatformViewLink( + viewType: linkViewType, + onCreatePlatformView: (PlatformViewCreationParams params) { + _controller = LinkViewController.fromParams(params, context); + return _controller + ..setUri(widget.link.uri) + ..setTarget(widget.link.target); + }, + surfaceFactory: + (BuildContext context, PlatformViewController controller) { + return PlatformViewSurface( + controller: controller, + gestureRecognizers: + Set>(), + hitTestBehavior: PlatformViewHitTestBehavior.transparent, + ); + }, + ), + ), + ], + ); + } +} + +/// Controls link views. +class LinkViewController extends PlatformViewController { + /// Creates a [LinkViewController] instance with the unique [viewId]. + LinkViewController(this.viewId, this.context) { + if (_instances.isEmpty) { + // This is the first controller being created, attach the global click + // listener. + _clickSubscription = html.window.onClick.listen(_onGlobalClick); + } + _instances[viewId] = this; + } + + /// Creates and initializes a [LinkViewController] instance with the given + /// platform view [params]. + factory LinkViewController.fromParams( + PlatformViewCreationParams params, + BuildContext context, + ) { + final int viewId = params.id; + final LinkViewController controller = LinkViewController(viewId, context); + controller._initialize().then((_) { + params.onPlatformViewCreated(viewId); + }); + return controller; + } + + static Map _instances = {}; + + static html.Element _viewFactory(int viewId) { + return _instances[viewId]!._element; + } + + static int? _hitTestedViewId; + + static late StreamSubscription _clickSubscription; + + static void _onGlobalClick(html.MouseEvent event) { + final int? viewId = getViewIdFromTarget(event); + _instances[viewId]?._onDomClick(event); + // After the DOM click event has been received, clean up the hit test state + // so we can start fresh on the next click. + unregisterHitTest(); + } + + /// Call this method to indicate that a hit test has been registered for the + /// given [controller]. + /// + /// The [onClick] callback is invoked when the anchor element receives a + /// `click` from the browser. + static void registerHitTest(LinkViewController controller) { + _hitTestedViewId = controller.viewId; + } + + /// Removes all information about previously registered hit tests. + static void unregisterHitTest() { + _hitTestedViewId = null; + } + + @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 { + _element = html.Element.tag('a'); + setProperty(_element, linkViewIdProperty, viewId); + _element.style + ..opacity = '0' + ..display = 'block' + ..width = '100%' + ..height = '100%' + ..cursor = 'unset'; + + // This is recommended on MDN: + // - https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-target + _element.setAttribute('rel', 'noreferrer noopener'); + + final Map args = { + 'id': viewId, + 'viewType': linkViewType, + }; + await SystemChannels.platform_views.invokeMethod('create', args); + } + + void _onDomClick(html.MouseEvent event) { + final bool isHitTested = _hitTestedViewId == viewId; + if (!isHitTested) { + // There was no hit test registered for this click. This means the click + // landed on the anchor element but not on the underlying widget. In this + // case, we prevent the browser from following the click. + event.preventDefault(); + return; + } + + if (_uri != null && _uri!.hasScheme) { + // External links will be handled by the browser, so we don't have to do + // anything. + return; + } + + // 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. + event.preventDefault(); + final String routeName = _uri.toString(); + pushRouteNameToFramework(context, routeName); + } + + Uri? _uri; + + /// Set the [Uri] value for this link. + /// + /// When Uri is null, the `href` attribute of the link is removed. + void setUri(Uri? uri) { + assert(_isInitialized); + _uri = uri; + if (uri == null) { + _element.removeAttribute('href'); + } else { + _element.setAttribute('href', uri.toString()); + } + } + + /// Set the [LinkTarget] value for this link. + void setTarget(LinkTarget target) { + assert(_isInitialized); + _element.setAttribute('target', _getHtmlTarget(target)); + } + + String _getHtmlTarget(LinkTarget target) { + switch (target) { + case LinkTarget.defaultTarget: + case LinkTarget.self: + return '_self'; + case LinkTarget.blank: + return '_blank'; + default: + throw Exception('Unknown LinkTarget value $target.'); + } + } + + @override + Future clearFocus() async { + // Currently this does nothing on Flutter Web. + // TODO(het): Implement this. See https://github.com/flutter/flutter/issues/39496 + } + + @override + Future dispatchPointerEvent(PointerEvent event) async { + // We do not dispatch pointer events to HTML views because they may contain + // cross-origin iframes, which only accept user-generated events. + } + + @override + Future dispose() async { + if (_isInitialized) { + assert(_instances[viewId] == this); + _instances.remove(viewId); + if (_instances.isEmpty) { + await _clickSubscription.cancel(); + } + await SystemChannels.platform_views.invokeMethod('dispose', viewId); + } + } +} + +/// Finds the view id of the DOM element targeted by the [event]. +int? getViewIdFromTarget(html.Event event) { + final html.Element? linkElement = getLinkElementFromTarget(event); + if (linkElement != null) { + return getProperty(linkElement, linkViewIdProperty); + } + return null; +} + +/// Finds the targeted DOM element by the [event]. +/// +/// It handles the case where the target element is inside a shadow DOM too. +html.Element? getLinkElementFromTarget(html.Event event) { + final html.EventTarget? target = event.target; + if (target != null && target is html.Element) { + if (isLinkElement(target)) { + return target; + } + if (target.shadowRoot != null) { + final html.Node? child = target.shadowRoot!.lastChild; + if (child != null && child is html.Element && isLinkElement(child)) { + return child; + } + } + } + return null; +} + +/// Checks if the given [element] is a link that was created by +/// [LinkViewController]. +bool isLinkElement(html.Element? element) { + return element != null && + element.tagName == 'A' && + hasProperty(element, linkViewIdProperty); +} 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 new file mode 100644 index 000000000000..5eacec5fe867 --- /dev/null +++ b/packages/url_launcher/url_launcher_web/lib/src/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(flutter/flutter#55000) Remove this file once web-only dart:ui APIs +// are exposed from a dedicated place. +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 new file mode 100644 index 000000000000..f2862af8b704 --- /dev/null +++ b/packages/url_launcher/url_launcher_web/lib/src/shims/dart_ui_fake.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 '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. + +/// Shim for web_ui engine.PlatformViewRegistry +/// https://github.com/flutter/engine/blob/master/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) {} +} + +/// Shim for web_ui engine.AssetManager. +/// https://github.com/flutter/engine/blob/master/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) {} +} + +/// Signature of callbacks that have no arguments and return no data. +typedef VoidCallback = void Function(); diff --git a/packages/url_launcher/url_launcher_web/lib/src/shims/dart_ui_real.dart b/packages/url_launcher/url_launcher_web/lib/src/shims/dart_ui_real.dart new file mode 100644 index 000000000000..276b768c76c5 --- /dev/null +++ b/packages/url_launcher/url_launcher_web/lib/src/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/url_launcher/url_launcher_web/lib/src/third_party/platform_detect/AUTHORS b/packages/url_launcher/url_launcher_web/lib/src/third_party/platform_detect/AUTHORS new file mode 100644 index 000000000000..dbf9d190931b --- /dev/null +++ b/packages/url_launcher/url_launcher_web/lib/src/third_party/platform_detect/AUTHORS @@ -0,0 +1,65 @@ +# 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. +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/url_launcher/url_launcher_web/lib/url_launcher_web.dart b/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart index 093e06a4d8ed..9249837bd46b 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 @@ -1,14 +1,17 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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 'src/shims/dart_ui.dart' as ui; 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/third_party/platform_detect/browser.dart'; const _safariTargetTopSchemes = { @@ -16,7 +19,7 @@ const _safariTargetTopSchemes = { 'tel', 'sms', }; -String _getUrlScheme(String url) => Uri.tryParse(url)?.scheme; +String? _getUrlScheme(String url) => Uri.tryParse(url)?.scheme; bool _isSafariTargetTopScheme(String url) => _safariTargetTopSchemes.contains(_getUrlScheme(url)); @@ -35,7 +38,7 @@ class UrlLauncherPlugin extends UrlLauncherPlatform { }.union(_safariTargetTopSchemes); /// A constructor that allows tests to override the window object used by the plugin. - UrlLauncherPlugin({@visibleForTesting html.Window debugWindow}) + UrlLauncherPlugin({@visibleForTesting html.Window? debugWindow}) : _window = debugWindow ?? html.window { _isSafari = navigatorIsSafari(_window.navigator); } @@ -43,13 +46,19 @@ class UrlLauncherPlugin extends UrlLauncherPlatform { /// Registers this class as the default instance of [UrlLauncherPlatform]. static void registerWith(Registrar registrar) { UrlLauncherPlatform.instance = UrlLauncherPlugin(); + ui.platformViewRegistry.registerViewFactory(linkViewType, linkViewFactory); + } + + @override + LinkDelegate get linkDelegate { + return (LinkInfo linkInfo) => WebLinkDelegate(linkInfo); } /// Opens the given [url] in the specified [webOnlyWindowName]. /// /// Returns the newly created window. @visibleForTesting - html.WindowBase openNewWindow(String url, {String webOnlyWindowName}) { + html.WindowBase openNewWindow(String url, {String? webOnlyWindowName}) { // We need to open mailto, tel and sms urls on the _top window context on safari browsers. // See https://github.com/flutter/flutter/issues/51461 for reference. final target = webOnlyWindowName ?? @@ -65,13 +74,13 @@ class UrlLauncherPlugin extends UrlLauncherPlatform { @override Future launch( String url, { - @required bool useSafariVC, - @required bool useWebView, - @required bool enableJavaScript, - @required bool enableDomStorage, - @required bool universalLinksOnly, - @required Map headers, - String webOnlyWindowName, + bool useSafariVC = false, + bool useWebView = false, + bool enableJavaScript = false, + bool enableDomStorage = false, + bool universalLinksOnly = false, + Map headers = const {}, + String? webOnlyWindowName, }) { return Future.value( openNewWindow(url, webOnlyWindowName: webOnlyWindowName) != null); diff --git a/packages/url_launcher/url_launcher_web/pubspec.yaml b/packages/url_launcher/url_launcher_web/pubspec.yaml index 957b25757036..7afdc0af85e2 100644 --- a/packages/url_launcher/url_launcher_web/pubspec.yaml +++ b/packages/url_launcher/url_launcher_web/pubspec.yaml @@ -1,10 +1,12 @@ name: url_launcher_web description: Web platform implementation of url_launcher -homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_web -# 0.1.y+z is compatible with 1.0.0, if you land a breaking change bump -# the version to 2.0.0. -# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.1.4+2 +repository: https://github.com/flutter/plugins/tree/master/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.1 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: @@ -14,22 +16,14 @@ flutter: fileName: url_launcher_web.dart dependencies: - url_launcher_platform_interface: ^1.0.8 flutter: sdk: flutter flutter_web_plugins: sdk: flutter - meta: ^1.1.7 + meta: ^1.3.0 # null safe + url_launcher_platform_interface: ^2.0.0 dev_dependencies: flutter_test: sdk: flutter - url_launcher: ^5.2.5 - pedantic: ^1.8.0 - mockito: ^4.1.1 - integration_test: - path: ../../integration_test - -environment: - sdk: ">=2.2.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" + pedantic: ^1.10.0 diff --git a/packages/url_launcher/url_launcher_web/test/README.md b/packages/url_launcher/url_launcher_web/test/README.md index 7c48d024ba57..7c5b4ad682ba 100644 --- a/packages/url_launcher/url_launcher_web/test/README.md +++ b/packages/url_launcher/url_launcher_web/test/README.md @@ -1,17 +1,5 @@ -# Running browser_tests +## test -Make sure you have updated to the latest Flutter master. +This package uses integration tests for testing. -1. Check what version of Chrome is running on the machine you're running tests on. - -2. Download and install driver for that version from here: - * - -3. Start the driver using `chromedriver --port=4444` - -4. Change into the `test` directory of your clone. - -5. Run tests: `flutter drive -d web-server --browser-name=chrome --target=test_driver/TEST_NAME_integration.dart`, or (in Linux): - - * Single: `./run_test test_driver/TEST_NAME_integration.dart` - * All: `./run_test` +See `example/README.md` for more info. diff --git a/packages/url_launcher/url_launcher_web/test/lib/main.dart b/packages/url_launcher/url_launcher_web/test/lib/main.dart deleted file mode 100644 index 10415204570c..000000000000 --- a/packages/url_launcher/url_launcher_web/test/lib/main.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 '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 Text('Testing... Look at the console output for results!'); - } -} diff --git a/packages/url_launcher/url_launcher_web/test/pubspec.yaml b/packages/url_launcher/url_launcher_web/test/pubspec.yaml deleted file mode 100644 index e755dff85004..000000000000 --- a/packages/url_launcher/url_launcher_web/test/pubspec.yaml +++ /dev/null @@ -1,22 +0,0 @@ -name: regular_integration_tests -publish_to: none - -environment: - sdk: ">=2.2.2 <3.0.0" - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - flutter_driver: - sdk: flutter - flutter_test: - sdk: flutter - http: ^0.12.2 - mockito: ^4.1.1 - url_launcher_web: - path: ../ - integration_test: - path: ../../../integration_test - diff --git a/packages/url_launcher/url_launcher_web/test/run_test b/packages/url_launcher/url_launcher_web/test/run_test deleted file mode 100755 index 74a8526a0fa3..000000000000 --- a/packages/url_launcher/url_launcher_web/test/run_test +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/bash -if pgrep -lf chromedriver > /dev/null; then - echo "chromedriver is running." - - if [ $# -eq 0 ]; then - echo "No target specified, running all tests..." - find test_driver/ -iname *_integration.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --target='{}' - else - echo "Running test target: $1..." - set -x - flutter drive -d web-server --web-port=7357 --browser-name=chrome --target=$1 - fi - - else - echo "chromedriver is not running." -fi - diff --git a/packages/url_launcher/url_launcher_web/test/test_driver/url_launcher_web_integration.dart b/packages/url_launcher/url_launcher_web/test/test_driver/url_launcher_web_integration.dart deleted file mode 100644 index d0dd6e38ee46..000000000000 --- a/packages/url_launcher/url_launcher_web/test/test_driver/url_launcher_web_integration.dart +++ /dev/null @@ -1,231 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:html' as html; -import 'package:flutter_test/flutter_test.dart'; -import 'package:url_launcher_web/url_launcher_web.dart'; -import 'package:mockito/mockito.dart'; -import 'package:integration_test/integration_test.dart'; - -class _MockWindow extends Mock implements html.Window {} - -class _MockNavigator extends Mock implements html.Navigator {} - -void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - - group('UrlLauncherPlugin', () { - _MockWindow mockWindow; - _MockNavigator mockNavigator; - - UrlLauncherPlugin plugin; - - setUp(() { - mockWindow = _MockWindow(); - mockNavigator = _MockNavigator(); - when(mockWindow.navigator).thenReturn(mockNavigator); - - plugin = UrlLauncherPlugin(debugWindow: mockWindow); - }); - - group('canLaunch', () { - testWidgets('"http" URLs -> true', (WidgetTester _) async { - expect(plugin.canLaunch('http://google.com'), completion(isTrue)); - }); - - testWidgets('"https" URLs -> true', (WidgetTester _) async { - expect( - plugin.canLaunch('https://go, (Widogle.com'), completion(isTrue)); - }); - - testWidgets('"mailto" URLs -> true', (WidgetTester _) async { - expect( - plugin.canLaunch('mailto:name@mydomain.com'), completion(isTrue)); - }); - - testWidgets('"tel" URLs -> true', (WidgetTester _) async { - expect(plugin.canLaunch('tel:5551234567'), completion(isTrue)); - }); - - testWidgets('"sms" URLs -> true', (WidgetTester _) async { - expect(plugin.canLaunch('sms:+19725551212?body=hello%20there'), - completion(isTrue)); - }); - }); - - group('launch', () { - setUp(() { - // Simulate that window.open does something. - when(mockWindow.open('https://www.google.com', '')) - .thenReturn(_MockWindow()); - when(mockWindow.open('mailto:name@mydomain.com', '')) - .thenReturn(_MockWindow()); - when(mockWindow.open('tel:5551234567', '')).thenReturn(_MockWindow()); - when(mockWindow.open('sms:+19725551212?body=hello%20there', '')) - .thenReturn(_MockWindow()); - }); - - testWidgets('launching a URL returns true', (WidgetTester _) async { - expect( - plugin.launch( - 'https://www.google.com', - useSafariVC: null, - useWebView: null, - universalLinksOnly: null, - enableDomStorage: null, - enableJavaScript: null, - headers: null, - ), - completion(isTrue)); - }); - - testWidgets('launching a "mailto" returns true', (WidgetTester _) async { - expect( - plugin.launch( - 'mailto:name@mydomain.com', - useSafariVC: null, - useWebView: null, - universalLinksOnly: null, - enableDomStorage: null, - enableJavaScript: null, - headers: null, - ), - completion(isTrue)); - }); - - testWidgets('launching a "tel" returns true', (WidgetTester _) async { - expect( - plugin.launch( - 'tel:5551234567', - useSafariVC: null, - useWebView: null, - universalLinksOnly: null, - enableDomStorage: null, - enableJavaScript: null, - headers: null, - ), - completion(isTrue)); - }); - - testWidgets('launching a "sms" returns true', (WidgetTester _) async { - expect( - plugin.launch( - 'sms:+19725551212?body=hello%20there', - useSafariVC: null, - useWebView: null, - universalLinksOnly: null, - enableDomStorage: null, - enableJavaScript: null, - headers: null, - ), - completion(isTrue)); - }); - }); - - group('openNewWindow', () { - testWidgets('http urls should be launched in a new window', - (WidgetTester _) async { - plugin.openNewWindow('http://www.google.com'); - - verify(mockWindow.open('http://www.google.com', '')); - }); - - testWidgets('https urls should be launched in a new window', - (WidgetTester _) async { - plugin.openNewWindow('https://www.google.com'); - - verify(mockWindow.open('https://www.google.com', '')); - }); - - testWidgets('mailto urls should be launched on a new window', - (WidgetTester _) async { - plugin.openNewWindow('mailto:name@mydomain.com'); - - verify(mockWindow.open('mailto:name@mydomain.com', '')); - }); - - testWidgets('tel urls should be launched on a new window', - (WidgetTester _) async { - plugin.openNewWindow('tel:5551234567'); - - verify(mockWindow.open('tel:5551234567', '')); - }); - - testWidgets('sms urls should be launched on a new window', - (WidgetTester _) async { - plugin.openNewWindow('sms:+19725551212?body=hello%20there'); - - verify(mockWindow.open('sms:+19725551212?body=hello%20there', '')); - }); - testWidgets( - 'setting webOnlyLinkTarget as _self opens the url in the same tab', - (WidgetTester _) async { - plugin.openNewWindow('https://www.google.com', - webOnlyWindowName: '_self'); - verify(mockWindow.open('https://www.google.com', '_self')); - }); - - testWidgets( - 'setting webOnlyLinkTarget as _blank opens the url in a new tab', - (WidgetTester _) async { - plugin.openNewWindow('https://www.google.com', - webOnlyWindowName: '_blank'); - verify(mockWindow.open('https://www.google.com', '_blank')); - }); - - group('Safari', () { - setUp(() { - when(mockNavigator.vendor).thenReturn('Apple Computer, Inc.'); - when(mockNavigator.appVersion).thenReturn( - '5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Safari/605.1.15'); - // Recreate the plugin, so it grabs the overrides from this group - plugin = UrlLauncherPlugin(debugWindow: mockWindow); - }); - - testWidgets('http urls should be launched in a new window', - (WidgetTester _) async { - plugin.openNewWindow('http://www.google.com'); - - verify(mockWindow.open('http://www.google.com', '')); - }); - - testWidgets('https urls should be launched in a new window', - (WidgetTester _) async { - plugin.openNewWindow('https://www.google.com'); - - verify(mockWindow.open('https://www.google.com', '')); - }); - - testWidgets('mailto urls should be launched on the same window', - (WidgetTester _) async { - plugin.openNewWindow('mailto:name@mydomain.com'); - - verify(mockWindow.open('mailto:name@mydomain.com', '_top')); - }); - - testWidgets('tel urls should be launched on the same window', - (WidgetTester _) async { - plugin.openNewWindow('tel:5551234567'); - - verify(mockWindow.open('tel:5551234567', '_top')); - }); - - testWidgets('sms urls should be launched on the same window', - (WidgetTester _) async { - plugin.openNewWindow('sms:+19725551212?body=hello%20there'); - - verify( - mockWindow.open('sms:+19725551212?body=hello%20there', '_top')); - }); - testWidgets( - 'mailto urls should use _blank if webOnlyWindowName is set as _blank', - (WidgetTester _) async { - plugin.openNewWindow('mailto:name@mydomain.com', - webOnlyWindowName: '_blank'); - verify(mockWindow.open('mailto:name@mydomain.com', '_blank')); - }); - }); - }); - }); -} diff --git a/packages/url_launcher/url_launcher_web/test/test_driver/url_launcher_web_integration_test.dart b/packages/url_launcher/url_launcher_web/test/test_driver/url_launcher_web_integration_test.dart deleted file mode 100644 index 64e2248a4f9b..000000000000 --- a/packages/url_launcher/url_launcher_web/test/test_driver/url_launcher_web_integration_test.dart +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:integration_test/integration_test_driver.dart'; - -Future main() async => integrationDriver(); diff --git a/packages/url_launcher/url_launcher_web/test/tests_exist_elsewhere_test.dart b/packages/url_launcher/url_launcher_web/test/tests_exist_elsewhere_test.dart new file mode 100644 index 000000000000..442c50144727 --- /dev/null +++ b/packages/url_launcher/url_launcher_web/test/tests_exist_elsewhere_test.dart @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('Tell the user where to find the real tests', () { + print('---'); + print('This package uses integration_test for its tests.'); + print('See `example/README.md` for more info.'); + print('---'); + }); +} diff --git a/packages/url_launcher/url_launcher_web/test/web/index.html b/packages/url_launcher/url_launcher_web/test/web/index.html deleted file mode 100644 index dc8d0cfe0428..000000000000 --- a/packages/url_launcher/url_launcher_web/test/web/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - Browser Tests - - - - - diff --git a/packages/url_launcher/url_launcher_windows/AUTHORS b/packages/url_launcher/url_launcher_windows/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/url_launcher/url_launcher_windows/CHANGELOG.md b/packages/url_launcher/url_launcher_windows/CHANGELOG.md index 7f534a7e5a38..e906254eef44 100644 --- a/packages/url_launcher/url_launcher_windows/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_windows/CHANGELOG.md @@ -1,3 +1,25 @@ +## 2.0.0 + +* Migrate to null-safety. +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. +* Set `implementation` in pubspec.yaml + +## 0.0.2+1 + +* Update Flutter SDK constraint. + +## 0.0.2 + +* Update integration test examples to use `testWidgets` instead of `test`. + +## 0.0.1+3 + +* Update Dart SDK constraint in example. + +## 0.0.1+2 + +* Check in windows/ directory for example/ + ## 0.0.1+1 * Update README to reflect endorsement. diff --git a/packages/url_launcher/url_launcher_windows/LICENSE b/packages/url_launcher/url_launcher_windows/LICENSE index 507569823f1b..c6823b81eb84 100644 --- a/packages/url_launcher/url_launcher_windows/LICENSE +++ b/packages/url_launcher/url_launcher_windows/LICENSE @@ -1,4 +1,4 @@ -Copyright 2019 The Chromium Authors. All rights reserved. +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: diff --git a/packages/url_launcher/url_launcher_windows/README.md b/packages/url_launcher/url_launcher_windows/README.md index fb5ad6700d26..4cebb7ed91fb 100644 --- a/packages/url_launcher/url_launcher_windows/README.md +++ b/packages/url_launcher/url_launcher_windows/README.md @@ -2,13 +2,6 @@ The Windows implementation of [`url_launcher`][1]. -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.0.y+z`. If you use -url_launcher_windows directly, rather than as an implementation detail -of `url_launcher`, please use `url_launcher_windows: '>=0.0.y+x <2.0.0'` -as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ## Usage ### Import the package diff --git a/packages/url_launcher/url_launcher_windows/example/integration_test/url_launcher_test.dart b/packages/url_launcher/url_launcher_windows/example/integration_test/url_launcher_test.dart index 7a33e00fec0a..ae9a9148f9d7 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 @@ -1,6 +1,6 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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'; @@ -9,7 +9,7 @@ import 'package:url_launcher_platform_interface/url_launcher_platform_interface. void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - test('canLaunch', () async { + testWidgets('canLaunch', (WidgetTester _) async { UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; expect(await launcher.canLaunch('randomstring'), false); diff --git a/packages/url_launcher/url_launcher_windows/example/lib/main.dart b/packages/url_launcher/url_launcher_windows/example/lib/main.dart index db59af1e5b95..86e06f3fafed 100644 --- a/packages/url_launcher/url_launcher_windows/example/lib/main.dart +++ b/packages/url_launcher/url_launcher_windows/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -26,7 +26,7 @@ class MyApp extends StatelessWidget { } class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); + MyHomePage({Key? key, required this.title}) : super(key: key); final String title; @override @@ -34,7 +34,7 @@ class MyHomePage extends StatefulWidget { } class _MyHomePageState extends State { - Future _launched; + Future? _launched; Future _launchInBrowser(String url) async { if (await UrlLauncherPlatform.instance.canLaunch(url)) { @@ -76,7 +76,7 @@ class _MyHomePageState extends State { padding: EdgeInsets.all(16.0), child: Text(toLaunch), ), - RaisedButton( + ElevatedButton( onPressed: () => setState(() { _launched = _launchInBrowser(toLaunch); }), diff --git a/packages/url_launcher/url_launcher_windows/example/pubspec.yaml b/packages/url_launcher/url_launcher_windows/example/pubspec.yaml index 8d6c22057bc2..11be3e84f03b 100644 --- a/packages/url_launcher/url_launcher_windows/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_windows/example/pubspec.yaml @@ -1,19 +1,29 @@ name: url_launcher_example description: Demonstrates the Windows implementation of the url_launcher plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" dependencies: flutter: sdk: flutter - url_launcher_platform_interface: any + url_launcher_platform_interface: ^2.0.0 url_launcher_windows: + # When depending on this package from a real application you should use: + # url_launcher_windows: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: integration_test: - path: ../../../integration_test + sdk: flutter flutter_driver: sdk: flutter - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/url_launcher/url_launcher_windows/example/test_driver/integration_test.dart b/packages/url_launcher/url_launcher_windows/example/test_driver/integration_test.dart index 7a2c21338786..4f10f2a522f3 100644 --- a/packages/url_launcher/url_launcher_windows/example/test_driver/integration_test.dart +++ b/packages/url_launcher/url_launcher_windows/example/test_driver/integration_test.dart @@ -1,17 +1,7 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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'; +import 'package:integration_test/integration_test_driver.dart'; -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String data = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - await driver.close(); - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} +Future main() => integrationDriver(); diff --git a/packages/url_launcher/url_launcher_windows/example/windows/.gitignore b/packages/url_launcher/url_launcher_windows/example/windows/.gitignore new file mode 100644 index 000000000000..d492d0d98c8f --- /dev/null +++ b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_windows/example/windows/CMakeLists.txt b/packages/url_launcher/url_launcher_windows/example/windows/CMakeLists.txt new file mode 100644 index 000000000000..abf90408efb4 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/example/windows/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.15) +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/url_launcher/url_launcher_windows/example/windows/flutter/CMakeLists.txt b/packages/url_launcher/url_launcher_windows/example/windows/flutter/CMakeLists.txt new file mode 100644 index 000000000000..c7a8c7607d81 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/example/windows/flutter/CMakeLists.txt @@ -0,0 +1,101 @@ +cmake_minimum_required(VERSION 3.15) + +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" +) +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 $ +) +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/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 new file mode 100644 index 000000000000..d9fdd53925c5 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,14 @@ +// +// 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_windows/example/windows/flutter/generated_plugin_registrant.h b/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000000..dc139d85a931 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// 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/flutter/generated_plugins.cmake b/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..411af46dd721 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugins.cmake @@ -0,0 +1,16 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_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/url_launcher/url_launcher_windows/example/windows/runner/CMakeLists.txt b/packages/url_launcher/url_launcher_windows/example/windows/runner/CMakeLists.txt new file mode 100644 index 000000000000..977e38b5d1d2 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/example/windows/runner/CMakeLists.txt @@ -0,0 +1,18 @@ +cmake_minimum_required(VERSION 3.15) +project(runner LANGUAGES CXX) + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "run_loop.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/url_launcher/url_launcher_windows/example/windows/runner/Runner.rc b/packages/url_launcher/url_launcher_windows/example/windows/runner/Runner.rc new file mode 100644 index 000000000000..944329afc03a --- /dev/null +++ b/packages/url_launcher/url_launcher_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) 2020 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/url_launcher/url_launcher_windows/example/windows/runner/flutter_window.cpp b/packages/url_launcher/url_launcher_windows/example/windows/runner/flutter_window.cpp new file mode 100644 index 000000000000..8e415602cf3b --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/example/windows/runner/flutter_window.cpp @@ -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. + +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(RunLoop* run_loop, + const flutter::DartProject& project) + : run_loop_(run_loop), 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()); + run_loop_->RegisterFlutterInstance(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + run_loop_->UnregisterFlutterInstance(flutter_controller_->engine()); + 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 opporutunity 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/url_launcher/url_launcher_windows/example/windows/runner/flutter_window.h b/packages/url_launcher/url_launcher_windows/example/windows/runner/flutter_window.h new file mode 100644 index 000000000000..8e9c12bbe022 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/example/windows/runner/flutter_window.h @@ -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. + +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "run_loop.h" +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow driven by the |run_loop|, hosting a + // Flutter view running |project|. + explicit FlutterWindow(RunLoop* run_loop, + 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 run loop driving events for this window. + RunLoop* run_loop_; + + // 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/url_launcher/url_launcher_windows/example/windows/runner/main.cpp b/packages/url_launcher/url_launcher_windows/example/windows/runner/main.cpp new file mode 100644 index 000000000000..126302b0be18 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/example/windows/runner/main.cpp @@ -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. + +#include +#include +#include + +#include "flutter_window.h" +#include "run_loop.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); + + RunLoop run_loop; + + flutter::DartProject project(L"data"); + FlutterWindow window(&run_loop, project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + run_loop.Run(); + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/packages/url_launcher/url_launcher_windows/example/windows/runner/resource.h b/packages/url_launcher/url_launcher_windows/example/windows/runner/resource.h new file mode 100644 index 000000000000..d5d958dc4257 --- /dev/null +++ b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_windows/example/windows/runner/resources/app_icon.ico b/packages/url_launcher/url_launcher_windows/example/windows/runner/resources/app_icon.ico new file mode 100644 index 000000000000..c04e20caf637 Binary files /dev/null and b/packages/url_launcher/url_launcher_windows/example/windows/runner/resources/app_icon.ico differ diff --git a/packages/url_launcher/url_launcher_windows/example/windows/runner/run_loop.cpp b/packages/url_launcher/url_launcher_windows/example/windows/runner/run_loop.cpp new file mode 100644 index 000000000000..1916500e6440 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/example/windows/runner/run_loop.cpp @@ -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. + +#include "run_loop.h" + +#include + +#include + +RunLoop::RunLoop() {} + +RunLoop::~RunLoop() {} + +void RunLoop::Run() { + bool keep_running = true; + TimePoint next_flutter_event_time = TimePoint::clock::now(); + while (keep_running) { + std::chrono::nanoseconds wait_duration = + std::max(std::chrono::nanoseconds(0), + next_flutter_event_time - TimePoint::clock::now()); + ::MsgWaitForMultipleObjects( + 0, nullptr, FALSE, static_cast(wait_duration.count() / 1000), + QS_ALLINPUT); + bool processed_events = false; + MSG message; + // All pending Windows messages must be processed; MsgWaitForMultipleObjects + // won't return again for items left in the queue after PeekMessage. + while (::PeekMessage(&message, nullptr, 0, 0, PM_REMOVE)) { + processed_events = true; + if (message.message == WM_QUIT) { + keep_running = false; + break; + } + ::TranslateMessage(&message); + ::DispatchMessage(&message); + // Allow Flutter to process messages each time a Windows message is + // processed, to prevent starvation. + next_flutter_event_time = + std::min(next_flutter_event_time, ProcessFlutterMessages()); + } + // If the PeekMessage loop didn't run, process Flutter messages. + if (!processed_events) { + next_flutter_event_time = + std::min(next_flutter_event_time, ProcessFlutterMessages()); + } + } +} + +void RunLoop::RegisterFlutterInstance( + flutter::FlutterEngine* flutter_instance) { + flutter_instances_.insert(flutter_instance); +} + +void RunLoop::UnregisterFlutterInstance( + flutter::FlutterEngine* flutter_instance) { + flutter_instances_.erase(flutter_instance); +} + +RunLoop::TimePoint RunLoop::ProcessFlutterMessages() { + TimePoint next_event_time = TimePoint::max(); + for (auto instance : flutter_instances_) { + std::chrono::nanoseconds wait_duration = instance->ProcessMessages(); + if (wait_duration != std::chrono::nanoseconds::max()) { + next_event_time = + std::min(next_event_time, TimePoint::clock::now() + wait_duration); + } + } + return next_event_time; +} diff --git a/packages/url_launcher/url_launcher_windows/example/windows/runner/run_loop.h b/packages/url_launcher/url_launcher_windows/example/windows/runner/run_loop.h new file mode 100644 index 000000000000..819ed3ed4995 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/example/windows/runner/run_loop.h @@ -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. + +#ifndef RUNNER_RUN_LOOP_H_ +#define RUNNER_RUN_LOOP_H_ + +#include + +#include +#include + +// A runloop that will service events for Flutter instances as well +// as native messages. +class RunLoop { + public: + RunLoop(); + ~RunLoop(); + + // Prevent copying + RunLoop(RunLoop const&) = delete; + RunLoop& operator=(RunLoop const&) = delete; + + // Runs the run loop until the application quits. + void Run(); + + // Registers the given Flutter instance for event servicing. + void RegisterFlutterInstance(flutter::FlutterEngine* flutter_instance); + + // Unregisters the given Flutter instance from event servicing. + void UnregisterFlutterInstance(flutter::FlutterEngine* flutter_instance); + + private: + using TimePoint = std::chrono::steady_clock::time_point; + + // Processes all currently pending messages for registered Flutter instances. + TimePoint ProcessFlutterMessages(); + + std::set flutter_instances_; +}; + +#endif // RUNNER_RUN_LOOP_H_ diff --git a/packages/url_launcher/url_launcher_windows/example/windows/runner/runner.exe.manifest b/packages/url_launcher/url_launcher_windows/example/windows/runner/runner.exe.manifest new file mode 100644 index 000000000000..c977c4a42589 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/example/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + 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 new file mode 100644 index 000000000000..537728149601 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/example/windows/runner/utils.cpp @@ -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. + +#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(); + } +} diff --git a/packages/url_launcher/url_launcher_windows/example/windows/runner/utils.h b/packages/url_launcher/url_launcher_windows/example/windows/runner/utils.h new file mode 100644 index 000000000000..16b3f0794597 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/example/windows/runner/utils.h @@ -0,0 +1,12 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +#endif // RUNNER_UTILS_H_ diff --git a/packages/url_launcher/url_launcher_windows/example/windows/runner/win32_window.cpp b/packages/url_launcher/url_launcher_windows/example/windows/runner/win32_window.cpp new file mode 100644 index 000000000000..a609a2002bb3 --- /dev/null +++ b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_windows/example/windows/runner/win32_window.h b/packages/url_launcher/url_launcher_windows/example/windows/runner/win32_window.h new file mode 100644 index 000000000000..d2a730052223 --- /dev/null +++ b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_windows/ios/url_launcher_windows.podspec b/packages/url_launcher/url_launcher_windows/ios/url_launcher_windows.podspec deleted file mode 100644 index 1c700d49f4b5..000000000000 --- a/packages/url_launcher/url_launcher_windows/ios/url_launcher_windows.podspec +++ /dev/null @@ -1,22 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# Run `pod lib lint url_launcher_windows.podspec' to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'url_launcher_windows' - s.version = '0.0.1' - s.summary = 'url_launcher_windows iOS stub' - s.description = <<-DESC - No-op implementation of the windows url_launcher plugin to avoid build issues on iOS - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_windows' } - s.dependency 'Flutter' - s.platform = :ios, '8.0' - - # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } - s.swift_version = '5.0' -end diff --git a/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart b/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart deleted file mode 100644 index 83435f981838..000000000000 --- a/packages/url_launcher/url_launcher_windows/lib/url_launcher_windows.dart +++ /dev/null @@ -1,3 +0,0 @@ -// The url_launcher_platform_interface defaults to MethodChannelUrlLauncher -// as its instance, which is all the Windows implementation needs. This file -// is here to silence warnings when publishing to pub. diff --git a/packages/url_launcher/url_launcher_windows/pubspec.yaml b/packages/url_launcher/url_launcher_windows/pubspec.yaml index 6a653b7f7665..1a82f3e94a43 100644 --- a/packages/url_launcher/url_launcher_windows/pubspec.yaml +++ b/packages/url_launcher/url_launcher_windows/pubspec.yaml @@ -1,21 +1,20 @@ name: url_launcher_windows description: Windows implementation of the url_launcher plugin. -# 0.0.y+z is compatible with 1.0.0, if you land a breaking change bump -# the version to 2.0.0. -# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.0.1+1 -homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_windows +repository: https://github.com/flutter/plugins/tree/master/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.0 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: + implements: url_launcher platforms: windows: pluginClass: UrlLauncherPlugin -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" - dependencies: flutter: sdk: flutter diff --git a/packages/url_launcher/url_launcher_windows/windows/include/url_launcher_windows/url_launcher_plugin.h b/packages/url_launcher/url_launcher_windows/windows/include/url_launcher_windows/url_launcher_plugin.h index cdd018a0924e..8af3924ded81 100644 --- a/packages/url_launcher/url_launcher_windows/windows/include/url_launcher_windows/url_launcher_plugin.h +++ b/packages/url_launcher/url_launcher_windows/windows/include/url_launcher_windows/url_launcher_plugin.h @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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_URL_LAUNCHER_URL_LAUNCHER_WINDOWS_WINDOWS_INCLUDE_URL_LAUNCHER_WINDOWS_URL_LAUNCHER_PLUGIN_H_ 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 dfb406385787..51740a3a4b04 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 @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/url_launcher_windows/url_launcher_plugin.h" diff --git a/packages/video_player/analysis_options.yaml b/packages/video_player/analysis_options.yaml new file mode 100644 index 000000000000..cda4f6e153e6 --- /dev/null +++ b/packages/video_player/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options_legacy.yaml diff --git a/packages/video_player/video_player/AUTHORS b/packages/video_player/video_player/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/video_player/video_player/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index f7fad2648b3e..5084021b33de 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,92 @@ +## 2.1.6 + +* Remove obsolete pre-1.0 warning from README. +* Add iOS unit and UI integration test targets. + +## 2.1.5 + +* Update example code in README to fix broken url. + +## 2.1.4 + +* Add an exoplayer URL to the maven repositories to address + a possible build regression in 2.1.2. + +## 2.1.3 + +* Fix pointer value to boolean conversion analyzer warnings. + +## 2.1.2 + +* Migrate maven repository from jcenter to mavenCentral. + +## 2.1.1 + +* Update example code in README to reflect API changes. + +## 2.1.0 + +* Add `httpHeaders` option to `VideoPlayerController.network` + +## 2.0.2 + +* Fix `VideoPlayerValue` size and aspect ratio documentation + +## 2.0.1 + +* Remove the deprecated API "exoPlayer.setAudioAttributes". + +## 2.0.0 + +* Migrate to null safety. +* Fix an issue where `isBuffering` was not updating on Android. +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) +* Fix `VideoPlayerValue toString()` test. +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. +* Migrate from deprecated `defaultBinaryMessenger`. +* Fix an issue where a crash can occur after a closing a video player view on iOS. +* Setting the `mixWithOthers` `VideoPlayerOptions` in web now is silently ignored instead of throwing an exception. + +## 1.0.2 + +* Update Flutter SDK constraint. + +## 1.0.1 + +* Android: Dispose video players when app is closed. + +## 1.0.0 + +* Announce 1.0.0. + +## 0.11.1+5 + +* Update Dart SDK constraint in example. +* Remove `test` dependency. +* Convert disabled driver test to integration_test. + +## 0.11.1+4 + +* Add `toString()` to `Caption`. +* Fix a bug on Android when loading videos from assets would crash. + +## 0.11.1+3 + +* Android: Upgrade ExoPlayer to 2.12.1. + +## 0.11.1+2 + +* Update android compileSdkVersion to 29. + +## 0.11.1+1 + +* Fixed uncanceled timers when calling `play` on the controller multiple times before `pause`, which + caused value listeners to be called indefinitely (after `pause`) and more often than needed. + +## 0.11.1 + +* Enable TLSv1.1 & TLSv1.2 for API 19 and below. + ## 0.11.0 * Added option to set the video playback speed on the video controller. diff --git a/packages/video_player/video_player/CONTRIBUTING.md b/packages/video_player/video_player/CONTRIBUTING.md new file mode 100644 index 000000000000..15c48038f6fc --- /dev/null +++ b/packages/video_player/video_player/CONTRIBUTING.md @@ -0,0 +1,82 @@ +## Updating pigeon-generated files + +If you update files in the pigeons/ directory, run the following +command in this directory (ignore the errors you get about +dependencies in the examples directory): + +```bash +flutter pub upgrade +flutter pub run pigeon --dart_null_safety --input pigeons/messages.dart +# git commit your changes so that your working environment is clean +(cd ../../../; ./script/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/LICENSE b/packages/video_player/video_player/LICENSE index a6d6c0749818..c6823b81eb84 100644 --- a/packages/video_player/video_player/LICENSE +++ b/packages/video_player/video_player/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017 The Chromium Authors. All rights reserved. +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: diff --git a/packages/video_player/video_player/README.md b/packages/video_player/video_player/README.md index 9c0e6b1abb28..7140527afb9f 100644 --- a/packages/video_player/video_player/README.md +++ b/packages/video_player/video_player/README.md @@ -1,25 +1,14 @@ # Video Player plugin for Flutter -[![pub package](https://img.shields.io/pub/v/video_player.svg)](https://pub.dartlang.org/packages/video_player) +[![pub package](https://img.shields.io/pub/v/video_player.svg)](https://pub.dev/packages/video_player) A Flutter plugin for iOS, Android and Web for playing back video on a Widget surface. -**Please set your constraint to `video_player: '>=0.10.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.10.y+z`. -Please use `video_player: '>=0.10.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ![The example app running in iOS](https://github.com/flutter/plugins/blob/master/packages/video_player/video_player/doc/demo_ipod.gif?raw=true) -*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! - ## Installation -First, add `video_player` as a [dependency in your pubspec.yaml file](https://flutter.io/using-packages/). +First, add `video_player` as a [dependency in your pubspec.yaml file](https://flutter.dev/using-packages/). ### iOS @@ -55,6 +44,8 @@ This plugin compiles for the web platform since version `0.10.5`, in recent enou Different web browsers may have different video-playback capabilities (supported formats, autoplay...). Check [package:video_player_web](https://pub.dev/packages/video_player_web) for more web-specific information. +The `VideoPlayerOptions.mixWithOthers` option can't be implemented in web, at least at the moment. If you use this option in web it will be silently ignored. + ## Supported Formats - On iOS, the backing player is [AVPlayer](https://developer.apple.com/documentation/avfoundation/avplayer). @@ -84,7 +75,7 @@ class _VideoAppState extends State { void initState() { super.initState(); _controller = VideoPlayerController.network( - 'http://www.sample-videos.com/video123/mp4/720/big_buck_bunny_720p_20mb.mp4') + 'https://sample-videos.com/video123/mp4/720/big_buck_bunny_720p_20mb.mp4') ..initialize().then((_) { // Ensure the first frame is shown after the video is initialized, even before the play button has been pressed. setState(() {}); @@ -97,7 +88,7 @@ class _VideoAppState extends State { title: 'Video Demo', home: Scaffold( body: Center( - child: _controller.value.initialized + child: _controller.value.isInitialized ? AspectRatio( aspectRatio: _controller.value.aspectRatio, child: VideoPlayer(_controller), diff --git a/packages/video_player/video_player/android/build.gradle b/packages/video_player/video_player/android/build.gradle index 12725f3f7142..2f0f5c16c37a 100644 --- a/packages/video_player/video_player/android/build.gradle +++ b/packages/video_player/video_player/android/build.gradle @@ -5,18 +5,24 @@ def args = ["-Xlint:deprecation","-Xlint:unchecked","-Werror"] buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:3.5.0' } } rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() + // Gradle versions older than 2.13.3 aren't published to the servers + // above, so add this URL as a workaround until upgrading past that + // version. + maven { + url 'https://google.bintray.com/exoplayer/' + } } } @@ -27,7 +33,7 @@ project.getTasks().withType(JavaCompile){ apply plugin: 'com.android.library' android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { minSdkVersion 16 @@ -44,9 +50,9 @@ android { } dependencies { - implementation 'com.google.android.exoplayer:exoplayer-core:2.9.6' - implementation 'com.google.android.exoplayer:exoplayer-hls:2.9.6' - implementation 'com.google.android.exoplayer:exoplayer-dash:2.9.6' - implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.9.6' + implementation 'com.google.android.exoplayer:exoplayer-core:2.12.1' + implementation 'com.google.android.exoplayer:exoplayer-hls:2.12.1' + implementation 'com.google.android.exoplayer:exoplayer-dash:2.12.1' + implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.12.1' } } 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/src/main/java/io/flutter/plugins/videoplayer/CustomSSLSocketFactory.java new file mode 100644 index 000000000000..fb6d2d4108cd --- /dev/null +++ b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/CustomSSLSocketFactory.java @@ -0,0 +1,74 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.videoplayer; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +public class CustomSSLSocketFactory extends SSLSocketFactory { + private SSLSocketFactory sslSocketFactory; + + public CustomSSLSocketFactory() throws KeyManagementException, NoSuchAlgorithmException { + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, null, null); + sslSocketFactory = context.getSocketFactory(); + } + + @Override + public String[] getDefaultCipherSuites() { + return sslSocketFactory.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return sslSocketFactory.getSupportedCipherSuites(); + } + + @Override + public Socket createSocket() throws IOException { + return enableProtocols(sslSocketFactory.createSocket()); + } + + @Override + public Socket createSocket(Socket s, String host, int port, boolean autoClose) + throws IOException { + return enableProtocols(sslSocketFactory.createSocket(s, host, port, autoClose)); + } + + @Override + public Socket createSocket(String host, int port) throws IOException { + return enableProtocols(sslSocketFactory.createSocket(host, port)); + } + + @Override + public Socket createSocket(String host, int port, InetAddress localHost, int localPort) + throws IOException { + return enableProtocols(sslSocketFactory.createSocket(host, port, localHost, localPort)); + } + + @Override + public Socket createSocket(InetAddress host, int port) throws IOException { + return enableProtocols(sslSocketFactory.createSocket(host, port)); + } + + @Override + public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) + throws IOException { + return enableProtocols(sslSocketFactory.createSocket(address, port, localAddress, localPort)); + } + + private Socket enableProtocols(Socket socket) { + if (socket instanceof SSLSocket) { + ((SSLSocket) socket).setEnabledProtocols(new String[] {"TLSv1.1", "TLSv1.2"}); + } + return socket; + } +} diff --git a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java index 78da7150edf0..e0a4a3b8dd08 100644 --- a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java +++ b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/Messages.java @@ -1,4 +1,8 @@ -// Autogenerated from Pigeon (v0.1.7), do not edit directly. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Autogenerated from Pigeon (v0.1.21), do not edit directly. // See also: https://pub.dev/packages/pigeon package io.flutter.plugins.videoplayer; @@ -83,12 +87,23 @@ 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; } @@ -102,6 +117,8 @@ static CreateMessage fromMap(HashMap map) { fromMapResult.packageName = (String) packageName; Object formatHint = map.get("formatHint"); fromMapResult.formatHint = (String) formatHint; + Object httpHeaders = map.get("httpHeaders"); + fromMapResult.httpHeaders = (HashMap) httpHeaders; return fromMapResult; } } @@ -597,7 +614,7 @@ static void setup(BinaryMessenger binaryMessenger, VideoPlayerApi api) { private static HashMap wrapError(Exception exception) { HashMap errorMap = new HashMap<>(); errorMap.put("message", exception.toString()); - errorMap.put("code", null); + errorMap.put("code", exception.getClass().getSimpleName()); errorMap.put("details", null); return errorMap; } diff --git a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java index 18835271a83a..981389583d2d 100644 --- a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java +++ b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/QueuingEventSink.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java index 8f8c898dea27..87784eebdefe 100644 --- a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java +++ b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayer.java @@ -1,3 +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. + package io.flutter.plugins.videoplayer; import static com.google.android.exoplayer2.Player.REPEAT_MODE_ALL; @@ -5,27 +9,23 @@ import android.content.Context; import android.net.Uri; -import android.os.Build; import android.view.Surface; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; -import com.google.android.exoplayer2.ExoPlayerFactory; import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.MediaItem; import com.google.android.exoplayer2.PlaybackParameters; import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.Player.EventListener; import com.google.android.exoplayer2.SimpleExoPlayer; import com.google.android.exoplayer2.audio.AudioAttributes; -import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; -import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; +import com.google.android.exoplayer2.source.ProgressiveMediaSource; import com.google.android.exoplayer2.source.dash.DashMediaSource; import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.source.smoothstreaming.DefaultSsChunkSource; import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource; -import com.google.android.exoplayer2.trackselection.DefaultTrackSelector; -import com.google.android.exoplayer2.trackselection.TrackSelector; import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; @@ -65,31 +65,36 @@ final class VideoPlayer { TextureRegistry.SurfaceTextureEntry textureEntry, String dataSource, String formatHint, + Map httpHeaders, VideoPlayerOptions options) { this.eventChannel = eventChannel; this.textureEntry = textureEntry; this.options = options; - TrackSelector trackSelector = new DefaultTrackSelector(); - exoPlayer = ExoPlayerFactory.newSimpleInstance(context, trackSelector); + exoPlayer = new SimpleExoPlayer.Builder(context).build(); Uri uri = Uri.parse(dataSource); DataSource.Factory dataSourceFactory; if (isHTTP(uri)) { - dataSourceFactory = + DefaultHttpDataSourceFactory httpDataSourceFactory = new DefaultHttpDataSourceFactory( "ExoPlayer", null, DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, true); + if (httpHeaders != null && !httpHeaders.isEmpty()) { + httpDataSourceFactory.getDefaultRequestProperties().set(httpHeaders); + } + dataSourceFactory = httpDataSourceFactory; } else { dataSourceFactory = new DefaultDataSourceFactory(context, "ExoPlayer"); } MediaSource mediaSource = buildMediaSource(uri, dataSourceFactory, formatHint, context); - exoPlayer.prepare(mediaSource); + exoPlayer.setMediaSource(mediaSource); + exoPlayer.prepare(); setupVideoPlayer(eventChannel, textureEntry); } @@ -131,18 +136,18 @@ private MediaSource buildMediaSource( return new SsMediaSource.Factory( new DefaultSsChunkSource.Factory(mediaDataSourceFactory), new DefaultDataSourceFactory(context, null, mediaDataSourceFactory)) - .createMediaSource(uri); + .createMediaSource(MediaItem.fromUri(uri)); case C.TYPE_DASH: return new DashMediaSource.Factory( new DefaultDashChunkSource.Factory(mediaDataSourceFactory), new DefaultDataSourceFactory(context, null, mediaDataSourceFactory)) - .createMediaSource(uri); + .createMediaSource(MediaItem.fromUri(uri)); case C.TYPE_HLS: - return new HlsMediaSource.Factory(mediaDataSourceFactory).createMediaSource(uri); + return new HlsMediaSource.Factory(mediaDataSourceFactory) + .createMediaSource(MediaItem.fromUri(uri)); case C.TYPE_OTHER: - return new ExtractorMediaSource.Factory(mediaDataSourceFactory) - .setExtractorsFactory(new DefaultExtractorsFactory()) - .createMediaSource(uri); + return new ProgressiveMediaSource.Factory(mediaDataSourceFactory) + .createMediaSource(MediaItem.fromUri(uri)); default: { throw new IllegalStateException("Unsupported type: " + type); @@ -172,10 +177,21 @@ public void onCancel(Object o) { exoPlayer.addListener( new EventListener() { + private boolean isBuffering = false; + + public void setBuffering(boolean buffering) { + if (isBuffering != buffering) { + isBuffering = buffering; + Map event = new HashMap<>(); + event.put("event", isBuffering ? "bufferingStart" : "bufferingEnd"); + eventSink.success(event); + } + } @Override - public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) { + public void onPlaybackStateChanged(final int playbackState) { if (playbackState == Player.STATE_BUFFERING) { + setBuffering(true); sendBufferingUpdate(); } else if (playbackState == Player.STATE_READY) { if (!isInitialized) { @@ -187,10 +203,15 @@ public void onPlayerStateChanged(final boolean playWhenReady, final int playback event.put("event", "completed"); eventSink.success(event); } + + if (playbackState != Player.STATE_BUFFERING) { + setBuffering(false); + } } @Override public void onPlayerError(final ExoPlaybackException error) { + setBuffering(false); if (eventSink != null) { eventSink.error("VideoError", "Video player had error " + error, null); } @@ -209,12 +230,8 @@ void sendBufferingUpdate() { @SuppressWarnings("deprecation") private static void setAudioAttributes(SimpleExoPlayer exoPlayer, boolean isMixMode) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - exoPlayer.setAudioAttributes( - new AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MOVIE).build(), !isMixMode); - } else { - exoPlayer.setAudioStreamType(C.STREAM_TYPE_MUSIC); - } + exoPlayer.setAudioAttributes( + new AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MOVIE).build(), !isMixMode); } void play() { 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/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java index 7381f4a941a5..85ad892f9e19 100644 --- a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java +++ b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.java @@ -1,3 +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. + package io.flutter.plugins.videoplayer; class VideoPlayerOptions { 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/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java index 7f863051f616..d77b45e03d4b 100644 --- a/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java +++ b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerPlugin.java @@ -1,13 +1,14 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // 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 android.content.Context; -import android.util.Log; +import android.os.Build; import android.util.LongSparseArray; -import io.flutter.embedding.engine.loader.FlutterLoader; +import io.flutter.FlutterInjector; +import io.flutter.Log; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.EventChannel; @@ -20,6 +21,10 @@ import io.flutter.plugins.videoplayer.Messages.VideoPlayerApi; import io.flutter.plugins.videoplayer.Messages.VolumeMessage; import io.flutter.view.TextureRegistry; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.util.Map; +import javax.net.ssl.HttpsURLConnection; /** Android platform implementation of the VideoPlayerPlugin. */ public class VideoPlayerPlugin implements FlutterPlugin, VideoPlayerApi { @@ -56,14 +61,27 @@ public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registra @Override public void onAttachedToEngine(FlutterPluginBinding binding) { - @SuppressWarnings("deprecation") - final FlutterLoader flutterLoader = FlutterLoader.getInstance(); + + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + try { + HttpsURLConnection.setDefaultSSLSocketFactory(new CustomSSLSocketFactory()); + } catch (KeyManagementException | NoSuchAlgorithmException e) { + Log.w( + TAG, + "Failed to enable TLSv1.1 and TLSv1.2 Protocols for API level 19 and below.\n" + + "For more information about Socket Security, please consult the following link:\n" + + "https://developer.android.com/reference/javax/net/ssl/SSLSocket", + e); + } + } + + final FlutterInjector injector = FlutterInjector.instance(); this.flutterState = new FlutterState( binding.getApplicationContext(), binding.getBinaryMessenger(), - flutterLoader::getLookupKeyForAsset, - flutterLoader::getLookupKeyForAsset, + injector.flutterLoader()::getLookupKeyForAsset, + injector.flutterLoader()::getLookupKeyForAsset, binding.getTextureRegistry()); flutterState.startListening(this, binding.getBinaryMessenger()); } @@ -75,6 +93,7 @@ public void onDetachedFromEngine(FlutterPluginBinding binding) { } flutterState.stopListening(binding.getBinaryMessenger()); flutterState = null; + initialize(); } private void disposeAllPlayers() { @@ -120,9 +139,11 @@ public TextureMessage create(CreateMessage arg) { handle, "asset:///" + assetLookupKey, null, + null, options); - videoPlayers.put(handle.id(), player); } else { + @SuppressWarnings("unchecked") + Map httpHeaders = arg.getHttpHeaders(); player = new VideoPlayer( flutterState.applicationContext, @@ -130,9 +151,10 @@ public TextureMessage create(CreateMessage arg) { handle, arg.getUri(), arg.getFormatHint(), + httpHeaders, options); - videoPlayers.put(handle.id(), player); } + videoPlayers.put(handle.id(), player); TextureMessage result = new TextureMessage(); result.setTextureId(handle.id()); diff --git a/packages/video_player/video_player/example/README.md b/packages/video_player/video_player/example/README.md index 55b086b4f33f..8ceb0ff485fa 100644 --- a/packages/video_player/video_player/example/README.md +++ b/packages/video_player/video_player/example/README.md @@ -5,4 +5,4 @@ Demonstrates how to use the video_player plugin. ## Getting Started For help getting started with Flutter, view our online -[documentation](http://flutter.io/). +[documentation](https://flutter.dev/). diff --git a/packages/video_player/video_player/example/android/app/build.gradle b/packages/video_player/video_player/example/android/app/build.gradle index 47e7214822cc..fc0673ad1bd8 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 28 + compileSdkVersion 29 lintOptions { disable 'InvalidPackage' @@ -34,7 +34,7 @@ android { defaultConfig { applicationId "io.flutter.plugins.videoplayerexample" minSdkVersion 16 - targetSdkVersion 28 + targetSdkVersion 29 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -64,4 +64,6 @@ 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/video_player/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/EmbeddingV1Activity.java b/packages/video_player/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/EmbeddingV1Activity.java index 841853dbd758..87c39ca07117 100644 --- a/packages/video_player/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/EmbeddingV1Activity.java +++ b/packages/video_player/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/EmbeddingV1Activity.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/example/android/app/src/test/java/io/flutter/plugins/videoplayerexample/FlutterActivityTest.java b/packages/video_player/video_player/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/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/example/android/build.gradle b/packages/video_player/video_player/example/android/build.gradle index 112aa2a87c27..456d020f6e2c 100644 --- a/packages/video_player/video_player/example/android/build.gradle +++ b/packages/video_player/video_player/example/android/build.gradle @@ -1,21 +1,18 @@ buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.0' + classpath 'com.android.tools.build:gradle:3.5.0' } } allprojects { repositories { google() - jcenter() - maven { - url 'https://google.bintray.com/exoplayer/' - } + mavenCentral() } } 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 2819f022f1fd..296b146b7318 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-4.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip 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 0953c8feb6c0..87575df3cf09 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 @@ -1,8 +1,11 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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: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:video_player/video_player.dart'; @@ -11,7 +14,7 @@ const Duration _playDuration = Duration(seconds: 1); void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); - VideoPlayerController _controller; + late VideoPlayerController _controller; tearDown(() async => _controller.dispose()); group('asset videos', () { @@ -22,45 +25,139 @@ void main() { testWidgets('can be initialized', (WidgetTester tester) async { await _controller.initialize(); - expect(_controller.value.initialized, true); + expect(_controller.value.isInitialized, true); expect(_controller.value.position, const Duration(seconds: 0)); expect(_controller.value.isPlaying, false); expect(_controller.value.duration, const Duration(seconds: 7, milliseconds: 540)); }); - testWidgets('can be played', (WidgetTester tester) async { - await _controller.initialize(); + 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 _controller.play(); - await tester.pumpAndSettle(_playDuration); + await networkController.play(); + await networkController.seekTo(const Duration(seconds: 5)); + await tester.pumpAndSettle(_playDuration); + await networkController.pause(); - expect(_controller.value.isPlaying, true); - expect(_controller.value.position, - (Duration position) => position > const Duration(seconds: 0)); - }, skip: Platform.isIOS); + expect(networkController.value.isPlaying, false); + expect(networkController.value.position, + (Duration position) => position > const Duration(seconds: 0)); - testWidgets('can seek', (WidgetTester tester) async { - await _controller.initialize(); + await started; + expect(startedBuffering, true); - await _controller.seekTo(const Duration(seconds: 3)); + await ended; + expect(endedBuffering, true); + }, + skip: !(kIsWeb || defaultTargetPlatform == TargetPlatform.android), + ); - expect(_controller.value.position, const Duration(seconds: 3)); - }, skip: Platform.isIOS); + 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); - testWidgets('can be paused', (WidgetTester tester) async { - await _controller.initialize(); + await _controller.play(); + await tester.pumpAndSettle(_playDuration); - // 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); + expect(_controller.value.isPlaying, true); + expect(_controller.value.position, + (Duration position) => position > const Duration(seconds: 0)); + }, + ); - // Verify that we stopped playing after the pause. - expect(_controller.value.isPlaying, false); - expect(_controller.value.position, pausedPosition); - }, skip: Platform.isIOS); + 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); + }, + ); + + testWidgets('test video player view with local asset', + (WidgetTester tester) async { + Future started() async { + await _controller.initialize(); + await _controller.play(); + return true; + } + + await tester.pumpWidget(Material( + elevation: 0, + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: FutureBuilder( + future: started(), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.data == true) { + return AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: VideoPlayer(_controller), + ); + } else { + return const Text('waiting for video to load'); + } + }, + ), + ), + ), + )); + + await tester.pumpAndSettle(); + expect(_controller.value.isPlaying, true); + }, skip: kIsWeb); // Web does not support local assets. }); } diff --git a/packages/video_player/video_player/example/ios/Podfile b/packages/video_player/video_player/example/ios/Podfile new file mode 100644 index 000000000000..3924e59aa0f9 --- /dev/null +++ b/packages/video_player/video_player/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/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj b/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.pbxproj index 9f0a7ef189b9..7cef313627f0 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 @@ -9,18 +9,34 @@ /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 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; @@ -28,8 +44,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -40,14 +54,15 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 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 = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; 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 = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -56,6 +71,12 @@ 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 */ @@ -63,12 +84,25 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 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 */ @@ -77,6 +111,8 @@ children = ( C18C242FF01156F58C0DAF1C /* Pods-Runner.debug.xcconfig */, B15EC39F4617FE1082B18834 /* Pods-Runner.release.xcconfig */, + 6CDC4DA5940705A6E7671616 /* Pods-RunnerTests.debug.xcconfig */, + 2A2EA522BDC492279A91AB75 /* Pods-RunnerTests.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -85,6 +121,7 @@ isa = PBXGroup; children = ( 20721C28387E1F78689EC502 /* libPods-Runner.a */, + 7BD232FD3BD3343A5F52AF50 /* libPods-RunnerTests.a */, ); name = Frameworks; sourceTree = ""; @@ -92,9 +129,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, @@ -107,6 +142,8 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, + F7151F3B26603ECA0028CB91 /* RunnerTests */, + F7151F2D26603EBD0028CB91 /* RunnerUITests */, 97C146EF1CF9000F007C117D /* Products */, 05E898481BC29A7FA83AA441 /* Pods */, 23104BB9DCF267F65AD246F9 /* Frameworks */, @@ -117,6 +154,8 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + F7151F2C26603EBD0028CB91 /* RunnerUITests.xctest */, + F7151F3A26603ECA0028CB91 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -145,6 +184,24 @@ 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 */ @@ -159,7 +216,6 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 929A04F81CC936396BFCB39E /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -170,6 +226,43 @@ 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 */ @@ -177,11 +270,21 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; + 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" */; @@ -198,6 +301,8 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + F7151F3926603ECA0028CB91 /* RunnerTests */, + F7151F2B26603EBD0028CB91 /* RunnerUITests */, ); }; /* End PBXProject section */ @@ -214,6 +319,20 @@ ); 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 */ @@ -229,36 +348,43 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 929A04F81CC936396BFCB39E /* [CP] Embed Pods Frameworks */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputPaths = ( ); - name = "[CP] Embed Pods Frameworks"; + name = "Run Script"; outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - 9740EEB61CF901F6004384FC /* Run Script */ = { + 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 = ( ); - name = "Run Script"; outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + 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; @@ -291,8 +417,37 @@ ); 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; @@ -315,7 +470,6 @@ /* Begin XCBuildConfiguration section */ 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -372,7 +526,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -437,7 +590,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.videoPlayerExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.videoPlayerExample; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -458,11 +611,67 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.videoPlayerExample; + 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 */ @@ -484,6 +693,24 @@ 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/project.xcworkspace/contents.xcworkspacedata b/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16ed0f..919434a6254f 100644 --- a/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/packages/video_player/video_player/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> 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 3bb3697ef41c..3f1ee9541e2f 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 @@ -37,6 +37,26 @@ + + + + + + + + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/video_player/video_player/example/ios/Runner/AppDelegate.h b/packages/video_player/video_player/example/ios/Runner/AppDelegate.h index d9e18e990f2e..0681d288bb70 100644 --- a/packages/video_player/video_player/example/ios/Runner/AppDelegate.h +++ b/packages/video_player/video_player/example/ios/Runner/AppDelegate.h @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/example/ios/Runner/AppDelegate.m b/packages/video_player/video_player/example/ios/Runner/AppDelegate.m index f08675707182..30b87969f44a 100644 --- a/packages/video_player/video_player/example/ios/Runner/AppDelegate.m +++ b/packages/video_player/video_player/example/ios/Runner/AppDelegate.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/example/ios/Runner/main.m b/packages/video_player/video_player/example/ios/Runner/main.m index bec320c0bee0..f97b9ef5c8a1 100644 --- a/packages/video_player/video_player/example/ios/Runner/main.m +++ b/packages/video_player/video_player/example/ios/Runner/main.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/example/ios/RunnerTests/Info.plist b/packages/video_player/video_player/example/ios/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/video_player/video_player/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/example/ios/RunnerTests/VideoPlayerTests.m b/packages/video_player/video_player/example/ios/RunnerTests/VideoPlayerTests.m new file mode 100644 index 000000000000..890866f34952 --- /dev/null +++ b/packages/video_player/video_player/example/ios/RunnerTests/VideoPlayerTests.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 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/Info.plist b/packages/video_player/video_player/example/ios/RunnerUITests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/video_player/video_player/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/example/ios/RunnerUITests/VideoPlayerUITests.m b/packages/video_player/video_player/example/ios/RunnerUITests/VideoPlayerUITests.m new file mode 100644 index 000000000000..62d8c532ca03 --- /dev/null +++ b/packages/video_player/video_player/example/ios/RunnerUITests/VideoPlayerUITests.m @@ -0,0 +1,52 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import 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 a99b9da6bd0c..eef23197ef50 100644 --- a/packages/video_player/video_player/example/lib/main.dart +++ b/packages/video_player/video_player/example/lib/main.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -108,7 +108,7 @@ class _ButterFlyAssetVideoInList extends StatelessWidget { /// A filler card to show the video in a list of scrolling contents. class _ExampleCard extends StatelessWidget { - const _ExampleCard({Key key, this.title}) : super(key: key); + const _ExampleCard({Key? key, required this.title}) : super(key: key); final String title; @@ -124,13 +124,13 @@ class _ExampleCard extends StatelessWidget { ), ButtonBar( children: [ - FlatButton( + TextButton( child: const Text('BUY TICKETS'), onPressed: () { /* ... */ }, ), - FlatButton( + TextButton( child: const Text('SELL TICKETS'), onPressed: () { /* ... */ @@ -150,7 +150,7 @@ class _ButterFlyAssetVideo extends StatefulWidget { } class _ButterFlyAssetVideoState extends State<_ButterFlyAssetVideo> { - VideoPlayerController _controller; + late VideoPlayerController _controller; @override void initState() { @@ -206,7 +206,7 @@ class _BumbleBeeRemoteVideo extends StatefulWidget { } class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { - VideoPlayerController _controller; + late VideoPlayerController _controller; Future _loadCaptions() async { final String fileContents = await DefaultAssetBundle.of(context) @@ -265,7 +265,8 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { } class _ControlsOverlay extends StatelessWidget { - const _ControlsOverlay({Key key, this.controller}) : super(key: key); + const _ControlsOverlay({Key? key, required this.controller}) + : super(key: key); static const _examplePlaybackRates = [ 0.25, @@ -345,7 +346,7 @@ class _PlayerVideoAndPopPage extends StatefulWidget { } class _PlayerVideoAndPopPageState extends State<_PlayerVideoAndPopPage> { - VideoPlayerController _videoPlayerController; + late VideoPlayerController _videoPlayerController; bool startedPlaying = false; @override diff --git a/packages/video_player/video_player/example/pubspec.yaml b/packages/video_player/video_player/example/pubspec.yaml index e0afa4193dc2..63f179a06211 100644 --- a/packages/video_player/video_player/example/pubspec.yaml +++ b/packages/video_player/video_player/example/pubspec.yaml @@ -1,10 +1,20 @@ 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: ">=1.12.13+hotfix.5" dependencies: flutter: sdk: flutter video_player: + # When depending on this package from a real application you should use: + # video_player: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: @@ -13,9 +23,9 @@ dev_dependencies: flutter_driver: sdk: flutter integration_test: - path: ../../../integration_test + sdk: flutter test: any - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true diff --git a/packages/video_player/video_player/example/test_driver/integration_test.dart b/packages/video_player/video_player/example/test_driver/integration_test.dart index 7a2c21338786..4f10f2a522f3 100644 --- a/packages/video_player/video_player/example/test_driver/integration_test.dart +++ b/packages/video_player/video_player/example/test_driver/integration_test.dart @@ -1,17 +1,7 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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'; +import 'package:integration_test/integration_test_driver.dart'; -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String data = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - await driver.close(); - final Map result = jsonDecode(data); - exit(result['result'] == 'true' ? 0 : 1); -} +Future main() => integrationDriver(); diff --git a/packages/video_player/video_player/example/test_driver/video_player.dart b/packages/video_player/video_player/example/test_driver/video_player.dart index cc498f41fccb..b72354e2187f 100644 --- a/packages/video_player/video_player/example/test_driver/video_player.dart +++ b/packages/video_player/video_player/example/test_driver/video_player.dart @@ -1,6 +1,6 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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; 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 47f3867d9019..1d5ac79c77bf 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 @@ -1,6 +1,6 @@ -// Copyright 2019, the Chromium project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All 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_driver/flutter_driver.dart'; diff --git a/packages/video_player/video_player/example/web/index.html b/packages/video_player/video_player/example/web/index.html index b1c45bdcd57f..0df50f1192dc 100644 --- a/packages/video_player/video_player/example/web/index.html +++ b/packages/video_player/video_player/example/web/index.html @@ -1,4 +1,7 @@ + diff --git a/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.h b/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.h index 18fdcca6d54e..6c9d91468d6b 100644 --- a/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.h +++ b/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.h @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/ios/Classes/FLTVideoPlayerPlugin.m b/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m index e6a4f6ccb0b7..b359c1b6c898 100644 --- a/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m +++ b/packages/video_player/video_player/ios/Classes/FLTVideoPlayerPlugin.m @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -46,7 +46,9 @@ @interface FLTVideoPlayer : NSObject @property(nonatomic, readonly) bool isPlaying; @property(nonatomic) bool isLooping; @property(nonatomic, readonly) bool isInitialized; -- (instancetype)initWithURL:(NSURL*)url frameUpdater:(FLTFrameUpdater*)frameUpdater; +- (instancetype)initWithURL:(NSURL*)url + frameUpdater:(FLTFrameUpdater*)frameUpdater + httpHeaders:(NSDictionary*)headers; - (void)play; - (void)pause; - (void)setIsLooping:(bool)isLooping; @@ -62,7 +64,7 @@ - (void)updatePlayingState; @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]; + return [self initWithURL:[NSURL fileURLWithPath:path] frameUpdater:frameUpdater httpHeaders:nil]; } - (void)addObservers:(AVPlayerItem*)item { @@ -162,8 +164,15 @@ - (void)createVideoOutputAndDisplayLink:(FLTFrameUpdater*)frameUpdater { _displayLink.paused = YES; } -- (instancetype)initWithURL:(NSURL*)url frameUpdater:(FLTFrameUpdater*)frameUpdater { - AVPlayerItem* item = [AVPlayerItem playerItemWithURL:url]; +- (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]; } @@ -392,7 +401,7 @@ - (CVPixelBufferRef)copyPixelBuffer { } } -- (void)onTextureUnregistered { +- (void)onTextureUnregistered:(NSObject*)texture { dispatch_async(dispatch_get_main_queue(), ^{ [self dispose]; }); @@ -522,7 +531,8 @@ - (FLTTextureMessage*)create:(FLTCreateMessage*)input error:(FlutterError**)erro return [self onPlayerSetup:player frameUpdater:frameUpdater]; } else if (input.uri) { player = [[FLTVideoPlayer alloc] initWithURL:[NSURL URLWithString:input.uri] - frameUpdater:frameUpdater]; + frameUpdater:frameUpdater + httpHeaders:input.httpHeaders]; return [self onPlayerSetup:player frameUpdater:frameUpdater]; } else { *error = [FlutterError errorWithCode:@"video_player" message:@"not implemented" details:nil]; diff --git a/packages/video_player/video_player/ios/Classes/messages.h b/packages/video_player/video_player/ios/Classes/messages.h index 3c68b3dd24d4..e21e7860ba09 100644 --- a/packages/video_player/video_player/ios/Classes/messages.h +++ b/packages/video_player/video_player/ios/Classes/messages.h @@ -1,4 +1,8 @@ -// Autogenerated from Pigeon (v0.1.7), do not edit directly. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Autogenerated from Pigeon (v0.1.21), do not edit directly. // See also: https://pub.dev/packages/pigeon #import @protocol FlutterBinaryMessenger; @@ -24,6 +28,7 @@ NS_ASSUME_NONNULL_BEGIN @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 diff --git a/packages/video_player/video_player/ios/Classes/messages.m b/packages/video_player/video_player/ios/Classes/messages.m index e71f8b89254d..0936bbc7d995 100644 --- a/packages/video_player/video_player/ios/Classes/messages.m +++ b/packages/video_player/video_player/ios/Classes/messages.m @@ -1,4 +1,8 @@ -// Autogenerated from Pigeon (v0.1.7), do not edit directly. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Autogenerated from Pigeon (v0.1.21), do not edit directly. // See also: https://pub.dev/packages/pigeon #import "messages.h" #import @@ -7,17 +11,19 @@ #error File requires ARC to be enabled. #endif -static NSDictionary *wrapResult(NSDictionary *result, FlutterError *error) { +static NSDictionary *wrapResult(NSDictionary *result, FlutterError *error) { NSDictionary *errorDict = (NSDictionary *)[NSNull null]; if (error) { - errorDict = [NSDictionary - dictionaryWithObjectsAndKeys:(error.code ? error.code : [NSNull null]), @"code", - (error.message ? error.message : [NSNull null]), @"message", - (error.details ? error.details : [NSNull null]), @"details", - nil]; + errorDict = @{ + @"code" : (error.code ? error.code : [NSNull null]), + @"message" : (error.message ? error.message : [NSNull null]), + @"details" : (error.details ? error.details : [NSNull null]), + }; } - return [NSDictionary dictionaryWithObjectsAndKeys:(result ? result : [NSNull null]), @"result", - errorDict, @"error", nil]; + return @{ + @"result" : (result ? result : [NSNull null]), + @"error" : errorDict, + }; } @interface FLTTextureMessage () @@ -84,6 +90,10 @@ + (FLTCreateMessage *)fromMap:(NSDictionary *)dict { 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 { @@ -93,7 +103,9 @@ - (NSDictionary *)toMap { (self.packageName ? self.packageName : [NSNull null]), @"packageName", (self.formatHint ? self.formatHint : [NSNull null]), - @"formatHint", nil]; + @"formatHint", + (self.httpHeaders ? self.httpHeaders : [NSNull null]), + @"httpHeaders", nil]; } @end @@ -220,8 +232,8 @@ void FLTVideoPlayerApiSetup(id binaryMessenger, id binaryMessenger, id binaryMessenger, id binaryMessenger, id binaryMessenger, id binaryMessenger, id binaryMessenger, id binaryMessenger, id binaryMessenger, id binaryMessenger, id _parseCaptionsFromSubRipString(String file) { end: startAndEnd.end, text: text, ); - - if (newCaption.start != null && newCaption.end != null) { + if (newCaption.start != newCaption.end) { captions.add(newCaption); } } @@ -64,7 +63,7 @@ class _StartAndEnd { RegExp(_subRipTimeStamp + _subRipArrow + _subRipTimeStamp); if (!format.hasMatch(line)) { - return _StartAndEnd(null, null); + return _StartAndEnd(Duration.zero, Duration.zero); } final List times = line.split(_subRipArrow); @@ -84,7 +83,7 @@ class _StartAndEnd { // Duration(hours: 0, minutes: 1, seconds: 59, milliseconds: 084) Duration _parseSubRipTimestamp(String timestampString) { if (!RegExp(_subRipTimeStamp).hasMatch(timestampString)) { - return null; + return Duration.zero; } final List commaSections = timestampString.split(','); diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index bf98096af45d..d5bd7d2f222d 100644 --- a/packages/video_player/video_player/lib/video_player.dart +++ b/packages/video_player/video_player/lib/video_player.dart @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -28,11 +28,12 @@ class VideoPlayerValue { /// Constructs a video with the given values. Only [duration] is required. The /// rest will initialize with default values when unset. VideoPlayerValue({ - @required this.duration, - this.size, - this.position = const Duration(), - this.caption = const Caption(), + required this.duration, + this.size = Size.zero, + this.position = Duration.zero, + this.caption = Caption.none, this.buffered = const [], + this.isInitialized = false, this.isPlaying = false, this.isLooping = false, this.isBuffering = false, @@ -41,17 +42,20 @@ class VideoPlayerValue { this.errorDescription, }); - /// Returns an instance with a `null` [Duration]. - VideoPlayerValue.uninitialized() : this(duration: null); + /// Returns an instance for a video that hasn't been loaded. + VideoPlayerValue.uninitialized() + : this(duration: Duration.zero, isInitialized: false); - /// Returns an instance with a `null` [Duration] and the given - /// [errorDescription]. + /// Returns an instance with the given [errorDescription]. VideoPlayerValue.erroneous(String errorDescription) - : this(duration: null, errorDescription: errorDescription); + : this( + duration: Duration.zero, + isInitialized: false, + errorDescription: errorDescription); /// The total duration of the video. /// - /// Is null when [initialized] is false. + /// The duration is [Duration.zero] if the video hasn't been initialized. final Duration duration; /// The current playback position. @@ -60,7 +64,7 @@ class VideoPlayerValue { /// The [Caption] that should be displayed based on the current [position]. /// /// This field will never be null. If there is no caption for the current - /// [position], this will be an empty [Caption] object. + /// [position], this will be a [Caption.none] object. final Caption caption; /// The currently buffered ranges. @@ -83,25 +87,27 @@ class VideoPlayerValue { /// A description of the error if present. /// - /// If [hasError] is false this is [null]. - final String errorDescription; + /// If [hasError] is false this is `null`. + final String? errorDescription; /// The [size] of the currently loaded video. - /// - /// Is null when [initialized] is false. final Size size; /// Indicates whether or not the video has been loaded and is ready to play. - bool get initialized => duration != null; + final bool isInitialized; /// Indicates whether or not the video is in an error state. If this is true /// [errorDescription] should have information about the problem. bool get hasError => errorDescription != null; - /// Returns [size.width] / [size.height] when size is non-null, or `1.0.` when - /// size is null or the aspect ratio would be less than or equal to 0.0. + /// 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 (size == null || size.width == 0 || size.height == 0) { + if (!isInitialized || size.width == 0 || size.height == 0) { return 1.0; } final double aspectRatio = size.width / size.height; @@ -114,17 +120,18 @@ class VideoPlayerValue { /// Returns a new instance that has the same values as this current instance, /// except for any overrides passed in as arguments to [copyWidth]. VideoPlayerValue copyWith({ - Duration duration, - Size size, - Duration position, - Caption caption, - List buffered, - bool isPlaying, - bool isLooping, - bool isBuffering, - double volume, - double playbackSpeed, - String errorDescription, + Duration? duration, + Size? size, + Duration? position, + Caption? caption, + List? buffered, + bool? isInitialized, + bool? isPlaying, + bool? isLooping, + bool? isBuffering, + double? volume, + double? playbackSpeed, + String? errorDescription, }) { return VideoPlayerValue( duration: duration ?? this.duration, @@ -132,6 +139,7 @@ class VideoPlayerValue { position: position ?? this.position, caption: caption ?? this.caption, buffered: buffered ?? this.buffered, + isInitialized: isInitialized ?? this.isInitialized, isPlaying: isPlaying ?? this.isPlaying, isLooping: isLooping ?? this.isLooping, isBuffering: isBuffering ?? this.isBuffering, @@ -149,6 +157,7 @@ class VideoPlayerValue { 'position: $position, ' 'caption: $caption, ' 'buffered: [${buffered.join(', ')}], ' + 'isInitialized: $isInitialized, ' 'isPlaying: $isPlaying, ' 'isLooping: $isLooping, ' 'isBuffering: $isBuffering, ' @@ -178,7 +187,8 @@ class VideoPlayerController extends ValueNotifier { {this.package, this.closedCaptionFile, this.videoPlayerOptions}) : dataSourceType = DataSourceType.asset, formatHint = null, - super(VideoPlayerValue(duration: null)); + httpHeaders = const {}, + super(VideoPlayerValue(duration: Duration.zero)); /// Constructs a [VideoPlayerController] playing a video from obtained from /// the network. @@ -187,11 +197,17 @@ class VideoPlayerController extends ValueNotifier { /// null. /// **Android only**: The [formatHint] option allows the caller to override /// the video format detection code. - VideoPlayerController.network(this.dataSource, - {this.formatHint, this.closedCaptionFile, this.videoPlayerOptions}) - : dataSourceType = DataSourceType.network, + /// [httpHeaders] option allows to specify HTTP headers + /// for the request to the [dataSource]. + VideoPlayerController.network( + this.dataSource, { + this.formatHint, + this.closedCaptionFile, + this.videoPlayerOptions, + this.httpHeaders = const {}, + }) : dataSourceType = DataSourceType.network, package = null, - super(VideoPlayerValue(duration: null)); + super(VideoPlayerValue(duration: Duration.zero)); /// Constructs a [VideoPlayerController] playing a video from a file. /// @@ -203,41 +219,50 @@ class VideoPlayerController extends ValueNotifier { dataSourceType = DataSourceType.file, package = null, formatHint = null, - super(VideoPlayerValue(duration: null)); - - int _textureId; + httpHeaders = const {}, + 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; + /// HTTP headers used for the request to the [dataSource]. + /// Only for [VideoPlayerController.network]. + /// Always empty for other video types. + final Map httpHeaders; + /// **Android only**. Will override the platform's generic file format /// detection with whatever is set here. - final VideoFormat formatHint; + final VideoFormat? formatHint; /// Describes the type of data source this [VideoPlayerController] /// is constructed with. final DataSourceType dataSourceType; /// Provide additional configuration options (optional). Like setting the audio mode to mix - final VideoPlayerOptions videoPlayerOptions; + final VideoPlayerOptions? videoPlayerOptions; /// Only set for [asset] videos. The package that the asset was loaded from. - final String package; + final String? package; /// Optional field to specify a file containing the closed /// captioning. /// /// This future will be awaited and the file will be loaded when /// [initialize()] is called. - final Future closedCaptionFile; + final Future? closedCaptionFile; - ClosedCaptionFile _closedCaptionFile; - Timer _timer; + ClosedCaptionFile? _closedCaptionFile; + Timer? _timer; bool _isDisposed = false; - Completer _creatingCompleter; - StreamSubscription _eventSubscription; - _VideoAppLifeCycleObserver _lifeCycleObserver; + Completer? _creatingCompleter; + StreamSubscription? _eventSubscription; + late _VideoAppLifeCycleObserver _lifeCycleObserver; + + /// The id of a texture that hasn't been initialized. + @visibleForTesting + static const int kUninitializedTextureId = -1; + int _textureId = kUninitializedTextureId; /// This is just exposed for testing. It shouldn't be used by anyone depending /// on the plugin. @@ -250,7 +275,7 @@ class VideoPlayerController extends ValueNotifier { _lifeCycleObserver.initialize(); _creatingCompleter = Completer(); - DataSource dataSourceDescription; + late DataSource dataSourceDescription; switch (dataSourceType) { case DataSourceType.asset: dataSourceDescription = DataSource( @@ -264,6 +289,7 @@ class VideoPlayerController extends ValueNotifier { sourceType: DataSourceType.network, uri: dataSource, formatHint: formatHint, + httpHeaders: httpHeaders, ); break; case DataSourceType.file: @@ -276,11 +302,12 @@ class VideoPlayerController extends ValueNotifier { if (videoPlayerOptions?.mixWithOthers != null) { await _videoPlayerPlatform - .setMixWithOthers(videoPlayerOptions.mixWithOthers); + .setMixWithOthers(videoPlayerOptions!.mixWithOthers); } - _textureId = await _videoPlayerPlatform.create(dataSourceDescription); - _creatingCompleter.complete(null); + _textureId = (await _videoPlayerPlatform.create(dataSourceDescription)) ?? + kUninitializedTextureId; + _creatingCompleter!.complete(null); final Completer initializingCompleter = Completer(); void eventListener(VideoEvent event) { @@ -293,6 +320,7 @@ class VideoPlayerController extends ValueNotifier { value = value.copyWith( duration: event.duration, size: event.size, + isInitialized: event.duration != null, ); initializingCompleter.complete(null); _applyLooping(); @@ -325,8 +353,8 @@ class VideoPlayerController extends ValueNotifier { } void errorListener(Object obj) { - final PlatformException e = obj; - value = VideoPlayerValue.erroneous(e.message); + final PlatformException e = obj as PlatformException; + value = VideoPlayerValue.erroneous(e.message!); _timer?.cancel(); if (!initializingCompleter.isCompleted) { initializingCompleter.completeError(obj); @@ -342,7 +370,7 @@ class VideoPlayerController extends ValueNotifier { @override Future dispose() async { if (_creatingCompleter != null) { - await _creatingCompleter.future; + await _creatingCompleter!.future; if (!_isDisposed) { _isDisposed = true; _timer?.cancel(); @@ -379,26 +407,29 @@ class VideoPlayerController extends ValueNotifier { } Future _applyLooping() async { - if (!value.initialized || _isDisposed) { + if (!value.isInitialized || _isDisposed) { return; } await _videoPlayerPlatform.setLooping(_textureId, value.isLooping); } Future _applyPlayPause() async { - if (!value.initialized || _isDisposed) { + if (!value.isInitialized || _isDisposed) { return; } if (value.isPlaying) { await _videoPlayerPlatform.play(_textureId); + + // Cancel previous timer. + _timer?.cancel(); _timer = Timer.periodic( const Duration(milliseconds: 500), (Timer timer) async { if (_isDisposed) { return; } - final Duration newPosition = await position; - if (_isDisposed) { + final Duration? newPosition = await position; + if (newPosition == null) { return; } _updatePosition(newPosition); @@ -416,14 +447,14 @@ class VideoPlayerController extends ValueNotifier { } Future _applyVolume() async { - if (!value.initialized || _isDisposed) { + if (!value.isInitialized || _isDisposed) { return; } await _videoPlayerPlatform.setVolume(_textureId, value.volume); } Future _applyPlaybackSpeed() async { - if (!value.initialized || _isDisposed) { + if (!value.isInitialized || _isDisposed) { return; } @@ -439,7 +470,7 @@ class VideoPlayerController extends ValueNotifier { } /// The position in the current video. - Future get position async { + Future get position async { if (_isDisposed) { return null; } @@ -516,17 +547,17 @@ class VideoPlayerController extends ValueNotifier { /// [Caption]. Caption _getCaptionAt(Duration position) { if (_closedCaptionFile == null) { - return Caption(); + return Caption.none; } // TODO: This would be more efficient as a binary search. - for (final caption in _closedCaptionFile.captions) { + for (final caption in _closedCaptionFile!.captions) { if (caption.start <= position && caption.end >= position) { return caption; } } - return Caption(); + return Caption.none; } void _updatePosition(Duration position) { @@ -542,7 +573,7 @@ class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver { final VideoPlayerController _controller; void initialize() { - WidgetsBinding.instance.addObserver(this); + WidgetsBinding.instance!.addObserver(this); } @override @@ -562,7 +593,7 @@ class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver { } void dispose() { - WidgetsBinding.instance.removeObserver(this); + WidgetsBinding.instance!.removeObserver(this); } } @@ -591,8 +622,9 @@ class _VideoPlayerState extends State { }; } - VoidCallback _listener; - int _textureId; + late VoidCallback _listener; + + late int _textureId; @override void initState() { @@ -619,7 +651,7 @@ class _VideoPlayerState extends State { @override Widget build(BuildContext context) { - return _textureId == null + return _textureId == VideoPlayerController.kUninitializedTextureId ? Container() : _videoPlayerPlatform.buildView(_textureId); } @@ -643,7 +675,7 @@ class VideoProgressColors { /// [backgroundColor] defaults to gray at 50% opacity. This is the background /// color behind both [playedColor] and [bufferedColor] to denote the total /// size of the video compared to either of those values. - VideoProgressColors({ + const VideoProgressColors({ this.playedColor = const Color.fromRGBO(255, 0, 0, 0.7), this.bufferedColor = const Color.fromRGBO(50, 50, 200, 0.2), this.backgroundColor = const Color.fromRGBO(200, 200, 200, 0.5), @@ -667,8 +699,8 @@ class VideoProgressColors { class _VideoScrubber extends StatefulWidget { _VideoScrubber({ - @required this.child, - @required this.controller, + required this.child, + required this.controller, }); final Widget child; @@ -686,7 +718,7 @@ class _VideoScrubberState extends State<_VideoScrubber> { @override Widget build(BuildContext context) { void seekToRelativePosition(Offset globalPosition) { - final RenderBox box = context.findRenderObject(); + final RenderBox box = context.findRenderObject() as RenderBox; final Offset tapPos = box.globalToLocal(globalPosition); final double relative = tapPos.dx / box.size.width; final Duration position = controller.value.duration * relative; @@ -697,7 +729,7 @@ class _VideoScrubberState extends State<_VideoScrubber> { behavior: HitTestBehavior.opaque, child: widget.child, onHorizontalDragStart: (DragStartDetails details) { - if (!controller.value.initialized) { + if (!controller.value.isInitialized) { return; } _controllerWasPlaying = controller.value.isPlaying; @@ -706,7 +738,7 @@ class _VideoScrubberState extends State<_VideoScrubber> { } }, onHorizontalDragUpdate: (DragUpdateDetails details) { - if (!controller.value.initialized) { + if (!controller.value.isInitialized) { return; } seekToRelativePosition(details.globalPosition); @@ -717,7 +749,7 @@ class _VideoScrubberState extends State<_VideoScrubber> { } }, onTapDown: (TapDownDetails details) { - if (!controller.value.initialized) { + if (!controller.value.isInitialized) { return; } seekToRelativePosition(details.globalPosition); @@ -742,10 +774,10 @@ class VideoProgressIndicator extends StatefulWidget { /// to `top: 5.0`. VideoProgressIndicator( this.controller, { - VideoProgressColors colors, - this.allowScrubbing, + this.colors = const VideoProgressColors(), + required this.allowScrubbing, this.padding = const EdgeInsets.only(top: 5.0), - }) : colors = colors ?? VideoProgressColors(); + }); /// The [VideoPlayerController] that actually associates a video with this /// widget. @@ -782,7 +814,7 @@ class _VideoProgressIndicatorState extends State { }; } - VoidCallback listener; + late VoidCallback listener; VideoPlayerController get controller => widget.controller; @@ -803,7 +835,7 @@ class _VideoProgressIndicatorState extends State { @override Widget build(BuildContext context) { Widget progressIndicator; - if (controller.value.initialized) { + if (controller.value.isInitialized) { final int duration = controller.value.duration.inMilliseconds; final int position = controller.value.position.inMilliseconds; @@ -875,17 +907,17 @@ class ClosedCaption extends StatelessWidget { /// [VideoPlayerValue.caption]. /// /// If [text] is null, nothing will be displayed. - const ClosedCaption({Key key, this.text, this.textStyle}) : super(key: key); + const ClosedCaption({Key? key, this.text, this.textStyle}) : super(key: key); /// The text that will be shown in the closed caption, or null if no caption /// should be shown. - final String text; + final String? text; /// Specifies how the text in the closed caption should look. /// /// If null, defaults to [DefaultTextStyle.of(context).style] with size 36 /// font colored white. - final TextStyle textStyle; + final TextStyle? textStyle; @override Widget build(BuildContext context) { @@ -910,7 +942,7 @@ class ClosedCaption extends StatelessWidget { ), child: Padding( padding: EdgeInsets.symmetric(horizontal: 2.0), - child: Text(text, style: effectiveTextStyle), + child: Text(text!, style: effectiveTextStyle), ), ), ), diff --git a/packages/video_player/video_player/pigeons/messages.dart b/packages/video_player/video_player/pigeons/messages.dart index 427aea279071..e893aaa6830d 100644 --- a/packages/video_player/video_player/pigeons/messages.dart +++ b/packages/video_player/video_player/pigeons/messages.dart @@ -1,3 +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. + +// @dart = 2.9 + import 'package:pigeon/pigeon_lib.dart'; class TextureMessage { @@ -29,6 +35,7 @@ class CreateMessage { String uri; String packageName; String formatHint; + Map httpHeaders; } class MixWithOthersMessage { @@ -52,6 +59,7 @@ abstract class VideoPlayerApi { void configurePigeon(PigeonOptions opts) { opts.dartOut = '../video_player_platform_interface/lib/messages.dart'; + opts.dartTestOut = '../video_player_platform_interface/lib/test.dart'; opts.objcHeaderOut = 'ios/Classes/messages.h'; opts.objcSourceOut = 'ios/Classes/messages.m'; opts.objcOptions.prefix = 'FLT'; diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 973c0bc82589..ed78213cbc2b 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -1,11 +1,13 @@ name: video_player description: Flutter plugin for displaying inline video with other Flutter widgets on Android, iOS, and web. -# 0.10.y+z is compatible with 1.0.0, if you land a breaking change bump -# the version to 2.0.0. -# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.11.0 -homepage: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player +repository: https://github.com/flutter/plugins/tree/master/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.1.6 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: @@ -19,25 +21,20 @@ flutter: default_package: video_player_web dependencies: - meta: ^1.0.5 - video_player_platform_interface: ^2.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. - # TODO(amirh): Revisit this (either update this part in the design or the pub tool). - # https://github.com/flutter/flutter/issues/46264 - video_player_web: '>=0.1.4 <2.0.0' - flutter: sdk: flutter - -dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 - pigeon: 0.1.7 + meta: ^1.3.0 + video_player_platform_interface: ^4.1.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 + video_player_web: ^2.0.0 -environment: - sdk: ">=2.8.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" +dev_dependencies: + 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 new file mode 100644 index 000000000000..369a3b362557 --- /dev/null +++ b/packages/video_player/video_player/test/closed_caption_file_test.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:flutter_test/flutter_test.dart'; +import 'package:video_player/src/closed_caption_file.dart'; + +void main() { + group('ClosedCaptionFile', () { + test('toString()', () { + final Caption caption = const Caption( + number: 1, + start: Duration(seconds: 1), + end: Duration(seconds: 2), + text: 'caption', + ); + + expect( + caption.toString(), + 'Caption(' + 'number: 1, ' + 'start: 0:00:01.000000, ' + 'end: 0:00:02.000000, ' + '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 cf25ff73e438..5808e0b9d2e3 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 @@ -1,4 +1,4 @@ -// Copyright 2020 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -108,6 +108,6 @@ This one is valid 3 00:01:54,724 --> 00:01:6,760 -This one should be ignored because the +This one should be ignored because the ned time is missing a digit. '''; diff --git a/packages/video_player/video_player/test/video_player_initialization_test.dart b/packages/video_player/video_player/test/video_player_initialization_test.dart index 1a09ed9f718c..13bfd7be7889 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 @@ -1,8 +1,7 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All 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:video_player/video_player.dart'; @@ -12,7 +11,7 @@ void main() { // This test needs to run first and therefore needs to be the only test // in this file. test('plugin initialized', () async { - WidgetsFlutterBinding.ensureInitialized(); + TestWidgetsFlutterBinding.ensureInitialized(); FakeVideoPlayerPlatform fakeVideoPlayerPlatform = FakeVideoPlayerPlatform(); final VideoPlayerController controller = VideoPlayerController.network( 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 35cab6204965..e17dac7897a6 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -12,11 +12,12 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:video_player/video_player.dart'; import 'package:video_player_platform_interface/messages.dart'; +import 'package:video_player_platform_interface/test.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; class FakeController extends ValueNotifier implements VideoPlayerController { - FakeController() : super(VideoPlayerValue(duration: null)); + FakeController() : super(VideoPlayerValue(duration: Duration.zero)); @override Future dispose() async { @@ -24,16 +25,19 @@ class FakeController extends ValueNotifier } @override - int textureId; + int textureId = VideoPlayerController.kUninitializedTextureId; @override String get dataSource => ''; + @override + Map get httpHeaders => {}; + @override DataSourceType get dataSourceType => DataSourceType.file; @override - String get package => null; + String get package => ''; @override Future get position async => value.position; @@ -60,13 +64,13 @@ class FakeController extends ValueNotifier Future setLooping(bool looping) async {} @override - VideoFormat get formatHint => null; + VideoFormat? get formatHint => null; @override Future get closedCaptionFile => _loadClosedCaption(); @override - VideoPlayerOptions get videoPlayerOptions => null; + VideoPlayerOptions? get videoPlayerOptions => null; } Future _loadClosedCaption() async => @@ -78,11 +82,13 @@ class _FakeClosedCaptionFile extends ClosedCaptionFile { return [ Caption( text: 'one', + number: 0, start: Duration(milliseconds: 100), end: Duration(milliseconds: 200), ), Caption( text: 'two', + number: 1, start: Duration(milliseconds: 300), end: Duration(milliseconds: 400), ), @@ -99,6 +105,7 @@ void main() { controller.textureId = 123; controller.value = controller.value.copyWith( duration: const Duration(milliseconds: 100), + isInitialized: true, ); await tester.pump(); @@ -131,8 +138,8 @@ void main() { await tester.pumpWidget(MaterialApp(home: ClosedCaption(text: text))); final Text textWidget = tester.widget(find.text(text)); - expect(textWidget.style.fontSize, 36.0); - expect(textWidget.style.color, Colors.white); + expect(textWidget.style!.fontSize, 36.0); + expect(textWidget.style!.color, Colors.white); }); testWidgets('uses given text and style', (WidgetTester tester) async { @@ -147,7 +154,7 @@ void main() { expect(find.text(text), findsOneWidget); final Text textWidget = tester.widget(find.text(text)); - expect(textWidget.style.fontSize, textStyle.fontSize); + expect(textWidget.style!.fontSize, textStyle.fontSize); }); testWidgets('handles null text', (WidgetTester tester) async { @@ -171,7 +178,7 @@ void main() { }); group('VideoPlayerController', () { - FakeVideoPlayerPlatform fakeVideoPlayerPlatform; + late FakeVideoPlayerPlatform fakeVideoPlayerPlatform; setUp(() { fakeVideoPlayerPlatform = FakeVideoPlayerPlatform(); @@ -196,22 +203,60 @@ void main() { ); await controller.initialize(); - expect(fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri, - 'https://127.0.0.1'); expect( - fakeVideoPlayerPlatform.dataSourceDescriptions[0].formatHint, null); + fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri, + 'https://127.0.0.1', + ); + expect( + fakeVideoPlayerPlatform.dataSourceDescriptions[0].formatHint, + null, + ); + expect( + fakeVideoPlayerPlatform.dataSourceDescriptions[0].httpHeaders, + {}, + ); }); test('network with hint', () async { final VideoPlayerController controller = VideoPlayerController.network( - 'https://127.0.0.1', - formatHint: VideoFormat.dash); + 'https://127.0.0.1', + formatHint: VideoFormat.dash, + ); await controller.initialize(); - expect(fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri, - 'https://127.0.0.1'); - expect(fakeVideoPlayerPlatform.dataSourceDescriptions[0].formatHint, - 'dash'); + expect( + fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri, + 'https://127.0.0.1', + ); + expect( + fakeVideoPlayerPlatform.dataSourceDescriptions[0].formatHint, + 'dash', + ); + expect( + fakeVideoPlayerPlatform.dataSourceDescriptions[0].httpHeaders, + {}, + ); + }); + + test('network with some headers', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + httpHeaders: {'Authorization': 'Bearer token'}, + ); + await controller.initialize(); + + expect( + fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri, + 'https://127.0.0.1', + ); + expect( + fakeVideoPlayerPlatform.dataSourceDescriptions[0].formatHint, + null, + ); + expect( + fakeVideoPlayerPlatform.dataSourceDescriptions[0].httpHeaders, + {'Authorization': 'Bearer token'}, + ); }); test('init errors', () async { @@ -219,7 +264,7 @@ void main() { 'http://testing.com/invalid_url', ); try { - dynamic error; + late dynamic error; fakeVideoPlayerPlatform.forceInitError = true; await controller.initialize().catchError((dynamic e) => error = e); final PlatformException platformEx = error; @@ -243,13 +288,14 @@ void main() { final VideoPlayerController controller = VideoPlayerController.network( 'https://127.0.0.1', ); - expect(controller.textureId, isNull); + expect( + controller.textureId, VideoPlayerController.kUninitializedTextureId); expect(await controller.position, const Duration(seconds: 0)); await controller.initialize(); await controller.dispose(); - expect(controller.textureId, isNotNull); + expect(controller.textureId, 0); expect(await controller.position, isNull); }); @@ -388,19 +434,19 @@ void main() { await controller.initialize(); expect(controller.value.position, const Duration()); - expect(controller.value.caption.text, isNull); + expect(controller.value.caption.text, ''); await controller.seekTo(const Duration(milliseconds: 100)); expect(controller.value.caption.text, 'one'); await controller.seekTo(const Duration(milliseconds: 250)); - expect(controller.value.caption.text, isNull); + expect(controller.value.caption.text, ''); await controller.seekTo(const Duration(milliseconds: 300)); expect(controller.value.caption.text, 'two'); await controller.seekTo(const Duration(milliseconds: 500)); - expect(controller.value.caption.text, isNull); + expect(controller.value.caption.text, ''); await controller.seekTo(const Duration(milliseconds: 300)); expect(controller.value.caption.text, 'two'); @@ -417,8 +463,7 @@ void main() { await controller.play(); expect(controller.value.isPlaying, isTrue); final FakeVideoEventStream fakeVideoEventStream = - fakeVideoPlayerPlatform.streams[controller.textureId]; - assert(fakeVideoEventStream != null); + fakeVideoPlayerPlatform.streams[controller.textureId]!; fakeVideoEventStream.eventsChannel .sendEvent({'event': 'completed'}); @@ -436,8 +481,7 @@ void main() { expect(controller.value.isBuffering, false); expect(controller.value.buffered, isEmpty); final FakeVideoEventStream fakeVideoEventStream = - fakeVideoPlayerPlatform.streams[controller.textureId]; - assert(fakeVideoEventStream != null); + fakeVideoPlayerPlatform.streams[controller.textureId]!; fakeVideoEventStream.eventsChannel .sendEvent({'event': 'bufferingStart'}); @@ -494,9 +538,9 @@ void main() { test('uninitialized()', () { final VideoPlayerValue uninitialized = VideoPlayerValue.uninitialized(); - expect(uninitialized.duration, isNull); - expect(uninitialized.position, equals(const Duration(seconds: 0))); - expect(uninitialized.caption, equals(const Caption())); + expect(uninitialized.duration, equals(Duration.zero)); + expect(uninitialized.position, equals(Duration.zero)); + expect(uninitialized.caption, equals(Caption.none)); expect(uninitialized.buffered, isEmpty); expect(uninitialized.isPlaying, isFalse); expect(uninitialized.isLooping, isFalse); @@ -504,9 +548,8 @@ void main() { expect(uninitialized.volume, 1.0); expect(uninitialized.playbackSpeed, 1.0); expect(uninitialized.errorDescription, isNull); - expect(uninitialized.size, isNull); - expect(uninitialized.size, isNull); - expect(uninitialized.initialized, isFalse); + expect(uninitialized.size, equals(Size.zero)); + expect(uninitialized.isInitialized, isFalse); expect(uninitialized.hasError, isFalse); expect(uninitialized.aspectRatio, 1.0); }); @@ -515,9 +558,9 @@ void main() { const String errorMessage = 'foo'; final VideoPlayerValue error = VideoPlayerValue.erroneous(errorMessage); - expect(error.duration, isNull); - expect(error.position, equals(const Duration(seconds: 0))); - expect(error.caption, equals(const Caption())); + expect(error.duration, equals(Duration.zero)); + expect(error.position, equals(Duration.zero)); + expect(error.caption, equals(Caption.none)); expect(error.buffered, isEmpty); expect(error.isPlaying, isFalse); expect(error.isLooping, isFalse); @@ -525,9 +568,8 @@ void main() { expect(error.volume, 1.0); expect(error.playbackSpeed, 1.0); expect(error.errorDescription, errorMessage); - expect(error.size, isNull); - expect(error.size, isNull); - expect(error.initialized, isFalse); + expect(error.size, equals(Size.zero)); + expect(error.isInitialized, isFalse); expect(error.hasError, isTrue); expect(error.aspectRatio, 1.0); }); @@ -536,10 +578,12 @@ void main() { const Duration duration = Duration(seconds: 5); const Size size = Size(400, 300); const Duration position = Duration(seconds: 1); - const Caption caption = Caption(text: 'foo'); + const Caption caption = Caption( + text: 'foo', number: 0, start: Duration.zero, end: Duration.zero); final List buffered = [ DurationRange(const Duration(seconds: 0), const Duration(seconds: 4)) ]; + const bool isInitialized = true; const bool isPlaying = true; const bool isLooping = true; const bool isBuffering = true; @@ -552,6 +596,7 @@ void main() { position: position, caption: caption, buffered: buffered, + isInitialized: isInitialized, isPlaying: isPlaying, isLooping: isLooping, isBuffering: isBuffering, @@ -564,8 +609,9 @@ void main() { 'VideoPlayerValue(duration: 0:00:05.000000, ' 'size: Size(400.0, 300.0), ' 'position: 0:00:01.000000, ' - 'caption: Instance of \'Caption\', ' + 'caption: Caption(number: 0, start: 0:00:00.000000, end: 0:00:00.000000, text: foo), ' 'buffered: [DurationRange(start: 0:00:00.000000, end: 0:00:04.000000)], ' + 'isInitialized: true, ' 'isPlaying: true, ' 'isLooping: true, ' 'isBuffering: true, ' @@ -584,15 +630,16 @@ void main() { group('aspectRatio', () { test('640x480 -> 4:3', () { final value = VideoPlayerValue( + isInitialized: true, size: Size(640, 480), duration: Duration(seconds: 1), ); expect(value.aspectRatio, 4 / 3); }); - test('null size -> 1.0', () { + test('no size -> 1.0', () { final value = VideoPlayerValue( - size: null, + isInitialized: true, duration: Duration(seconds: 1), ); expect(value.aspectRatio, 1.0); @@ -600,6 +647,7 @@ void main() { test('height = 0 -> 1.0', () { final value = VideoPlayerValue( + isInitialized: true, size: Size(640, 0), duration: Duration(seconds: 1), ); @@ -608,6 +656,7 @@ void main() { test('width = 0 -> 1.0', () { final value = VideoPlayerValue( + isInitialized: true, size: Size(0, 480), duration: Duration(seconds: 1), ); @@ -616,6 +665,7 @@ void main() { test('negative aspect ratio -> 1.0', () { final value = VideoPlayerValue( + isInitialized: true, size: Size(640, -480), duration: Duration(seconds: 1), ); @@ -644,7 +694,7 @@ void main() { File(''), videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true)); await controller.initialize(); - expect(controller.videoPlayerOptions.mixWithOthers, true); + expect(controller.videoPlayerOptions!.mixWithOthers, true); }); } @@ -704,7 +754,7 @@ class FakeVideoPlayerPlatform extends TestHostVideoPlayerApi { @override void seekTo(PositionMessage arg) { calls.add('seekTo'); - _positions[arg.textureId] = Duration(milliseconds: arg.position); + _positions[arg.textureId!] = Duration(milliseconds: arg.position!); } @override @@ -740,7 +790,7 @@ class FakeVideoEventStream { int height; Duration duration; bool initWithError; - FakeEventsChannel eventsChannel; + late FakeEventsChannel eventsChannel; void onListen() { if (!initWithError) { @@ -762,7 +812,7 @@ class FakeEventsChannel { eventsMethodChannel.setMockMethodCallHandler(onMethodCall); } - MethodChannel eventsMethodChannel; + late MethodChannel eventsMethodChannel; VoidCallback onListen; Future onMethodCall(MethodCall call) { @@ -778,7 +828,7 @@ class FakeEventsChannel { _sendMessage(const StandardMethodCodec().encodeSuccessEnvelope(event)); } - void sendError(String code, [String message, dynamic details]) { + void sendError(String code, [String? message, dynamic details]) { _sendMessage(const StandardMethodCodec().encodeErrorEnvelope( code: code, message: message, @@ -787,11 +837,7 @@ class FakeEventsChannel { } void _sendMessage(ByteData data) { - // TODO(jackson): This has been deprecated and should be replaced - // with `ServicesBinding.instance.defaultBinaryMessenger` when it's - // available on all the versions of Flutter that we test. - // ignore: deprecated_member_use - defaultBinaryMessenger.handlePlatformMessage( - eventsMethodChannel.name, data, (ByteData data) {}); + ServicesBinding.instance!.defaultBinaryMessenger.handlePlatformMessage( + eventsMethodChannel.name, data, (ByteData? data) {}); } } diff --git a/packages/video_player/video_player_platform_interface/AUTHORS b/packages/video_player/video_player_platform_interface/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/video_player/video_player_platform_interface/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/video_player/video_player_platform_interface/CHANGELOG.md b/packages/video_player/video_player_platform_interface/CHANGELOG.md index 8af22f783675..24631513f800 100644 --- a/packages/video_player/video_player_platform_interface/CHANGELOG.md +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -1,3 +1,24 @@ +## 4.1.0 + +* Add `httpHeaders` to `DataSource` + +## 4.0.0 + +* **Breaking Changes**: + * Migrate to null-safety + * Update to latest Pigeon. This includes a breaking change to how the test logic is exposed. +* Add note about the `mixWithOthers` option being ignored on the web. +* Make DataSource's `uri` parameter nullable. +* `messages.dart` sets Dart `2.12`. + +## 3.0.0 + +* Version 3 only was published as nullsafety "previews". + +## 2.2.1 + +* Update Flutter SDK constraint. + ## 2.2.0 * Added option to set the video playback speed on the video controller. diff --git a/packages/video_player/video_player_platform_interface/LICENSE b/packages/video_player/video_player_platform_interface/LICENSE index a6d6c0749818..c6823b81eb84 100644 --- a/packages/video_player/video_player_platform_interface/LICENSE +++ b/packages/video_player/video_player_platform_interface/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017 The Chromium Authors. All rights reserved. +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: diff --git a/packages/video_player/video_player_platform_interface/lib/messages.dart b/packages/video_player/video_player_platform_interface/lib/messages.dart index bfe65f1fd2ea..0ddbfaeaf247 100644 --- a/packages/video_player/video_player_platform_interface/lib/messages.dart +++ b/packages/video_player/video_player_platform_interface/lib/messages.dart @@ -1,554 +1,425 @@ -// Autogenerated from Pigeon (v0.1.7), do not edit directly. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// Autogenerated from Pigeon (v0.1.21), do not edit directly. // See also: https://pub.dev/packages/pigeon // ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import -// @dart = 2.8 +// @dart = 2.12 import 'dart:async'; -import 'package:flutter/services.dart'; import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; +import 'package:flutter/services.dart'; + class TextureMessage { - int textureId; - // ignore: unused_element - Map _toMap() { - final Map pigeonMap = {}; + int? textureId; + + Object encode() { + final Map pigeonMap = {}; pigeonMap['textureId'] = textureId; return pigeonMap; } - // ignore: unused_element - static TextureMessage _fromMap(Map pigeonMap) { - if (pigeonMap == null) { - return null; - } - final TextureMessage result = TextureMessage(); - result.textureId = pigeonMap['textureId']; - return result; + static TextureMessage decode(Object message) { + final Map pigeonMap = message as Map; + return TextureMessage()..textureId = pigeonMap['textureId'] as int?; } } class CreateMessage { - String asset; - String uri; - String packageName; - String formatHint; - // ignore: unused_element - Map _toMap() { - final Map pigeonMap = {}; + String? asset; + String? uri; + String? packageName; + String? formatHint; + Map? httpHeaders; + + Object encode() { + final Map pigeonMap = {}; pigeonMap['asset'] = asset; pigeonMap['uri'] = uri; pigeonMap['packageName'] = packageName; pigeonMap['formatHint'] = formatHint; + pigeonMap['httpHeaders'] = httpHeaders; return pigeonMap; } - // ignore: unused_element - static CreateMessage _fromMap(Map pigeonMap) { - if (pigeonMap == null) { - return null; - } - final CreateMessage result = CreateMessage(); - result.asset = pigeonMap['asset']; - result.uri = pigeonMap['uri']; - result.packageName = pigeonMap['packageName']; - result.formatHint = pigeonMap['formatHint']; - return result; + static CreateMessage decode(Object message) { + final Map pigeonMap = message as Map; + return CreateMessage() + ..asset = pigeonMap['asset'] as String? + ..uri = pigeonMap['uri'] as String? + ..packageName = pigeonMap['packageName'] as String? + ..formatHint = pigeonMap['formatHint'] as String? + ..httpHeaders = pigeonMap['httpHeaders'] as Map?; } } class LoopingMessage { - int textureId; - bool isLooping; - // ignore: unused_element - Map _toMap() { - final Map pigeonMap = {}; + int? textureId; + bool? isLooping; + + Object encode() { + final Map pigeonMap = {}; pigeonMap['textureId'] = textureId; pigeonMap['isLooping'] = isLooping; return pigeonMap; } - // ignore: unused_element - static LoopingMessage _fromMap(Map pigeonMap) { - if (pigeonMap == null) { - return null; - } - final LoopingMessage result = LoopingMessage(); - result.textureId = pigeonMap['textureId']; - result.isLooping = pigeonMap['isLooping']; - return result; + static LoopingMessage decode(Object message) { + final Map pigeonMap = message as Map; + return LoopingMessage() + ..textureId = pigeonMap['textureId'] as int? + ..isLooping = pigeonMap['isLooping'] as bool?; } } class VolumeMessage { - int textureId; - double volume; - // ignore: unused_element - Map _toMap() { - final Map pigeonMap = {}; + int? textureId; + double? volume; + + Object encode() { + final Map pigeonMap = {}; pigeonMap['textureId'] = textureId; pigeonMap['volume'] = volume; return pigeonMap; } - // ignore: unused_element - static VolumeMessage _fromMap(Map pigeonMap) { - if (pigeonMap == null) { - return null; - } - final VolumeMessage result = VolumeMessage(); - result.textureId = pigeonMap['textureId']; - result.volume = pigeonMap['volume']; - return result; + static VolumeMessage decode(Object message) { + final Map pigeonMap = message as Map; + return VolumeMessage() + ..textureId = pigeonMap['textureId'] as int? + ..volume = pigeonMap['volume'] as double?; } } class PlaybackSpeedMessage { - int textureId; - double speed; - // ignore: unused_element - Map _toMap() { - final Map pigeonMap = {}; + int? textureId; + double? speed; + + Object encode() { + final Map pigeonMap = {}; pigeonMap['textureId'] = textureId; pigeonMap['speed'] = speed; return pigeonMap; } - // ignore: unused_element - static PlaybackSpeedMessage _fromMap(Map pigeonMap) { - if (pigeonMap == null) { - return null; - } - final PlaybackSpeedMessage result = PlaybackSpeedMessage(); - result.textureId = pigeonMap['textureId']; - result.speed = pigeonMap['speed']; - return result; + static PlaybackSpeedMessage decode(Object message) { + final Map pigeonMap = message as Map; + return PlaybackSpeedMessage() + ..textureId = pigeonMap['textureId'] as int? + ..speed = pigeonMap['speed'] as double?; } } class PositionMessage { - int textureId; - int position; - // ignore: unused_element - Map _toMap() { - final Map pigeonMap = {}; + int? textureId; + int? position; + + Object encode() { + final Map pigeonMap = {}; pigeonMap['textureId'] = textureId; pigeonMap['position'] = position; return pigeonMap; } - // ignore: unused_element - static PositionMessage _fromMap(Map pigeonMap) { - if (pigeonMap == null) { - return null; - } - final PositionMessage result = PositionMessage(); - result.textureId = pigeonMap['textureId']; - result.position = pigeonMap['position']; - return result; + static PositionMessage decode(Object message) { + final Map pigeonMap = message as Map; + return PositionMessage() + ..textureId = pigeonMap['textureId'] as int? + ..position = pigeonMap['position'] as int?; } } class MixWithOthersMessage { - bool mixWithOthers; - // ignore: unused_element - Map _toMap() { - final Map pigeonMap = {}; + bool? mixWithOthers; + + Object encode() { + final Map pigeonMap = {}; pigeonMap['mixWithOthers'] = mixWithOthers; return pigeonMap; } - // ignore: unused_element - static MixWithOthersMessage _fromMap(Map pigeonMap) { - if (pigeonMap == null) { - return null; - } - final MixWithOthersMessage result = MixWithOthersMessage(); - result.mixWithOthers = pigeonMap['mixWithOthers']; - return result; + static MixWithOthersMessage decode(Object message) { + final Map pigeonMap = message as Map; + return MixWithOthersMessage() + ..mixWithOthers = pigeonMap['mixWithOthers'] as bool?; } } class VideoPlayerApi { Future initialize() async { - const BasicMessageChannel channel = BasicMessageChannel( + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.initialize', StandardMessageCodec()); - - final Map replyMap = await channel.send(null); + final Map? replyMap = + await channel.send(null) as Map?; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); } else if (replyMap['error'] != null) { - final Map error = replyMap['error']; + final Map error = + replyMap['error'] as Map; throw PlatformException( - code: error['code'], - message: error['message'], - details: error['details']); + code: error['code'] as String, + message: error['message'] as String?, + details: error['details'], + ); } else { // noop } } Future create(CreateMessage arg) async { - final Map requestMap = arg._toMap(); - const BasicMessageChannel channel = BasicMessageChannel( + final Object encoded = arg.encode(); + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.create', StandardMessageCodec()); - - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = + await channel.send(encoded) as Map?; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); } else if (replyMap['error'] != null) { - final Map error = replyMap['error']; + final Map error = + replyMap['error'] as Map; throw PlatformException( - code: error['code'], - message: error['message'], - details: error['details']); + code: error['code'] as String, + message: error['message'] as String?, + details: error['details'], + ); } else { - return TextureMessage._fromMap(replyMap['result']); + return TextureMessage.decode(replyMap['result']!); } } Future dispose(TextureMessage arg) async { - final Map requestMap = arg._toMap(); - const BasicMessageChannel channel = BasicMessageChannel( + final Object encoded = arg.encode(); + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.dispose', StandardMessageCodec()); - - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = + await channel.send(encoded) as Map?; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); } else if (replyMap['error'] != null) { - final Map error = replyMap['error']; + final Map error = + replyMap['error'] as Map; throw PlatformException( - code: error['code'], - message: error['message'], - details: error['details']); + code: error['code'] as String, + message: error['message'] as String?, + details: error['details'], + ); } else { // noop } } Future setLooping(LoopingMessage arg) async { - final Map requestMap = arg._toMap(); - const BasicMessageChannel channel = BasicMessageChannel( + final Object encoded = arg.encode(); + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.setLooping', StandardMessageCodec()); - - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = + await channel.send(encoded) as Map?; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); } else if (replyMap['error'] != null) { - final Map error = replyMap['error']; + final Map error = + replyMap['error'] as Map; throw PlatformException( - code: error['code'], - message: error['message'], - details: error['details']); + code: error['code'] as String, + message: error['message'] as String?, + details: error['details'], + ); } else { // noop } } Future setVolume(VolumeMessage arg) async { - final Map requestMap = arg._toMap(); - const BasicMessageChannel channel = BasicMessageChannel( + final Object encoded = arg.encode(); + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.setVolume', StandardMessageCodec()); - - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = + await channel.send(encoded) as Map?; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); } else if (replyMap['error'] != null) { - final Map error = replyMap['error']; + final Map error = + replyMap['error'] as Map; throw PlatformException( - code: error['code'], - message: error['message'], - details: error['details']); + code: error['code'] as String, + message: error['message'] as String?, + details: error['details'], + ); } else { // noop } } Future setPlaybackSpeed(PlaybackSpeedMessage arg) async { - final Map requestMap = arg._toMap(); - const BasicMessageChannel channel = BasicMessageChannel( + final Object encoded = arg.encode(); + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed', StandardMessageCodec()); - - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = + await channel.send(encoded) as Map?; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); } else if (replyMap['error'] != null) { - final Map error = replyMap['error']; + final Map error = + replyMap['error'] as Map; throw PlatformException( - code: error['code'], - message: error['message'], - details: error['details']); + code: error['code'] as String, + message: error['message'] as String?, + details: error['details'], + ); } else { // noop } } Future play(TextureMessage arg) async { - final Map requestMap = arg._toMap(); - const BasicMessageChannel channel = BasicMessageChannel( + final Object encoded = arg.encode(); + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.play', StandardMessageCodec()); - - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = + await channel.send(encoded) as Map?; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); } else if (replyMap['error'] != null) { - final Map error = replyMap['error']; + final Map error = + replyMap['error'] as Map; throw PlatformException( - code: error['code'], - message: error['message'], - details: error['details']); + code: error['code'] as String, + message: error['message'] as String?, + details: error['details'], + ); } else { // noop } } Future position(TextureMessage arg) async { - final Map requestMap = arg._toMap(); - const BasicMessageChannel channel = BasicMessageChannel( + final Object encoded = arg.encode(); + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.position', StandardMessageCodec()); - - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = + await channel.send(encoded) as Map?; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); } else if (replyMap['error'] != null) { - final Map error = replyMap['error']; + final Map error = + replyMap['error'] as Map; throw PlatformException( - code: error['code'], - message: error['message'], - details: error['details']); + code: error['code'] as String, + message: error['message'] as String?, + details: error['details'], + ); } else { - return PositionMessage._fromMap(replyMap['result']); + return PositionMessage.decode(replyMap['result']!); } } Future seekTo(PositionMessage arg) async { - final Map requestMap = arg._toMap(); - const BasicMessageChannel channel = BasicMessageChannel( + final Object encoded = arg.encode(); + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.seekTo', StandardMessageCodec()); - - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = + await channel.send(encoded) as Map?; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); } else if (replyMap['error'] != null) { - final Map error = replyMap['error']; + final Map error = + replyMap['error'] as Map; throw PlatformException( - code: error['code'], - message: error['message'], - details: error['details']); + code: error['code'] as String, + message: error['message'] as String?, + details: error['details'], + ); } else { // noop } } Future pause(TextureMessage arg) async { - final Map requestMap = arg._toMap(); - const BasicMessageChannel channel = BasicMessageChannel( + final Object encoded = arg.encode(); + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.pause', StandardMessageCodec()); - - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = + await channel.send(encoded) as Map?; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); } else if (replyMap['error'] != null) { - final Map error = replyMap['error']; + final Map error = + replyMap['error'] as Map; throw PlatformException( - code: error['code'], - message: error['message'], - details: error['details']); + code: error['code'] as String, + message: error['message'] as String?, + details: error['details'], + ); } else { // noop } } Future setMixWithOthers(MixWithOthersMessage arg) async { - final Map requestMap = arg._toMap(); - const BasicMessageChannel channel = BasicMessageChannel( + final Object encoded = arg.encode(); + const BasicMessageChannel channel = BasicMessageChannel( 'dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers', StandardMessageCodec()); - - final Map replyMap = await channel.send(requestMap); + final Map? replyMap = + await channel.send(encoded) as Map?; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); } else if (replyMap['error'] != null) { - final Map error = replyMap['error']; + final Map error = + replyMap['error'] as Map; throw PlatformException( - code: error['code'], - message: error['message'], - details: error['details']); + code: error['code'] as String, + message: error['message'] as String?, + details: error['details'], + ); } else { // noop } } } - -abstract class TestHostVideoPlayerApi { - void initialize(); - TextureMessage create(CreateMessage arg); - void dispose(TextureMessage arg); - void setLooping(LoopingMessage arg); - void setVolume(VolumeMessage arg); - void setPlaybackSpeed(PlaybackSpeedMessage arg); - void play(TextureMessage arg); - PositionMessage position(TextureMessage arg); - void seekTo(PositionMessage arg); - void pause(TextureMessage arg); - void setMixWithOthers(MixWithOthersMessage arg); - static void setup(TestHostVideoPlayerApi api) { - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.initialize', - StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - api.initialize(); - return {}; - }); - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.create', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = - message as Map; - final CreateMessage input = CreateMessage._fromMap(mapMessage); - final TextureMessage output = api.create(input); - return {'result': output._toMap()}; - }); - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.dispose', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = - message as Map; - final TextureMessage input = TextureMessage._fromMap(mapMessage); - api.dispose(input); - return {}; - }); - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setLooping', - StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = - message as Map; - final LoopingMessage input = LoopingMessage._fromMap(mapMessage); - api.setLooping(input); - return {}; - }); - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setVolume', - StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = - message as Map; - final VolumeMessage input = VolumeMessage._fromMap(mapMessage); - api.setVolume(input); - return {}; - }); - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed', - StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = - message as Map; - final PlaybackSpeedMessage input = - PlaybackSpeedMessage._fromMap(mapMessage); - api.setPlaybackSpeed(input); - return {}; - }); - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.play', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = - message as Map; - final TextureMessage input = TextureMessage._fromMap(mapMessage); - api.play(input); - return {}; - }); - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.position', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = - message as Map; - final TextureMessage input = TextureMessage._fromMap(mapMessage); - final PositionMessage output = api.position(input); - return {'result': output._toMap()}; - }); - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.seekTo', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = - message as Map; - final PositionMessage input = PositionMessage._fromMap(mapMessage); - api.seekTo(input); - return {}; - }); - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.pause', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = - message as Map; - final TextureMessage input = TextureMessage._fromMap(mapMessage); - api.pause(input); - return {}; - }); - } - { - const BasicMessageChannel channel = BasicMessageChannel( - 'dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers', - StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = - message as Map; - final MixWithOthersMessage input = - MixWithOthersMessage._fromMap(mapMessage); - api.setMixWithOthers(input); - return {}; - }); - } - } -} diff --git a/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart b/packages/video_player/video_player_platform_interface/lib/method_channel_video_player.dart index 0ea443fb6e12..e92e87013d68 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -26,7 +26,7 @@ class MethodChannelVideoPlayer extends VideoPlayerPlatform { } @override - Future create(DataSource dataSource) async { + Future create(DataSource dataSource) async { CreateMessage message = CreateMessage(); switch (dataSource.sourceType) { @@ -37,6 +37,7 @@ class MethodChannelVideoPlayer extends VideoPlayerPlatform { case DataSourceType.network: message.uri = dataSource.uri; message.formatHint = _videoFormatStringMap[dataSource.formatHint]; + message.httpHeaders = dataSource.httpHeaders; break; case DataSourceType.file: message.uri = dataSource.uri; @@ -91,7 +92,7 @@ class MethodChannelVideoPlayer extends VideoPlayerPlatform { Future getPosition(int textureId) async { PositionMessage response = await _api.position(TextureMessage()..textureId = textureId); - return Duration(milliseconds: response.position); + return Duration(milliseconds: response.position!); } @override diff --git a/packages/video_player/video_player_platform_interface/lib/test.dart b/packages/video_player/video_player_platform_interface/lib/test.dart new file mode 100644 index 000000000000..b4fd81f44f41 --- /dev/null +++ b/packages/video_player/video_player_platform_interface/lib/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. + +// Autogenerated from Pigeon (v0.1.21), do not edit directly. +// See also: https://pub.dev/packages/pigeon +// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import +// @dart = 2.12 +import 'dart:async'; +import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'messages.dart'; + +abstract class TestHostVideoPlayerApi { + void initialize(); + TextureMessage create(CreateMessage arg); + void dispose(TextureMessage arg); + void setLooping(LoopingMessage arg); + void setVolume(VolumeMessage arg); + void setPlaybackSpeed(PlaybackSpeedMessage arg); + void play(TextureMessage arg); + PositionMessage position(TextureMessage arg); + void seekTo(PositionMessage arg); + void pause(TextureMessage arg); + void setMixWithOthers(MixWithOthersMessage arg); + static void setup(TestHostVideoPlayerApi? api) { + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.initialize', + StandardMessageCodec()); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + // ignore message + api.initialize(); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.create', StandardMessageCodec()); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.VideoPlayerApi.create was null. Expected CreateMessage.'); + final CreateMessage input = CreateMessage.decode(message!); + final TextureMessage output = api.create(input); + return {'result': output.encode()}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.dispose', StandardMessageCodec()); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.VideoPlayerApi.dispose was null. Expected TextureMessage.'); + final TextureMessage input = TextureMessage.decode(message!); + api.dispose(input); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.setLooping', + StandardMessageCodec()); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.VideoPlayerApi.setLooping was null. Expected LoopingMessage.'); + final LoopingMessage input = LoopingMessage.decode(message!); + api.setLooping(input); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.setVolume', + StandardMessageCodec()); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.VideoPlayerApi.setVolume was null. Expected VolumeMessage.'); + final VolumeMessage input = VolumeMessage.decode(message!); + api.setVolume(input); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed', + StandardMessageCodec()); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed was null. Expected PlaybackSpeedMessage.'); + final PlaybackSpeedMessage input = + PlaybackSpeedMessage.decode(message!); + api.setPlaybackSpeed(input); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.play', StandardMessageCodec()); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.VideoPlayerApi.play was null. Expected TextureMessage.'); + final TextureMessage input = TextureMessage.decode(message!); + api.play(input); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.position', StandardMessageCodec()); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.VideoPlayerApi.position was null. Expected TextureMessage.'); + final TextureMessage input = TextureMessage.decode(message!); + final PositionMessage output = api.position(input); + return {'result': output.encode()}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.seekTo', StandardMessageCodec()); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.VideoPlayerApi.seekTo was null. Expected PositionMessage.'); + final PositionMessage input = PositionMessage.decode(message!); + api.seekTo(input); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.pause', StandardMessageCodec()); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.VideoPlayerApi.pause was null. Expected TextureMessage.'); + final TextureMessage input = TextureMessage.decode(message!); + api.pause(input); + return {}; + }); + } + } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers', + StandardMessageCodec()); + if (api == null) { + channel.setMockMessageHandler(null); + } else { + channel.setMockMessageHandler((Object? message) async { + assert(message != null, + 'Argument for dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers was null. Expected MixWithOthersMessage.'); + final MixWithOthersMessage input = + MixWithOthersMessage.decode(message!); + api.setMixWithOthers(input); + return {}; + }); + } + } + } +} diff --git a/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart b/packages/video_player/video_player_platform_interface/lib/video_player_platform_interface.dart index 2757fb135af6..b2bff990401e 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -7,7 +7,7 @@ import 'dart:ui'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; -import 'package:meta/meta.dart' show required, visibleForTesting; +import 'package:meta/meta.dart' show visibleForTesting; import 'method_channel_video_player.dart'; @@ -66,7 +66,7 @@ abstract class VideoPlayerPlatform { } /// Creates an instance of a video player and returns its textureId. - Future create(DataSource dataSource) { + Future create(DataSource dataSource) { throw UnimplementedError('create() has not been implemented.'); } @@ -146,11 +146,12 @@ class DataSource { /// The [package] argument must be non-null when the asset comes from a /// package and null otherwise. DataSource({ - @required this.sourceType, + required this.sourceType, this.uri, this.formatHint, this.asset, this.package, + this.httpHeaders = const {}, }); /// The way in which the video was originally loaded. @@ -163,18 +164,23 @@ class DataSource { /// /// This will be in different formats depending on the [DataSourceType] of /// the original video. - final String uri; + final String? uri; /// **Android only**. Will override the platform's generic file format /// detection with whatever is set here. - final VideoFormat formatHint; + final VideoFormat? formatHint; + + /// HTTP headers used for the request to the [uri]. + /// Only for [DataSourceType.network] videos. + /// Always empty for other video types. + Map httpHeaders; /// The name of the asset. Only set for [DataSourceType.asset] videos. - final String asset; + final String? asset; /// The package that the asset was loaded from. Only set for /// [DataSourceType.asset] videos. - final String package; + final String? package; } /// The way in which the video was originally loaded. @@ -216,7 +222,7 @@ class VideoEvent { /// Depending on the [eventType], the [duration], [size] and [buffered] /// arguments can be null. VideoEvent({ - @required this.eventType, + required this.eventType, this.duration, this.size, this.buffered, @@ -228,17 +234,17 @@ class VideoEvent { /// Duration of the video. /// /// Only used if [eventType] is [VideoEventType.initialized]. - final Duration duration; + final Duration? duration; /// Size of the video. /// /// Only used if [eventType] is [VideoEventType.initialized]. - final Size size; + final Size? size; /// Buffered parts of the video. /// /// Only used if [eventType] is [VideoEventType.bufferingUpdate]. - final List buffered; + final List? buffered; @override bool operator ==(Object other) { @@ -346,6 +352,9 @@ class DurationRange { class VideoPlayerOptions { /// Set this to true to mix the video players audio with other audio sources. /// The default value is false + /// + /// Note: This option will be silently ignored in the web platform (there is + /// currently no way to implement this feature in this platform). final bool mixWithOthers; /// set additional optional player settings diff --git a/packages/video_player/video_player_platform_interface/pubspec.yaml b/packages/video_player/video_player_platform_interface/pubspec.yaml index cc3cd79f1f33..2a0ef10a9d2b 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. -homepage: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_platform_interface +repository: https://github.com/flutter/plugins/tree/master/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: 2.2.0 +version: 4.1.0 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" dependencies: flutter: sdk: flutter - meta: ^1.0.5 - -dev_dependencies: flutter_test: sdk: flutter - mockito: ^4.1.1 - pedantic: ^1.8.0 + meta: ^1.3.0 -environment: - sdk: ">=2.8.0 <3.0.0" - flutter: ">=1.10.0 <2.0.0" +dev_dependencies: + pedantic: ^1.10.0 diff --git a/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart b/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart index c4791001ad92..33a5b34b615d 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 @@ -1,4 +1,4 @@ -// Copyright 2017 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -6,20 +6,20 @@ import 'dart:ui'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/mockito.dart'; import 'package:video_player_platform_interface/messages.dart'; import 'package:video_player_platform_interface/method_channel_video_player.dart'; +import 'package:video_player_platform_interface/test.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; class _ApiLogger implements TestHostVideoPlayerApi { final List log = []; - TextureMessage textureMessage; - CreateMessage createMessage; - PositionMessage positionMessage; - LoopingMessage loopingMessage; - VolumeMessage volumeMessage; - PlaybackSpeedMessage playbackSpeedMessage; - MixWithOthersMessage mixWithOthersMessage; + TextureMessage? textureMessage; + CreateMessage? createMessage; + PositionMessage? positionMessage; + LoopingMessage? loopingMessage; + VolumeMessage? volumeMessage; + PlaybackSpeedMessage? playbackSpeedMessage; + MixWithOthersMessage? mixWithOthersMessage; @override TextureMessage create(CreateMessage arg) { @@ -97,28 +97,11 @@ void main() { expect(VideoPlayerPlatform.instance, isInstanceOf()); }); - - test('Cannot be implemented with `implements`', () { - expect(() { - VideoPlayerPlatform.instance = ImplementsVideoPlayerPlatform(); - }, throwsA(isInstanceOf())); - }); - - test('Can be mocked with `implements`', () { - final ImplementsVideoPlayerPlatform mock = - ImplementsVideoPlayerPlatform(); - when(mock.isMock).thenReturn(true); - VideoPlayerPlatform.instance = mock; - }); - - test('Can be extended', () { - VideoPlayerPlatform.instance = ExtendsVideoPlayerPlatform(); - }); }); group('$MethodChannelVideoPlayer', () { final MethodChannelVideoPlayer player = MethodChannelVideoPlayer(); - _ApiLogger log; + late _ApiLogger log; setUp(() { log = _ApiLogger(); @@ -136,177 +119,176 @@ void main() { test('dispose', () async { await player.dispose(1); expect(log.log.last, 'dispose'); - expect(log.textureMessage.textureId, 1); + expect(log.textureMessage?.textureId, 1); }); test('create with asset', () async { - final int textureId = await player.create(DataSource( + final int? textureId = await player.create(DataSource( sourceType: DataSourceType.asset, asset: 'someAsset', package: 'somePackage', )); expect(log.log.last, 'create'); - expect(log.createMessage.asset, 'someAsset'); - expect(log.createMessage.packageName, 'somePackage'); + expect(log.createMessage?.asset, 'someAsset'); + expect(log.createMessage?.packageName, 'somePackage'); expect(textureId, 3); }); test('create with network', () async { - final int textureId = await player.create(DataSource( + final int? textureId = await player.create(DataSource( sourceType: DataSourceType.network, uri: 'someUri', formatHint: VideoFormat.dash, )); expect(log.log.last, 'create'); - expect(log.createMessage.uri, 'someUri'); - expect(log.createMessage.formatHint, 'dash'); + expect(log.createMessage?.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( + final int? textureId = await player.create(DataSource( sourceType: DataSourceType.file, uri: 'someUri', )); expect(log.log.last, 'create'); - expect(log.createMessage.uri, 'someUri'); + expect(log.createMessage?.uri, 'someUri'); expect(textureId, 3); }); test('setLooping', () async { await player.setLooping(1, true); expect(log.log.last, 'setLooping'); - expect(log.loopingMessage.textureId, 1); - expect(log.loopingMessage.isLooping, true); + expect(log.loopingMessage?.textureId, 1); + expect(log.loopingMessage?.isLooping, true); }); test('play', () async { await player.play(1); expect(log.log.last, 'play'); - expect(log.textureMessage.textureId, 1); + expect(log.textureMessage?.textureId, 1); }); test('pause', () async { await player.pause(1); expect(log.log.last, 'pause'); - expect(log.textureMessage.textureId, 1); + expect(log.textureMessage?.textureId, 1); }); test('setMixWithOthers', () async { await player.setMixWithOthers(true); expect(log.log.last, 'setMixWithOthers'); - expect(log.mixWithOthersMessage.mixWithOthers, true); + expect(log.mixWithOthersMessage?.mixWithOthers, true); await player.setMixWithOthers(false); expect(log.log.last, 'setMixWithOthers'); - expect(log.mixWithOthersMessage.mixWithOthers, false); + expect(log.mixWithOthersMessage?.mixWithOthers, false); }); test('setVolume', () async { await player.setVolume(1, 0.7); expect(log.log.last, 'setVolume'); - expect(log.volumeMessage.textureId, 1); - expect(log.volumeMessage.volume, 0.7); + expect(log.volumeMessage?.textureId, 1); + expect(log.volumeMessage?.volume, 0.7); }); test('setPlaybackSpeed', () async { await player.setPlaybackSpeed(1, 1.5); expect(log.log.last, 'setPlaybackSpeed'); - expect(log.playbackSpeedMessage.textureId, 1); - expect(log.playbackSpeedMessage.speed, 1.5); + expect(log.playbackSpeedMessage?.textureId, 1); + expect(log.playbackSpeedMessage?.speed, 1.5); }); test('seekTo', () async { await player.seekTo(1, const Duration(milliseconds: 12345)); expect(log.log.last, 'seekTo'); - expect(log.positionMessage.textureId, 1); - expect(log.positionMessage.position, 12345); + expect(log.positionMessage?.textureId, 1); + expect(log.positionMessage?.position, 12345); }); test('getPosition', () async { final Duration position = await player.getPosition(1); expect(log.log.last, 'position'); - expect(log.textureMessage.textureId, 1); + expect(log.textureMessage?.textureId, 1); expect(position, const Duration(milliseconds: 234)); }); test('videoEventsFor', () async { - // TODO(cbenhagen): This has been deprecated and should be replaced - // with `ServicesBinding.instance.defaultBinaryMessenger` when it's - // available on all the versions of Flutter that we test. - // ignore: deprecated_member_use - defaultBinaryMessenger.setMockMessageHandler( + ServicesBinding.instance?.defaultBinaryMessenger.setMockMessageHandler( "flutter.io/videoPlayer/videoEvents123", - (ByteData message) async { + (ByteData? message) async { final MethodCall methodCall = const StandardMethodCodec().decodeMethodCall(message); if (methodCall.method == 'listen') { - // TODO(cbenhagen): This has been deprecated and should be replaced - // with `ServicesBinding.instance.defaultBinaryMessenger` when it's - // available on all the versions of Flutter that we test. - // ignore: deprecated_member_use - await defaultBinaryMessenger.handlePlatformMessage( - "flutter.io/videoPlayer/videoEvents123", - const StandardMethodCodec() - .encodeSuccessEnvelope({ - 'event': 'initialized', - 'duration': 98765, - 'width': 1920, - 'height': 1080, - }), - (ByteData data) {}); - - // TODO(cbenhagen): This has been deprecated and should be replaced - // with `ServicesBinding.instance.defaultBinaryMessenger` when it's - // available on all the versions of Flutter that we test. - // ignore: deprecated_member_use - await defaultBinaryMessenger.handlePlatformMessage( - "flutter.io/videoPlayer/videoEvents123", - const StandardMethodCodec() - .encodeSuccessEnvelope({ - 'event': 'completed', - }), - (ByteData data) {}); - - // TODO(cbenhagen): This has been deprecated and should be replaced - // with `ServicesBinding.instance.defaultBinaryMessenger` when it's - // available on all the versions of Flutter that we test. - // ignore: deprecated_member_use - await defaultBinaryMessenger.handlePlatformMessage( - "flutter.io/videoPlayer/videoEvents123", - const StandardMethodCodec() - .encodeSuccessEnvelope({ - 'event': 'bufferingUpdate', - 'values': >[ - [0, 1234], - [1235, 4000], - ], - }), - (ByteData data) {}); - - // TODO(cbenhagen): This has been deprecated and should be replaced - // with `ServicesBinding.instance.defaultBinaryMessenger` when it's - // available on all the versions of Flutter that we test. - // ignore: deprecated_member_use - await defaultBinaryMessenger.handlePlatformMessage( - "flutter.io/videoPlayer/videoEvents123", - const StandardMethodCodec() - .encodeSuccessEnvelope({ - 'event': 'bufferingStart', - }), - (ByteData data) {}); - - // TODO(cbenhagen): This has been deprecated and should be replaced - // with `ServicesBinding.instance.defaultBinaryMessenger` when it's - // available on all the versions of Flutter that we test. - // ignore: deprecated_member_use - await defaultBinaryMessenger.handlePlatformMessage( - "flutter.io/videoPlayer/videoEvents123", - const StandardMethodCodec() - .encodeSuccessEnvelope({ - 'event': 'bufferingEnd', - }), - (ByteData data) {}); + await ServicesBinding.instance?.defaultBinaryMessenger + .handlePlatformMessage( + "flutter.io/videoPlayer/videoEvents123", + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'initialized', + 'duration': 98765, + 'width': 1920, + 'height': 1080, + }), + (ByteData? data) {}); + + await ServicesBinding.instance?.defaultBinaryMessenger + .handlePlatformMessage( + "flutter.io/videoPlayer/videoEvents123", + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'completed', + }), + (ByteData? data) {}); + + await ServicesBinding.instance?.defaultBinaryMessenger + .handlePlatformMessage( + "flutter.io/videoPlayer/videoEvents123", + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'bufferingUpdate', + 'values': >[ + [0, 1234], + [1235, 4000], + ], + }), + (ByteData? data) {}); + + await ServicesBinding.instance?.defaultBinaryMessenger + .handlePlatformMessage( + "flutter.io/videoPlayer/videoEvents123", + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'bufferingStart', + }), + (ByteData? data) {}); + + await ServicesBinding.instance?.defaultBinaryMessenger + .handlePlatformMessage( + "flutter.io/videoPlayer/videoEvents123", + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'bufferingEnd', + }), + (ByteData? data) {}); return const StandardMethodCodec().encodeSuccessEnvelope(null); } else if (methodCall.method == 'cancel') { @@ -343,8 +325,3 @@ void main() { }); }); } - -class ImplementsVideoPlayerPlatform extends Mock - implements VideoPlayerPlatform {} - -class ExtendsVideoPlayerPlatform extends VideoPlayerPlatform {} diff --git a/packages/video_player/video_player_web/AUTHORS b/packages/video_player/video_player_web/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/video_player/video_player_web/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/video_player/video_player_web/CHANGELOG.md b/packages/video_player/video_player_web/CHANGELOG.md index d18504913d89..38bfe90f7b1e 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,3 +1,23 @@ +## 2.0.1 + +* Fix videos not playing in Safari/Chrome on iOS by setting autoplay to false +* Change sizing code of `Video` widget's `HtmlElementView` so it works well when slotted. +* Move tests to `example` directory, so they run as integration_tests with `flutter drive`. + +## 2.0.0 + +* Migrate to null safety. +* Calling `setMixWithOthers()` now is silently ignored instead of throwing an exception. +* Fixed an issue where `isBuffering` was not updating on Web. + +## 0.1.4+2 + +* Update Flutter SDK constraint. + +## 0.1.4+1 + +* Substitute `undefined_prefixed_name: ignore` analyzer setting by a `dart:ui` shim with conditional exports. [Issue](https://github.com/flutter/flutter/issues/69309). + ## 0.1.4 * Added option to set the video playback speed on the video controller. diff --git a/packages/video_player/video_player_web/LICENSE b/packages/video_player/video_player_web/LICENSE index a6d6c0749818..c6823b81eb84 100644 --- a/packages/video_player/video_player_web/LICENSE +++ b/packages/video_player/video_player_web/LICENSE @@ -1,4 +1,4 @@ -Copyright 2017 The Chromium Authors. All rights reserved. +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: diff --git a/packages/video_player/video_player_web/README.md b/packages/video_player/video_player_web/README.md index 216b926bf26e..d44f738aeb66 100644 --- a/packages/video_player/video_player_web/README.md +++ b/packages/video_player/video_player_web/README.md @@ -3,13 +3,6 @@ The web implementation of [`video_player`][1]. -**Please set your constraint to `video_player_web: '>=0.1.y+x <2.0.0'`** - -## Backward compatible 1.0.0 version is coming -The plugin has reached a stable API, we guarantee that version `1.0.0` will be backward compatible with `0.1.y+z`. -Please use `video_player_web: '>=0.1.y+x <2.0.0'` as your dependency constraint to allow a smoother ecosystem migration. -For more details see: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 - ## Usage This package is the endorsed implementation of `video_player` for the web platform since version `0.10.5`, so it gets automatically added to your application by depending on `video_player: ^0.10.5`. @@ -35,6 +28,10 @@ The Web platform does **not** suppport `dart:io`, so attempts to create a `Video Playing videos without prior interaction with the site might be prohibited by the browser and lead to runtime errors. See also: https://goo.gl/xX8pDD. +## Mixing audio with other audio sources + +The `VideoPlayerOptions.mixWithOthers` option can't be implemented in web, at least at the moment. If you use this option it will be silently ignored. + ## Supported Formats **Different web browsers support different sets of video codecs.** diff --git a/packages/video_player/video_player_web/analysis_options.yaml b/packages/video_player/video_player_web/analysis_options.yaml deleted file mode 100644 index 443b16551ec9..000000000000 --- a/packages/video_player/video_player_web/analysis_options.yaml +++ /dev/null @@ -1,10 +0,0 @@ -# This is a temporary file to allow us to unblock the flutter/plugins repo CI. -# It disables some of lints that were disabled inline. Disabling lints inline -# is no longer possible, so this file is required. -# TODO(ditman) https://github.com/flutter/flutter/issues/55000 (clean this up) - -include: ../../../analysis_options.yaml - -analyzer: - errors: - undefined_prefixed_name: ignore diff --git a/packages/video_player/video_player_web/example/README.md b/packages/video_player/video_player_web/example/README.md new file mode 100644 index 000000000000..0ec01e025570 --- /dev/null +++ b/packages/video_player/video_player_web/example/README.md @@ -0,0 +1,21 @@ +# Testing + +This package utilizes the `integration_test` package to run its tests in a web browser. + +See [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) for more info. + +## Running the tests + +Make sure you have updated to the latest Flutter master. + +1. Check what version of Chrome is running on the machine you're running tests on. + +2. Download and install driver for that version from here: + * + +3. Start the driver using `chromedriver --port=4444` + +4. Run tests: `flutter drive -d web-server --browser-name=chrome --driver=test_driver/integration_driver.dart --target=integration_test/TEST_NAME.dart`, or (in Linux): + + * Single: `./run_test.sh integration_test/TEST_NAME.dart` + * All: `./run_test.sh` diff --git a/packages/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 new file mode 100644 index 000000000000..d3ad80c890f1 --- /dev/null +++ b/packages/video_player/video_player_web/example/integration_test/video_player_web_test.dart @@ -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. + +import 'dart:async'; + +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:video_player_platform_interface/video_player_platform_interface.dart'; +import 'package:video_player_web/video_player_web.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('VideoPlayer for Web', () { + late Future textureId; + + setUp(() { + VideoPlayerPlatform.instance = VideoPlayerPlugin(); + textureId = VideoPlayerPlatform.instance + .create( + DataSource( + sourceType: DataSourceType.network, + uri: + 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', + ), + ) + .then((textureId) => textureId!); + }); + + testWidgets('can init', (WidgetTester tester) async { + expect(VideoPlayerPlatform.instance.init(), completes); + }); + + testWidgets('can create from network', (WidgetTester tester) async { + expect( + VideoPlayerPlatform.instance.create( + DataSource( + sourceType: DataSourceType.network, + uri: + 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'), + ), + completion(isNonZero)); + }); + + testWidgets('can create from asset', (WidgetTester tester) async { + expect( + VideoPlayerPlatform.instance.create( + DataSource( + sourceType: DataSourceType.asset, + asset: 'videos/bee.mp4', + package: 'bee_vids', + ), + ), + completion(isNonZero)); + }); + + testWidgets('cannot create from file', (WidgetTester tester) async { + expect( + VideoPlayerPlatform.instance.create( + DataSource( + sourceType: DataSourceType.file, + uri: '/videos/bee.mp4', + ), + ), + throwsUnimplementedError); + }); + + testWidgets('can dispose', (WidgetTester tester) async { + expect(VideoPlayerPlatform.instance.dispose(await textureId), completes); + }); + + testWidgets('can set looping', (WidgetTester tester) async { + expect( + VideoPlayerPlatform.instance.setLooping(await textureId, true), + completes, + ); + }); + + testWidgets('can play', (WidgetTester tester) async { + // Mute video to allow autoplay (See https://goo.gl/xX8pDD) + await VideoPlayerPlatform.instance.setVolume(await textureId, 0); + expect(VideoPlayerPlatform.instance.play(await textureId), completes); + }); + + testWidgets('throws PlatformException when playing bad media', + (WidgetTester tester) async { + int videoPlayerId = (await VideoPlayerPlatform.instance.create( + DataSource( + sourceType: DataSourceType.network, + uri: + 'https://flutter.github.io/assets-for-api-docs/assets/videos/_non_existent_video.mp4'), + ))!; + + Stream eventStream = + VideoPlayerPlatform.instance.videoEventsFor(videoPlayerId); + + // Mute video to allow autoplay (See https://goo.gl/xX8pDD) + await VideoPlayerPlatform.instance.setVolume(videoPlayerId, 0); + await VideoPlayerPlatform.instance.play(videoPlayerId); + + expect(() async { + await eventStream.last; + }, throwsA(isA())); + }); + + testWidgets('can pause', (WidgetTester tester) async { + expect(VideoPlayerPlatform.instance.pause(await textureId), completes); + }); + + testWidgets('can set volume', (WidgetTester tester) async { + expect( + VideoPlayerPlatform.instance.setVolume(await textureId, 0.8), + completes, + ); + }); + + testWidgets('can set playback speed', (WidgetTester tester) async { + expect( + VideoPlayerPlatform.instance.setPlaybackSpeed(await textureId, 2.0), + completes, + ); + }); + + testWidgets('can seek to position', (WidgetTester tester) async { + expect( + VideoPlayerPlatform.instance.seekTo( + await textureId, + Duration(seconds: 1), + ), + completes, + ); + }); + + testWidgets('can get position', (WidgetTester tester) async { + expect(VideoPlayerPlatform.instance.getPosition(await textureId), + completion(isInstanceOf())); + }); + + testWidgets('can get video event stream', (WidgetTester tester) async { + expect(VideoPlayerPlatform.instance.videoEventsFor(await textureId), + isInstanceOf>()); + }); + + testWidgets('can build view', (WidgetTester tester) async { + expect(VideoPlayerPlatform.instance.buildView(await textureId), + isInstanceOf()); + }); + + testWidgets('ignores setting mixWithOthers', (WidgetTester tester) async { + expect(VideoPlayerPlatform.instance.setMixWithOthers(true), completes); + expect(VideoPlayerPlatform.instance.setMixWithOthers(false), completes); + }); + }); +} diff --git a/packages/video_player/video_player_web/example/lib/main.dart b/packages/video_player/video_player_web/example/lib/main.dart new file mode 100644 index 000000000000..e1a38dcdcd46 --- /dev/null +++ b/packages/video_player/video_player_web/example/lib/main.dart @@ -0,0 +1,25 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +void main() { + runApp(MyApp()); +} + +/// App for testing +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State { + @override + Widget build(BuildContext context) { + return Directionality( + textDirection: TextDirection.ltr, + child: Text('Testing... Look at the console output for results!'), + ); + } +} diff --git a/packages/video_player/video_player_web/example/pubspec.yaml b/packages/video_player/video_player_web/example/pubspec.yaml new file mode 100644 index 000000000000..c172eeaf1223 --- /dev/null +++ b/packages/video_player/video_player_web/example/pubspec.yaml @@ -0,0 +1,20 @@ +name: connectivity_for_web_integration_tests +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.2.0" + +dependencies: + video_player_web: + path: ../ + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter diff --git a/packages/video_player/video_player_web/example/run_test.sh b/packages/video_player/video_player_web/example/run_test.sh new file mode 100755 index 000000000000..aa52974f310e --- /dev/null +++ b/packages/video_player/video_player_web/example/run_test.sh @@ -0,0 +1,22 @@ +#!/usr/bin/bash +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +if pgrep -lf chromedriver > /dev/null; then + echo "chromedriver is running." + + if [ $# -eq 0 ]; then + echo "No target specified, running all tests..." + find integration_test/ -iname *_test.dart | xargs -n1 -i -t flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target='{}' + else + echo "Running test target: $1..." + set -x + flutter drive -d web-server --web-port=7357 --browser-name=chrome --driver=test_driver/integration_test.dart --target=$1 + fi + + else + echo "chromedriver is not running." + echo "Please, check the README.md for instructions on how to use run_test.sh" +fi + diff --git a/packages/video_player/video_player_web/example/test_driver/integration_test.dart b/packages/video_player/video_player_web/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/video_player/video_player_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/video_player/video_player_web/example/web/index.html b/packages/video_player/video_player_web/example/web/index.html new file mode 100644 index 000000000000..7fb138cc90fa --- /dev/null +++ b/packages/video_player/video_player_web/example/web/index.html @@ -0,0 +1,13 @@ + + + + + + example + + + + + diff --git a/packages/video_player/video_player_web/ios/video_player_web.podspec b/packages/video_player/video_player_web/ios/video_player_web.podspec deleted file mode 100644 index 5129b7c69032..000000000000 --- a/packages/video_player/video_player_web/ios/video_player_web.podspec +++ /dev/null @@ -1,20 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'video_player_web' - s.version = '0.0.1' - s.summary = 'No-op implementation of video_player_web web plugin to avoid build issues on iOS' - s.description = <<-DESC -temp fake video_player_web plugin - DESC - s.homepage = 'https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_web' - s.license = { :file => '../LICENSE' } - s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } - s.source = { :path => '.' } - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.ios.deployment_target = '8.0' -end \ No newline at end of file diff --git a/packages/video_player/video_player_web/lib/src/shims/dart_ui.dart b/packages/video_player/video_player_web/lib/src/shims/dart_ui.dart new file mode 100644 index 000000000000..5eacec5fe867 --- /dev/null +++ b/packages/video_player/video_player_web/lib/src/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(flutter/flutter#55000) Remove this file once web-only dart:ui APIs +// are exposed from a dedicated place. +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 new file mode 100644 index 000000000000..f2862af8b704 --- /dev/null +++ b/packages/video_player/video_player_web/lib/src/shims/dart_ui_fake.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 '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. + +/// Shim for web_ui engine.PlatformViewRegistry +/// https://github.com/flutter/engine/blob/master/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) {} +} + +/// Shim for web_ui engine.AssetManager. +/// https://github.com/flutter/engine/blob/master/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) {} +} + +/// Signature of callbacks that have no arguments and return no data. +typedef VoidCallback = void Function(); diff --git a/packages/video_player/video_player_web/lib/src/shims/dart_ui_real.dart b/packages/video_player/video_player_web/lib/src/shims/dart_ui_real.dart new file mode 100644 index 000000000000..276b768c76c5 --- /dev/null +++ b/packages/video_player/video_player_web/lib/src/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/video_player/video_player_web/lib/video_player_web.dart b/packages/video_player/video_player_web/lib/video_player_web.dart index 251da3779e7f..f9e27d16725a 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 @@ -1,6 +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 'dart:async'; import 'dart:html'; -import 'dart:ui' as ui; +import 'src/shims/dart_ui.dart' as ui; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -50,7 +54,7 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { @override Future dispose(int textureId) async { - _videoPlayers[textureId].dispose(); + _videoPlayers[textureId]!.dispose(); _videoPlayers.remove(textureId); return null; } @@ -66,20 +70,18 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { final int textureId = _textureCounter; _textureCounter++; - String uri; + late String uri; switch (dataSource.sourceType) { case DataSourceType.network: // Do NOT modify the incoming uri, it can be a Blob, and Safari doesn't // like blobs that have changed. - uri = dataSource.uri; + uri = dataSource.uri ?? ''; break; case DataSourceType.asset: - String assetUrl = dataSource.asset; - if (dataSource.package != null && dataSource.package.isNotEmpty) { + String assetUrl = dataSource.asset!; + if (dataSource.package != null && dataSource.package!.isNotEmpty) { assetUrl = 'packages/${dataSource.package}/$assetUrl'; } - // 'webOnlyAssetManager' is only in the web version of dart:ui - // ignore: undefined_prefixed_name assetUrl = ui.webOnlyAssetManager.getAssetUrl(assetUrl); uri = assetUrl; break; @@ -101,76 +103,95 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { @override Future setLooping(int textureId, bool looping) async { - return _videoPlayers[textureId].setLooping(looping); + return _videoPlayers[textureId]!.setLooping(looping); } @override Future play(int textureId) async { - return _videoPlayers[textureId].play(); + return _videoPlayers[textureId]!.play(); } @override Future pause(int textureId) async { - return _videoPlayers[textureId].pause(); + return _videoPlayers[textureId]!.pause(); } @override Future setVolume(int textureId, double volume) async { - return _videoPlayers[textureId].setVolume(volume); + return _videoPlayers[textureId]!.setVolume(volume); } @override Future setPlaybackSpeed(int textureId, double speed) async { assert(speed > 0); - return _videoPlayers[textureId].setPlaybackSpeed(speed); + return _videoPlayers[textureId]!.setPlaybackSpeed(speed); } @override Future seekTo(int textureId, Duration position) async { - return _videoPlayers[textureId].seekTo(position); + return _videoPlayers[textureId]!.seekTo(position); } @override Future getPosition(int textureId) async { - _videoPlayers[textureId].sendBufferingUpdate(); - return _videoPlayers[textureId].getPosition(); + _videoPlayers[textureId]!.sendBufferingUpdate(); + return _videoPlayers[textureId]!.getPosition(); } @override Stream videoEventsFor(int textureId) { - return _videoPlayers[textureId].eventController.stream; + return _videoPlayers[textureId]!.eventController.stream; } @override Widget buildView(int textureId) { return HtmlElementView(viewType: 'videoPlayer-$textureId'); } + + /// Sets the audio mode to mix with other sources (ignored) + @override + Future setMixWithOthers(bool mixWithOthers) => Future.value(); } class _VideoPlayer { - _VideoPlayer({this.uri, this.textureId}); + _VideoPlayer({required this.uri, required this.textureId}); final StreamController eventController = StreamController(); final String uri; final int textureId; - VideoElement videoElement; + late VideoElement videoElement; bool isInitialized = false; + bool isBuffering = false; + + void setBuffering(bool buffering) { + if (isBuffering != buffering) { + isBuffering = buffering; + eventController.add(VideoEvent( + eventType: isBuffering + ? VideoEventType.bufferingStart + : VideoEventType.bufferingEnd)); + } + } void initialize() { videoElement = VideoElement() ..src = uri ..autoplay = false ..controls = false - ..style.border = 'none'; + ..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 - // ignore: undefined_prefixed_name ui.platformViewRegistry.registerViewFactory( 'videoPlayer-$textureId', (int viewId) => videoElement); @@ -179,22 +200,38 @@ class _VideoPlayer { isInitialized = true; sendInitialized(); } + setBuffering(false); + }); + + videoElement.onCanPlayThrough.listen((dynamic _) { + setBuffering(false); + }); + + videoElement.onPlaying.listen((dynamic _) { + setBuffering(false); + }); + + videoElement.onWaiting.listen((dynamic _) { + setBuffering(true); + sendBufferingUpdate(); }); // The error event fires when some form of error occurs while attempting to load or perform the media. videoElement.onError.listen((Event _) { + setBuffering(false); // The Event itself (_) doesn't contain info about the actual error. // We need to look at the HTMLMediaElement.error. // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/error - MediaError error = videoElement.error; + MediaError error = videoElement.error!; eventController.addError(PlatformException( - code: _kErrorValueToErrorName[error.code], + code: _kErrorValueToErrorName[error.code]!, message: error.message != '' ? error.message : _kDefaultErrorMessage, details: _kErrorValueToErrorDescription[error.code], )); }); videoElement.onEnded.listen((dynamic _) { + setBuffering(false); eventController.add(VideoEvent(eventType: VideoEventType.completed)); }); } @@ -261,8 +298,8 @@ class _VideoPlayer { milliseconds: (videoElement.duration * 1000).round(), ), size: Size( - videoElement.videoWidth.toDouble() ?? 0.0, - videoElement.videoHeight.toDouble() ?? 0.0, + videoElement.videoWidth.toDouble(), + videoElement.videoHeight.toDouble(), ), ), ); diff --git a/packages/video_player/video_player_web/pubspec.yaml b/packages/video_player/video_player_web/pubspec.yaml index 98191bf6ba85..568a9262b5f0 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -1,10 +1,12 @@ name: video_player_web description: Web platform implementation of video_player. -homepage: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_web -# 0.1.y+z is compatible with 1.0.0, if you land a breaking change bump -# the version to 2.0.0. -# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.1.4 +repository: https://github.com/flutter/plugins/tree/master/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.1 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: @@ -18,16 +20,10 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - meta: ^1.1.7 - video_player_platform_interface: ^2.2.0 + meta: ^1.3.0 + video_player_platform_interface: ^4.0.0 dev_dependencies: flutter_test: sdk: flutter - video_player: - path: ../video_player - pedantic: ^1.8.0 - -environment: - sdk: ">=2.8.0 <3.0.0" - flutter: ">=1.12.8 <2.0.0" + pedantic: ^1.10.0 diff --git a/packages/video_player/video_player_web/test/README.md b/packages/video_player/video_player_web/test/README.md new file mode 100644 index 000000000000..7c5b4ad682ba --- /dev/null +++ b/packages/video_player/video_player_web/test/README.md @@ -0,0 +1,5 @@ +## test + +This package uses integration tests for testing. + +See `example/README.md` for more info. diff --git a/packages/video_player/video_player_web/test/tests_exist_elsewhere_test.dart b/packages/video_player/video_player_web/test/tests_exist_elsewhere_test.dart new file mode 100644 index 000000000000..442c50144727 --- /dev/null +++ b/packages/video_player/video_player_web/test/tests_exist_elsewhere_test.dart @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '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/video_player/video_player_web/test/video_player_web_test.dart b/packages/video_player/video_player_web/test/video_player_web_test.dart deleted file mode 100644 index 453079bfcd40..000000000000 --- a/packages/video_player/video_player_web/test/video_player_web_test.dart +++ /dev/null @@ -1,140 +0,0 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. -@TestOn('browser') - -import 'dart:async'; - -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:video_player/video_player.dart'; -import 'package:video_player_platform_interface/video_player_platform_interface.dart'; -import 'package:video_player_web/video_player_web.dart'; - -void main() { - group('VideoPlayer for Web', () { - int textureId; - - setUp(() async { - VideoPlayerPlatform.instance = VideoPlayerPlugin(); - textureId = await VideoPlayerPlatform.instance.create( - DataSource( - sourceType: DataSourceType.network, - uri: - 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'), - ); - }); - - test('$VideoPlayerPlugin is the live instance', () { - expect(VideoPlayerPlatform.instance, isA()); - }); - - test('can init', () { - expect(VideoPlayerPlatform.instance.init(), completes); - }); - - test('can create from network', () { - expect( - VideoPlayerPlatform.instance.create( - DataSource( - sourceType: DataSourceType.network, - uri: - 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4'), - ), - completion(isNonZero)); - }); - - test('can create from asset', () { - expect( - VideoPlayerPlatform.instance.create( - DataSource( - sourceType: DataSourceType.asset, - asset: 'videos/bee.mp4', - package: 'bee_vids', - ), - ), - completion(isNonZero)); - }); - - test('cannot create from file', () { - expect( - VideoPlayerPlatform.instance.create( - DataSource( - sourceType: DataSourceType.file, - uri: '/videos/bee.mp4', - ), - ), - throwsUnimplementedError); - }); - - test('can dispose', () { - expect(VideoPlayerPlatform.instance.dispose(textureId), completes); - }); - - test('can set looping', () { - expect( - VideoPlayerPlatform.instance.setLooping(textureId, true), completes); - }); - - test('can play', () async { - // Mute video to allow autoplay (See https://goo.gl/xX8pDD) - await VideoPlayerPlatform.instance.setVolume(textureId, 0); - expect(VideoPlayerPlatform.instance.play(textureId), completes); - }); - - test('throws PlatformException when playing bad media', () async { - int videoPlayerId = await VideoPlayerPlatform.instance.create( - DataSource( - sourceType: DataSourceType.network, - uri: - 'https://flutter.github.io/assets-for-api-docs/assets/videos/_non_existent_video.mp4'), - ); - - Stream eventStream = - VideoPlayerPlatform.instance.videoEventsFor(videoPlayerId); - - // Mute video to allow autoplay (See https://goo.gl/xX8pDD) - await VideoPlayerPlatform.instance.setVolume(videoPlayerId, 0); - await VideoPlayerPlatform.instance.play(videoPlayerId); - - expect(eventStream, emitsError(isA())); - }); - - test('can pause', () { - expect(VideoPlayerPlatform.instance.pause(textureId), completes); - }); - - test('can set volume', () { - expect(VideoPlayerPlatform.instance.setVolume(textureId, 0.8), completes); - }); - - test('can set playback speed', () { - expect( - VideoPlayerPlatform.instance.setPlaybackSpeed(textureId, 2.0), - completes, - ); - }); - - test('can seek to position', () { - expect( - VideoPlayerPlatform.instance.seekTo(textureId, Duration(seconds: 1)), - completes); - }); - - test('can get position', () { - expect(VideoPlayerPlatform.instance.getPosition(textureId), - completion(isInstanceOf())); - }); - - test('can get video event stream', () { - expect(VideoPlayerPlatform.instance.videoEventsFor(textureId), - isInstanceOf>()); - }); - - test('can build view', () { - expect(VideoPlayerPlatform.instance.buildView(textureId), - isInstanceOf()); - }); - }); -} diff --git a/packages/webview_flutter/AUTHORS b/packages/webview_flutter/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/webview_flutter/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/webview_flutter/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md index f32724589020..9167f9240044 100644 --- a/packages/webview_flutter/CHANGELOG.md +++ b/packages/webview_flutter/CHANGELOG.md @@ -1,3 +1,85 @@ +## NEXT + +* Add iOS UI integration test target. + +## 2.0.8 + +* Migrate maven repository from jcenter to mavenCentral. + +## 2.0.7 + +* Republished 2.0.6 with Flutter 2.2 to avoid https://github.com/dart-lang/pub/issues/3001 + +## 2.0.6 + +* WebView requires at least Android 19 if you are using +hybrid composition ([flutter/issues/59894](https://github.com/flutter/flutter/issues/59894)). + +## 2.0.5 + +* Example app observes `uiMode`, so the WebView isn't reattached when the UI mode changes. (e.g. switching to Dark mode). + +## 2.0.4 + +* Fix a bug where `allowsInlineMediaPlayback` is not respected on iOS. + +## 2.0.3 + +* Fixes bug where scroll bars on the Android non-hybrid WebView are rendered on +the wrong side of the screen. + +## 2.0.2 + +* Fixes bug where text fields are hidden behind the keyboard +when hybrid composition is used [flutter/issues/75667](https://github.com/flutter/flutter/issues/75667). + +## 2.0.1 + +* Run CocoaPods iOS tests in RunnerUITests target + +## 2.0.0 + +* Migration to null-safety. +* Added support for progress tracking. +* Add section to the wiki explaining how to use Material components. +* Update integration test to workaround an iOS 14 issue with `evaluateJavascript`. +* Fix `onWebResourceError` on iOS. +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) +* Added `allowsInlineMediaPlayback` property. + +## 1.0.8 + +* Update Flutter SDK constraint. + +## 1.0.7 + +* Minor documentation update to indicate known issue on iOS 13.4 and 13.5. + * See: https://github.com/flutter/flutter/issues/53490 + +## 1.0.6 + +* Invoke the WebView.onWebResourceError on iOS when the webview content process crashes. + +## 1.0.5 + +* Fix example in the readme. + +## 1.0.4 + +* Suppress the `deprecated_member_use` warning in the example app for `ScaffoldMessenger.showSnackBar`. + +## 1.0.3 + +* Update android compileSdkVersion to 29. + +## 1.0.2 + +* Android Code Inspection and Clean up. + +## 1.0.1 + +* Add documentation for `WebViewPlatformCreatedCallback`. + ## 1.0.0 - Out of developer preview 🎉. * Bumped the minimal Flutter SDK to 1.22 where platform views are out of developer preview, and diff --git a/packages/webview_flutter/LICENSE b/packages/webview_flutter/LICENSE index ad33cf3c3ed1..c6823b81eb84 100644 --- a/packages/webview_flutter/LICENSE +++ b/packages/webview_flutter/LICENSE @@ -1,4 +1,4 @@ -Copyright 2018 The Chromium Authors. All rights reserved. +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: diff --git a/packages/webview_flutter/README.md b/packages/webview_flutter/README.md index c7b55f1c89dd..2bfc312d36ab 100644 --- a/packages/webview_flutter/README.md +++ b/packages/webview_flutter/README.md @@ -1,6 +1,6 @@ # WebView for Flutter -[![pub package](https://img.shields.io/pub/v/webview_flutter.svg)](https://pub.dartlang.org/packages/webview_flutter) +[![pub package](https://img.shields.io/pub/v/webview_flutter.svg)](https://pub.dev/packages/webview_flutter) A Flutter plugin that provides a WebView widget. @@ -8,14 +8,12 @@ On iOS the WebView widget is backed by a [WKWebView](https://developer.apple.com On Android the WebView widget is backed by a [WebView](https://developer.android.com/reference/android/webkit/WebView). ## Usage -Add `webview_flutter` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). +Add `webview_flutter` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). You can now include a WebView widget in your widget tree. See the [WebView](https://pub.dev/documentation/webview_flutter/latest/webview_flutter/WebView-class.html) widget's Dartdoc for more details on how to use the widget. - - ## Android Platform Views The WebView is relying on [Platform Views](https://flutter.dev/docs/development/platform-integration/platform-views) to embed @@ -28,7 +26,19 @@ implementation. Note that on Android versions prior to Android 10 Hybrid Composi ### Using Hybrid Composition -To enable hybrid composition, set `WebView.platform = SurfaceAndroidWebView();` in `initState()`. +1. Set the `minSdkVersion` in `android/app/build.gradle`: + +```groovy +android { + defaultConfig { + minSdkVersion 19 + } +} +``` + +This means that app will only be available for users that run Android SDK 19 or higher. + +2. To enable hybrid composition, set `WebView.platform = SurfaceAndroidWebView();` in `initState()`. For example: ```dart @@ -37,9 +47,15 @@ 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(); } @@ -52,13 +68,7 @@ class WebViewExample extends StatefulWidget { } ``` -`SurfaceAndroidWebView()` requires [API level 19](https://developer.android.com/studio/releases/platforms?hl=th#4.4). The plugin itself doesn't enforce the API level, so if you want to make the app available on devices running this API level or above, add the following to `/android/app/build.gradle`: +#### Enable Material Components for Android -```gradle -android { - defaultConfig { - // Required by the Flutter WebView plugin. - minSdkVersion 19 - } - } -``` +To use Material Components when the user interacts with input elements in the WebView, +follow the steps described in the [Enabling Material Components instructions](https://flutter.dev/docs/deployment/android#enabling-material-components). diff --git a/packages/webview_flutter/analysis_options.yaml b/packages/webview_flutter/analysis_options.yaml new file mode 100644 index 000000000000..cda4f6e153e6 --- /dev/null +++ b/packages/webview_flutter/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options_legacy.yaml diff --git a/packages/webview_flutter/android/build.gradle b/packages/webview_flutter/android/build.gradle index 893badc0e175..0ad39b773746 100644 --- a/packages/webview_flutter/android/build.gradle +++ b/packages/webview_flutter/android/build.gradle @@ -4,7 +4,7 @@ version '1.0-SNAPSHOT' buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -15,19 +15,20 @@ buildscript { rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } apply plugin: 'com.android.library' android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { - minSdkVersion 16 + minSdkVersion 19 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } + lintOptions { disable 'InvalidPackage' } diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java index 1273e7349620..31e3fe08c057 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java @@ -1,3 +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. + package io.flutter.plugins.webviewflutter; import static android.hardware.display.DisplayManager.DisplayListener; diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java index 86b4fd412a29..df3f21daadeb 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java index 9bec8fa0ef13..ebc7c31987f4 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -29,7 +29,7 @@ public class FlutterWebView implements PlatformView, MethodCallHandler { private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames"; - private final InputAwareWebView webView; + private final WebView webView; private final MethodChannel methodChannel; private final FlutterWebViewClient flutterWebViewClient; private final Handler platformThreadHandler; @@ -72,6 +72,11 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) { return true; } + + @Override + public void onProgressChanged(WebView view, int progress) { + flutterWebViewClient.onLoadingProgress(progress); + } } @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) @@ -87,7 +92,13 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) { DisplayManager displayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); displayListenerProxy.onPreWebViewInitialization(displayManager); - webView = new InputAwareWebView(context, containerView); + + Boolean usesHybridComposition = (Boolean) params.get("usesHybridComposition"); + webView = + (usesHybridComposition) + ? new WebView(context) + : new InputAwareWebView(context, containerView); + displayListenerProxy.onPostWebViewInitialization(displayManager); platformThreadHandler = new Handler(context.getMainLooper()); @@ -103,13 +114,16 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) { methodChannel.setMethodCallHandler(this); flutterWebViewClient = new FlutterWebViewClient(methodChannel); - applySettings((Map) params.get("settings")); + Map settings = (Map) params.get("settings"); + if (settings != null) applySettings(settings); if (params.containsKey(JS_CHANNEL_NAMES_FIELD)) { - registerJavaScriptChannelNames((List) params.get(JS_CHANNEL_NAMES_FIELD)); + List names = (List) params.get(JS_CHANNEL_NAMES_FIELD); + if (names != null) registerJavaScriptChannelNames(names); } - updateAutoMediaPlaybackPolicy((Integer) params.get("autoMediaPlaybackPolicy")); + Integer autoMediaPlaybackPolicy = (Integer) params.get("autoMediaPlaybackPolicy"); + if (autoMediaPlaybackPolicy != null) updateAutoMediaPlaybackPolicy(autoMediaPlaybackPolicy); if (params.containsKey("userAgent")) { String userAgent = (String) params.get("userAgent"); updateUserAgent(userAgent); @@ -132,7 +146,9 @@ public View getView() { // 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() { - webView.unlockInputConnection(); + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).unlockInputConnection(); + } } // @Override @@ -142,7 +158,9 @@ public void onInputConnectionUnlocked() { // 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() { - webView.lockInputConnection(); + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).lockInputConnection(); + } } // @Override @@ -152,7 +170,9 @@ public void onInputConnectionLocked() { // 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) { - webView.setContainerView(flutterView); + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).setContainerView(flutterView); + } } // @Override @@ -162,7 +182,9 @@ public void onFlutterViewAttached(View flutterView) { // 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() { - webView.setContainerView(null); + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).setContainerView(null); + } } @Override @@ -316,7 +338,7 @@ private void getTitle(Result result) { } private void scrollTo(MethodCall methodCall, Result result) { - Map request = (Map) methodCall.arguments; + Map request = methodCall.arguments(); int x = (int) request.get("x"); int y = (int) request.get("y"); @@ -326,7 +348,7 @@ private void scrollTo(MethodCall methodCall, Result result) { } private void scrollBy(MethodCall methodCall, Result result) { - Map request = (Map) methodCall.arguments; + Map request = methodCall.arguments(); int x = (int) request.get("x"); int y = (int) request.get("y"); @@ -346,7 +368,8 @@ private void applySettings(Map settings) { for (String key : settings.keySet()) { switch (key) { case "jsMode": - updateJsMode((Integer) settings.get(key)); + Integer mode = (Integer) settings.get(key); + if (mode != null) updateJsMode(mode); break; case "hasNavigationDelegate": final boolean hasNavigationDelegate = (boolean) settings.get(key); @@ -359,13 +382,21 @@ private void applySettings(Map settings) { case "debuggingEnabled": final boolean debuggingEnabled = (boolean) settings.get(key); - webView.setWebContentsDebuggingEnabled(debuggingEnabled); + 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); } @@ -389,7 +420,9 @@ 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; - webView.getSettings().setMediaPlaybackRequiresUserGesture(requireUserGesture); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + webView.getSettings().setMediaPlaybackRequiresUserGesture(requireUserGesture); + } } private void registerJavaScriptChannelNames(List channelNames) { @@ -406,7 +439,9 @@ private void updateUserAgent(String userAgent) { @Override public void dispose() { methodChannel.setMethodCallHandler(null); - webView.dispose(); + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).dispose(); + } webView.destroy(); } } diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java index 24926bfc4117..148be952db6e 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -30,6 +30,7 @@ class FlutterWebViewClient { private static final String TAG = "FlutterWebViewClient"; private final MethodChannel methodChannel; private boolean hasNavigationDelegate; + boolean hasProgressTracking; FlutterWebViewClient(MethodChannel methodChannel) { this.methodChannel = methodChannel; @@ -125,6 +126,14 @@ private void onPageFinished(WebView view, String url) { methodChannel.invokeMethod("onPageFinished", args); } + void onLoadingProgress(int progress) { + if (hasProgressTracking) { + Map args = new HashMap<>(); + args.put("progress", progress); + methodChannel.invokeMethod("onProgress", args); + } + } + private void onWebResourceError( final int errorCode, final String description, final String failingUrl) { final Map args = new HashMap<>(); diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java index 0aa2f58f743d..51b2a3809fff 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -222,9 +222,9 @@ && isCalledFromListPopupWindowShow() private boolean isCalledFromListPopupWindowShow() { StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace(); - for (int i = 0; i < stackTraceElements.length; i++) { - if (stackTraceElements[i].getClassName().equals(ListPopupWindow.class.getCanonicalName()) - && stackTraceElements[i].getMethodName().equals("show")) { + for (StackTraceElement stackTraceElement : stackTraceElements) { + if (stackTraceElement.getClassName().equals(ListPopupWindow.class.getCanonicalName()) + && stackTraceElement.getMethodName().equals("show")) { return true; } } diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java index f23aae5b2b69..4d596351b3d0 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java index 8fbdfaff1a6d..1c865c9444e2 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java @@ -1,4 +1,4 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFactory.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFactory.java index 6fdc36fbe545..22de668e0126 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFactory.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFactory.java @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter 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/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java index 5ed2da3046c3..dc329e2273d0 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java +++ b/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java @@ -1,4 +1,4 @@ -// Copyright 2018 The Chromium Authors. All rights reserved. +// Copyright 2013 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -54,9 +54,7 @@ public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registra public void onAttachedToEngine(FlutterPluginBinding binding) { BinaryMessenger messenger = binding.getBinaryMessenger(); binding - .getFlutterEngine() - .getPlatformViewsController() - .getRegistry() + .getPlatformViewRegistry() .registerViewFactory( "plugins.flutter.io/webview", new WebViewFactory(messenger, /*containerView=*/ null)); flutterCookieManager = new FlutterCookieManager(messenger); diff --git a/packages/webview_flutter/example/README.md b/packages/webview_flutter/example/README.md index bf2f819e87b3..850ee74397a9 100644 --- a/packages/webview_flutter/example/README.md +++ b/packages/webview_flutter/example/README.md @@ -5,4 +5,4 @@ Demonstrates how to use the webview_flutter plugin. ## Getting Started For help getting started with Flutter, view our online -[documentation](https://flutter.io/). +[documentation](https://flutter.dev/). diff --git a/packages/webview_flutter/example/android/app/build.gradle b/packages/webview_flutter/example/android/app/build.gradle index 706d501c4060..47eb97623747 100644 --- a/packages/webview_flutter/example/android/app/build.gradle +++ b/packages/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 28 + compileSdkVersion 29 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 16 + minSdkVersion 19 targetSdkVersion 28 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/packages/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/EmbeddingV1ActivityTest.java b/packages/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/EmbeddingV1ActivityTest.java index 07f9bf2f7802..56691d2fc82a 100644 --- a/packages/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/EmbeddingV1ActivityTest.java +++ b/packages/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/EmbeddingV1ActivityTest.java @@ -1,3 +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. + package io.flutter.plugins.webviewflutterexample; import androidx.test.rule.ActivityTestRule; diff --git a/packages/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java b/packages/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java index a9b1a9412cbd..b18308ab2feb 100644 --- a/packages/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java +++ b/packages/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java @@ -1,3 +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. + package io.flutter.plugins.webviewflutterexample; import androidx.test.rule.ActivityTestRule; diff --git a/packages/webview_flutter/example/android/app/src/main/AndroidManifest.xml b/packages/webview_flutter/example/android/app/src/main/AndroidManifest.xml index f895f92bd7a4..02f270fb9c49 100644 --- a/packages/webview_flutter/example/android/app/src/main/AndroidManifest.xml +++ b/packages/webview_flutter/example/android/app/src/main/AndroidManifest.xml @@ -14,11 +14,10 @@ android:name="flutterEmbedding" android:value="2" /> + + 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 new file mode 100644 index 000000000000..bcecab36d14a --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + 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 new file mode 100644 index 000000000000..b52123be65d4 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/java/io/flutter/plugins/wifi_info_flutter_example/MainActivity.java @@ -0,0 +1,9 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.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/main/res/drawable/launch_background.xml b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 000000000000..f74085f3f6a2 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/packages/quick_actions/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from packages/quick_actions/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/quick_actions/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from packages/quick_actions/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/quick_actions/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from packages/quick_actions/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/quick_actions/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from packages/quick_actions/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/quick_actions/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from packages/quick_actions/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/wifi_info_flutter/wifi_info_flutter/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/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 000000000000..449a9f930826 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/values/styles.xml b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 000000000000..d74aa35c2826 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + 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 new file mode 100644 index 000000000000..357602a1d503 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + 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 new file mode 100644 index 000000000000..456d020f6e2c --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/android/build.gradle @@ -0,0 +1,29 @@ +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.properties b/packages/wifi_info_flutter/wifi_info_flutter/example/android/gradle.properties new file mode 100644 index 000000000000..a6738207fd15 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx1536M +android.useAndroidX=true +android.enableJetifier=true +android.enableR8=true 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 new file mode 100644 index 000000000000..296b146b7318 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/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-5.6.2-all.zip diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/settings.gradle b/packages/wifi_info_flutter/wifi_info_flutter/example/android/settings.gradle new file mode 100644 index 000000000000..44e62bcf06ae --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/android/settings.gradle @@ -0,0 +1,11 @@ +include ':app' + +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() + +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/packages/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 new file mode 100644 index 000000000000..8190062e3ebd --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/integration_test/wifi_info_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 '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 new file mode 100644 index 000000000000..f2872cf474ee --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + 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/connectivity/connectivity_macos/example/ios/Flutter/Debug.xcconfig b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/connectivity/connectivity_macos/example/ios/Flutter/Debug.xcconfig rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/Debug.xcconfig diff --git a/packages/connectivity/connectivity_macos/example/ios/Flutter/Release.xcconfig b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/connectivity/connectivity_macos/example/ios/Flutter/Release.xcconfig rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/Release.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 new file mode 100644 index 000000000000..07a4e08abf54 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Podfile @@ -0,0 +1,44 @@ +# 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 new file mode 100644 index 000000000000..e0a688dfffad --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,496 @@ +// !$*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 */; }; +/* 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 = ""; }; + 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 = ( + ); + 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 */, + CF3B75C9A7D2FA2A4C99F110 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 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 = ( + 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"; + }; + 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/android_alarm_manager/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/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/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/espresso/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from packages/espresso/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/packages/android_intent/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/android_intent/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/wifi_info_flutter/wifi_info_flutter/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/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/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/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 000000000000..f9b0d7c5ea15 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/AppDelegate.h b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..0681d288bb70 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/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/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/AppDelegate.m b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..442514aaecbe --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/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. + +#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/in_app_purchase/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 similarity index 100% rename from packages/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png similarity index 100% rename from packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from packages/in_app_purchase/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from packages/in_app_purchase/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from packages/in_app_purchase/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from packages/in_app_purchase/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/packages/in_app_purchase/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from packages/in_app_purchase/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from packages/flutter_plugin_android_lifecycle/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/espresso/example/ios/Runner/Base.lproj/Main.storyboard b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/espresso/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Base.lproj/Main.storyboard 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 new file mode 100644 index 000000000000..2c7f6b8c6b85 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + 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 new file mode 100644 index 000000000000..f97b9ef5c8a1 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/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/wifi_info_flutter/wifi_info_flutter/example/lib/main.dart b/packages/wifi_info_flutter/wifi_info_flutter/example/lib/main.dart new file mode 100644 index 000000000000..8258815b0c09 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/lib/main.dart @@ -0,0 +1,173 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// 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 new file mode 100644 index 000000000000..6303907c32d6 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/pubspec.yaml @@ -0,0 +1,27 @@ +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/example/test_driver/integration_test.dart b/packages/wifi_info_flutter/wifi_info_flutter/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/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/espresso/ios/Assets/.gitkeep b/packages/wifi_info_flutter/wifi_info_flutter/ios/Assets/.gitkeep similarity index 100% rename from packages/espresso/ios/Assets/.gitkeep rename to packages/wifi_info_flutter/wifi_info_flutter/ios/Assets/.gitkeep 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 new file mode 100644 index 000000000000..359562b7761c --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/FLTWifiInfoLocationHandler.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. + +#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 new file mode 100644 index 000000000000..2fe19c5e70e9 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/FLTWifiInfoLocationHandler.m @@ -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 "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 new file mode 100644 index 000000000000..41f165717809 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/WifiInfoFlutterPlugin.h @@ -0,0 +1,8 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#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 new file mode 100644 index 000000000000..47bd90c4429b --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/ios/Classes/WifiInfoFlutterPlugin.m @@ -0,0 +1,134 @@ +// Copyright 2013 The Flutter Authors. 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 new file mode 100644 index 000000000000..c3b3416ad767 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/ios/wifi_info_flutter.podspec @@ -0,0 +1,24 @@ +# +# 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 new file mode 100644 index 000000000000..1c89ee82fc3e --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/lib/wifi_info_flutter.dart @@ -0,0 +1,148 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '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 new file mode 100644 index 000000000000..cbda364aa35e --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/pubspec.yaml @@ -0,0 +1,27 @@ +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 new file mode 100644 index 000000000000..93cf378de437 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/test/wifi_info_flutter_test.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: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 new file mode 100644 index 000000000000..a6d65e2f3343 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/.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: 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 new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/AUTHORS @@ -0,0 +1,66 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> diff --git a/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/CHANGELOG.md b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/CHANGELOG.md new file mode 100644 index 000000000000..34f8e84cd780 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/CHANGELOG.md @@ -0,0 +1,16 @@ +## 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/LICENSE b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/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/wifi_info_flutter/wifi_info_flutter_platform_interface/README.md b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/README.md new file mode 100644 index 000000000000..f2039c3d5865 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/README.md @@ -0,0 +1,26 @@ +# 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 new file mode 100644 index 000000000000..d5f05e6121a9 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/src/enums.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. + +/// 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 new file mode 100644 index 000000000000..79f27e8cde44 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/src/method_channel_wifi_info_flutter.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: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 new file mode 100644 index 000000000000..62330d4261a0 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/lib/wifi_info_flutter_platform_interface.dart @@ -0,0 +1,74 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +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 new file mode 100644 index 000000000000..14ca643aa045 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/pubspec.yaml @@ -0,0 +1,21 @@ +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 new file mode 100644 index 000000000000..875f2ab4089a --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter_platform_interface/test/method_channel_wifi_info_flutter_test.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 '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/build_all_plugins_app.sh b/script/build_all_plugins_app.sh index f1f68ddbe985..3b3416021a42 100755 --- a/script/build_all_plugins_app.sh +++ b/script/build_all_plugins_app.sh @@ -1,48 +1,47 @@ #!/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. + +# Usage: +# +# ./script/build_all_plugins_app.sh apk +# ./script/build_all_plugins_app.sh ios # This script builds the app in flutter/plugins/example/all_plugins to make # sure all first party plugins can be compiled together. # So that users can run this script from anywhere and it will work as expected. readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" > /dev/null && pwd)" + readonly REPO_DIR="$(dirname "$SCRIPT_DIR")" source "$SCRIPT_DIR/common.sh" -check_changed_packages > /dev/null +# This list should be kept as short as possible, and things should remain here +# only as long as necessary, since in general the goal is for all of the latest +# versions of plugins to be mutually compatible. +# +# An example use case for this list would be to temporarily add plugins while +# updating multiple plugins for a breaking change in a common dependency in +# cases where using a relaxed version constraint isn't possible. readonly EXCLUDED_PLUGINS_LIST=( - "connectivity_macos" - "connectivity_platform_interface" - "connectivity_web" - "extension_google_sign_in_as_googleapis_auth" - "flutter_plugin_android_lifecycle" - "google_maps_flutter_platform_interface" - "google_maps_flutter_web" - "google_sign_in_platform_interface" - "google_sign_in_web" - "image_picker_platform_interface" - "instrumentation_adapter" - "path_provider_linux" - "path_provider_macos" - "path_provider_platform_interface" - "path_provider_web" - "plugin_platform_interface" - "shared_preferences_linux" - "shared_preferences_macos" - "shared_preferences_platform_interface" - "shared_preferences_web" - "shared_preferences_windows" - "url_launcher_linux" - "url_launcher_macos" - "url_launcher_platform_interface" - "url_launcher_web" - "video_player_platform_interface" - "video_player_web" + "plugin_platform_interface" # This should never be a direct app dependency. ) # Comma-separated string of the list above readonly EXCLUDED=$(IFS=, ; echo "${EXCLUDED_PLUGINS_LIST[*]}") -(cd "$REPO_DIR" && pub global run flutter_plugin_tools all-plugins-app --exclude $EXCLUDED) +ALL_EXCLUDED=($EXCLUDED) + +echo "Excluding the following plugins: $ALL_EXCLUDED" + +(cd "$REPO_DIR" && plugin_tools all-plugins-app --exclude $ALL_EXCLUDED) + +# Master now creates null-safe app code by default; migrate stable so both +# branches are building in the same mode. +if [[ "${CHANNEL}" == "stable" ]]; then + (cd $REPO_DIR/all_plugins && dart migrate --apply-changes) +fi function error() { echo "$@" 1>&2 @@ -50,23 +49,22 @@ function error() { failures=0 -for version in "debug" "release"; do +BUILD_MODES=("debug" "release") +# Web doesn't support --debug for builds. +if [[ "$1" == "web" ]]; then + BUILD_MODES=("release") +fi + +for version in "${BUILD_MODES[@]}"; do + echo "Building $version..." (cd $REPO_DIR/all_plugins && flutter build $@ --$version) if [ $? -eq 0 ]; then echo "Successfully built $version all_plugins app." - echo "All first party plugins compile together." + echo "All first-party plugins compile together." else error "Failed to build $version all_plugins app." - if [[ "${#CHANGED_PACKAGE_LIST[@]}" == 0 ]]; then - error "There was a failure to compile all first party plugins together, but there were no changes detected in packages." - else - error "Changes to the following packages may prevent all first party plugins from compiling together:" - for package in "${CHANGED_PACKAGE_LIST[@]}"; do - error "$package" - done - echo "" - fi + error "This indicates a conflict between two or more first-party plugins." failures=$(($failures + 1)) fi done diff --git a/script/check_publish.sh b/script/check_publish.sh deleted file mode 100755 index 2e53fc80cb47..000000000000 --- a/script/check_publish.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -set -e - -# This script checks to make sure that each of the plugins *could* be published. -# It doesn't actually publish anything. - -# So that users can run this script from anywhere and it will work as expected. -readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -readonly REPO_DIR="$(dirname "$SCRIPT_DIR")" - -source "$SCRIPT_DIR/common.sh" - -function check_publish() { - local failures=() - for dir in $(pub global run flutter_plugin_tools list --plugins="$1"); do - local package_name=$(basename "$dir") - - echo "Checking that $package_name can be published." - if [[ $(cd "$dir" && cat pubspec.yaml | grep -E "^publish_to: none") ]]; then - echo "Package $package_name is marked as unpublishable. Skipping." - elif (cd "$dir" && flutter pub publish -- --dry-run > /dev/null); then - echo "Package $package_name is able to be published." - else - error "Unable to publish $package_name" - failures=("${failures[@]}" "$package_name") - fi - done - if [[ "${#failures[@]}" != 0 ]]; then - error "FAIL: The following ${#failures[@]} package(s) failed the publishing check:" - for failure in "${failures[@]}"; do - error "$failure" - done - fi - return "${#failures[@]}" -} - -# Sets CHANGED_PACKAGE_LIST and CHANGED_PACKAGES -check_changed_packages - -if [[ "${#CHANGED_PACKAGE_LIST[@]}" != 0 ]]; then - check_publish "${CHANGED_PACKAGES}" -fi diff --git a/script/common.sh b/script/common.sh index 7950a3ea71cd..11eb64101f2b 100644 --- a/script/common.sh +++ b/script/common.sh @@ -1,47 +1,14 @@ #!/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. function error() { echo "$@" 1>&2 } -function get_branch_base_sha() { - local branch_base_sha="$(git merge-base --fork-point FETCH_HEAD HEAD || git merge-base FETCH_HEAD HEAD)" - echo "$branch_base_sha" -} - -function check_changed_packages() { - # Try get a merge base for the branch and calculate affected packages. - # We need this check because some CIs can do a single branch clones with a limited history of commits. - local packages - local branch_base_sha="$(get_branch_base_sha)" - if [[ "$branch_base_sha" != "" ]]; then - echo "Checking for changed packages from $branch_base_sha" - IFS=$'\n' packages=( $(git diff --name-only "$branch_base_sha" HEAD | grep -o "packages/[^/]*" | sed -e "s/packages\///g" | sort | uniq) ) - else - error "Cannot find a merge base for the current branch to run an incremental build..." - error "Please rebase your branch onto the latest master!" - return 1 - fi - - CHANGED_PACKAGES="" - CHANGED_PACKAGE_LIST=() - - # Filter out packages that have been deleted. - for package in "${packages[@]}"; do - if [ -d "$REPO_DIR/packages/$package" ]; then - CHANGED_PACKAGES="${CHANGED_PACKAGES},$package" - CHANGED_PACKAGE_LIST=("${CHANGED_PACKAGE_LIST[@]}" "$package") - fi - done - - if [[ "${#CHANGED_PACKAGE_LIST[@]}" == 0 ]]; then - echo "No changes detected in packages." - else - echo "Detected changes in the following ${#CHANGED_PACKAGE_LIST[@]} package(s):" - for package in "${CHANGED_PACKAGE_LIST[@]}"; do - echo "$package" - done - echo "" - fi - return 0 +# Runs the plugin tools from the plugin_tools git submodule. +function plugin_tools() { + (pushd "$REPO_DIR/script/tool" && dart pub get && popd) >/dev/null + dart run "$REPO_DIR/script/tool/bin/flutter_plugin_tools.dart" "$@" } diff --git a/script/incremental_build.sh b/script/incremental_build.sh deleted file mode 100755 index 30c166b4c666..000000000000 --- a/script/incremental_build.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/bin/bash -set -e - -readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" -readonly REPO_DIR="$(dirname "$SCRIPT_DIR")" - -source "$SCRIPT_DIR/common.sh" - -if [ "$(expr substr $(uname -s) 1 5)" == "MINGW" ]; then - PUB=pub.bat -else - PUB=pub -fi - -# Plugins that deliberately use their own analysis_options.yaml. -# -# This list should only be deleted from, never added to. This only exists -# because we adopted stricter analysis rules recently and needed to exclude -# already failing packages to start linting the repo as a whole. -# -# TODO(mklim): Remove everything from this list. https://github.com/flutter/flutter/issues/45440 -CUSTOM_ANALYSIS_PLUGINS=( - "in_app_purchase" - "camera" - "video_player/video_player_web" - "google_maps_flutter/google_maps_flutter_web" -) -# Comma-separated string of the list above -readonly CUSTOM_FLAG=$(IFS=, ; echo "${CUSTOM_ANALYSIS_PLUGINS[*]}") -# Set some default actions if run without arguments. -ACTIONS=("$@") -if [[ "${#ACTIONS[@]}" == 0 ]]; then - ACTIONS=("analyze" "--custom-analysis" "$CUSTOM_FLAG" "test" "java-test") -elif [[ "${ACTIONS[@]}" == "analyze" ]]; then - ACTIONS=("analyze" "--custom-analysis" "$CUSTOM_FLAG") -fi - -BRANCH_NAME="${BRANCH_NAME:-"$(git rev-parse --abbrev-ref HEAD)"}" - -# This has to be turned into a list and then split out to the command line, -# otherwise it gets treated as a single argument. -PLUGIN_SHARDING=($PLUGIN_SHARDING) - -if [[ "${BRANCH_NAME}" == "master" ]]; then - echo "Running for all packages" - (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools "${ACTIONS[@]}" ${PLUGIN_SHARDING[@]}) -else - # Sets CHANGED_PACKAGES - check_changed_packages - - if [[ "$CHANGED_PACKAGES" == "" ]]; then - echo "No changes detected in packages." - echo "Running for all packages" - (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools "${ACTIONS[@]}" ${PLUGIN_SHARDING[@]}) - else - echo running "${ACTIONS[@]}" - (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools "${ACTIONS[@]}" --plugins="$CHANGED_PACKAGES" ${PLUGIN_SHARDING[@]}) - echo "Running version check for changed packages" - (cd "$REPO_DIR" && $PUB global run flutter_plugin_tools version-check --base_sha="$(get_branch_base_sha)") - fi -fi diff --git a/script/tool/CHANGELOG.md b/script/tool/CHANGELOG.md new file mode 100644 index 000000000000..ae81ced63662 --- /dev/null +++ b/script/tool/CHANGELOG.md @@ -0,0 +1,346 @@ +## NEXT + +- Add a --build-id flag to `firebase-test-lab` instead of hard-coding the use of + `CIRRUS_BUILD_ID`. `CIRRUS_BUILD_ID` is the default value for that flag, for backward + compatibility. +- `xctest` now supports running macOS tests in addition to iOS + - **Breaking change**: it now requires an `--ios` and/or `--macos` flag. +- The tooling now runs in strong null-safe mode. + +## 0.2.0 + +- Remove `xctest`'s `--skip`, which is redundant with `--ignore`. + +## 0.1.4 + +- Add a `pubspec-check` command + +## 0.1.3 + +- Cosmetic fix to `publish-check` output +- Add a --dart-sdk option to `analyze` +- Allow reverts in `version-check` + +## 0.1.2 + +- Add `against-pub` flag for version-check, which allows the command to check version with pub. +- Add `machine` flag for publish-check, which replaces outputs to something parsable by machines. +- Add `skip-conformation` flag to publish-plugin to allow auto publishing. +- Change `run-on-changed-packages` to consider all packages as changed if any + files have been changed that could affect the entire repository. + +## 0.1.1 + +- Update the allowed third-party licenses for flutter/packages. + +## 0.1.0+1 + +- Re-add the bin/ directory. + +## 0.1.0 + +- **NOTE**: This is no longer intended as a general-purpose package, and is now + supported only for flutter/plugins and flutter/tools. +- Fix version checks + - Remove handling of pre-release null-safe versions +- Fix build all for null-safe template apps +- Improve handling of web integration tests +- Supports enforcing standardized copyright files +- Improve handling of iOS tests + +## v.0.0.45+3 + +- Pin `collection` to `1.14.13` to be able to target Flutter stable (v1.22.6). + +## v.0.0.45+2 + +- Make `publish-plugin` to work on non-flutter packages. + +## v.0.0.45+1 + +- Don't call `flutter format` if there are no Dart files to format. + +## v.0.0.45 + +- Add exclude flag to exclude any plugin from further processing. + +## v.0.0.44+7 + +- `all-plugins-app` doesn't override the AGP version. + +## v.0.0.44+6 + +- Fix code formatting. + +## v.0.0.44+5 + +- Remove `-v` flag on drive-examples. + +## v.0.0.44+4 + +- Fix bug where directory isn't passed + +## v.0.0.44+3 + +- More verbose logging + +## v.0.0.44+2 + +- Remove pre-alpha Windows workaround to create examples on the fly. + +## v.0.0.44+1 + +- Print packages that passed tests in `xctest` command. +- Remove printing the whole list of simulators. + +## v.0.0.44 + +- Add 'xctest' command to run xctests. + +## v.0.0.43 + +- Allow minor `*-nullsafety` pre release packages. + +## v.0.0.42+1 + +- Fix test command when `--enable-experiment` is called. + +## v.0.0.42 + +- Allow `*-nullsafety` pre release packages. + +## v.0.0.41 + +- Support `--enable-experiment` flag in subcommands `test`, `build-examples`, `drive-examples`, +and `firebase-test-lab`. + +## v.0.0.40 + +- Support `integration_test/` directory for `drive-examples` command + +## v.0.0.39 + +- Support `integration_test/` directory for `package:integration_test` + +## v.0.0.38 + +- Add C++ and ObjC++ to clang-format. + +## v.0.0.37+2 + +- Make `http` and `http_multi_server` dependency version constraint more flexible. + +## v.0.0.37+1 + +- All_plugin test puts the plugin dependencies into dependency_overrides. + +## v.0.0.37 + +- Only builds mobile example apps when necessary. + +## v.0.0.36+3 + +- Add support for Linux plugins. + +## v.0.0.36+2 + +- Default to showing podspec lint warnings + +## v.0.0.36+1 + +- Serialize linting podspecs. + +## v.0.0.36 + +- Remove retry on Firebase Test Lab's call to gcloud set. +- Remove quiet flag from Firebase Test Lab's gcloud set command. +- Allow Firebase Test Lab command to continue past gcloud set network failures. + This is a mitigation for the network service sometimes not responding, + but it isn't actually necessary to have a network connection for this command. + +## v.0.0.35+1 + +- Minor cleanup to the analyze test. + +## v.0.0.35 + +- Firebase Test Lab command generates a configurable unique path suffix for results. + +## v.0.0.34 + +- Firebase Test Lab command now only tries to configure the project once +- Firebase Test Lab command now retries project configuration up to five times. + +## v.0.0.33+1 + +- Fixes formatting issues that got past our CI due to + https://github.com/flutter/flutter/issues/51585. +- Changes the default package name for testing method `createFakePubspec` back + its previous behavior. + +## v.0.0.33 + +- Version check command now fails on breaking changes to platform interfaces. +- Updated version check test to be more flexible. + +## v.0.0.32+7 + +- Ensure that Firebase Test Lab tests have a unique storage bucket for each test run. + +## v.0.0.32+6 + +- Ensure that Firebase Test Lab tests have a unique storage bucket for each package. + +## v.0.0.32+5 + +- Remove --fail-fast and --silent from lint podspec command. + +## v.0.0.32+4 + +- Update `publish-plugin` to use `flutter pub publish` instead of just `pub + publish`. Enforces a `pub publish` command that matches the Dart SDK in the + user's Flutter install. + +## v.0.0.32+3 + +- Update Firebase Testlab deprecated test device. (Pixel 3 API 28 -> Pixel 4 API 29). + +## v.0.0.32+2 + +- Runs pub get before building macos to avoid failures. + +## v.0.0.32+1 + +- Default macOS example builds to false. Previously they were running whenever + CI was itself running on macOS. + +## v.0.0.32 + +- `analyze` now asserts that the global `analysis_options.yaml` is the only one + by default. Individual directories can be excluded from this check with the + new `--custom-analysis` flag. + +## v.0.0.31+1 + +- Add --skip and --no-analyze flags to podspec command. + +## v.0.0.31 + +- Add support for macos on `DriveExamplesCommand` and `BuildExamplesCommand`. + +## v.0.0.30 + +- Adopt pedantic analysis options, fix firebase_test_lab_test. + +## v.0.0.29 + +- Add a command to run pod lib lint on podspec files. + +## v.0.0.28 + +- Increase Firebase test lab timeouts to 5 minutes. + +## v.0.0.27 + +- Run tests with `--platform=chrome` for web plugins. + +## v.0.0.26 + +- Add a command for publishing plugins to pub. + +## v.0.0.25 + +- Update `DriveExamplesCommand` to use `ProcessRunner`. +- Make `DriveExamplesCommand` rely on `ProcessRunner` to determine if the test fails or not. +- Add simple tests for `DriveExamplesCommand`. + +## v.0.0.24 + +- Gracefully handle pubspec.yaml files for new plugins. +- Additional unit testing. + +## v.0.0.23 + +- Add a test case for transitive dependency solving in the + `create_all_plugins_app` command. + +## v.0.0.22 + +- Updated firebase-test-lab command with updated conventions for test locations. +- Updated firebase-test-lab to add an optional "device" argument. +- Updated version-check command to always compare refs instead of using the working copy. +- Added unit tests for the firebase-test-lab and version-check commands. +- Add ProcessRunner to mock running processes for testing. + +## v.0.0.21 + +- Support the `--plugins` argument for federated plugins. + +## v.0.0.20 + +- Support for finding federated plugins, where one directory contains + multiple packages for different platform implementations. + +## v.0.0.19+3 + +- Use `package:file` for file I/O. + +## v.0.0.19+2 + +- Use java as language when calling `flutter create`. + +## v.0.0.19+1 + +- Rename command for `CreateAllPluginsAppCommand`. + +## v.0.0.19 + +- Use flutter create to build app testing plugin compilation. + +## v.0.0.18+2 + +- Fix `.travis.yml` file name in `README.md`. + +## v0.0.18+1 + +- Skip version check if it contains `publish_to: none`. + +## v0.0.18 + +- Add option to exclude packages from generated pubspec command. + +## v0.0.17+4 + +- Avoid trying to version-check pubspecs that are missing a version. + +## v0.0.17+3 + +- version-check accounts for [pre-1.0 patch versions](https://github.com/flutter/flutter/issues/35412). + +## v0.0.17+2 + +- Fix exception handling for version checker + +## v0.0.17+1 + +- Fix bug where we used a flag instead of an option + +## v0.0.17 + +- Add a command for checking the version number + +## v0.0.16 + +- Add a command for generating `pubspec.yaml` for All Plugins app. + +## v0.0.15 + +- Add a command for running driver tests of plugin examples. + +## v0.0.14 + +- Check for dependencies->flutter instead of top level flutter node. + +## v0.0.13 + +- Differentiate between Flutter and non-Flutter (but potentially Flutter consumed) Dart packages. diff --git a/script/tool/LICENSE b/script/tool/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/script/tool/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/script/tool/README.md b/script/tool/README.md new file mode 100644 index 000000000000..c0bcd7c5e102 --- /dev/null +++ b/script/tool/README.md @@ -0,0 +1,119 @@ +# Flutter Plugin Tools + +This is a set of utilities used in the flutter/plugins and flutter/packages +repositories. It is no longer explictily maintained as a general-purpose tool +for multi-package repositories, so your mileage may vary if using it in other +repositories. + +Note: The commands in tools are designed to run at the root of the repository or `/packages/`. + +## Getting Started + +In flutter/plugins, the tool is run from source. In flutter/packages, the +[published version](https://pub.dev/packages/flutter_plugin_tools) is used +instead. (It is marked as Discontinued since it is no longer maintained as +a general-purpose tool, but updates are still published for use in +flutter/packages.) + +### From Source (flutter/plugins only) + +Set up: + +```sh +cd ./script/tool && dart pub get && cd ../../ +``` + +Run: + +```sh +dart run ./script/tool/lib/src/main.dart +``` + +### Published Version + +Set up: + +```sh +dart pub global activate flutter_plugin_tools +``` + +Run: + +```sh +dart pub global run flutter_plugin_tools +``` + +## Commands + +Run with `--help` for a full list of commands and arguments, but the +following shows a number of common commands being run for a specific 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.) + +### Format Code + +```sh +cd +dart run ./script/tool/lib/src/main.dart format --plugins plugin_name +``` + +### Run the Dart Static Analyzer + +```sh +cd +dart run ./script/tool/lib/src/main.dart analyze --plugins plugin_name +``` + +### Run Dart Unit Tests + +```sh +cd +dart run ./script/tool/lib/src/main.dart test --plugins plugin_name +``` + +### Run XCTests + +```sh +cd +# For iOS: +dart run ./script/tool/lib/src/main.dart xctest --ios --plugins plugin_name +# For macOS: +dart run ./script/tool/lib/src/main.dart xctest --macos --plugins plugin_name +``` + +### Publish a Release + +``sh +cd +git checkout +dart run ./script/tool/lib/src/main.dart publish-plugin --package +`` + +By default the tool tries to push tags to the `upstream` remote, but some +additional settings can be configured. Run `dart run ./script/tool/lib/src/main.dart publish-plugin --help` for more usage information. + +The tool wraps `pub publish` for pushing the package to pub, and then will +automatically use git to try to create and push tags. It has some additional +safety checking around `pub publish` too. By default `pub publish` publishes +_everything_, including untracked or uncommitted files in version control. +`publish-plugin` will first check the status of the local +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. + +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) diff --git a/script/tool/bin/flutter_plugin_tools.dart b/script/tool/bin/flutter_plugin_tools.dart new file mode 100644 index 000000000000..0f30bee0d258 --- /dev/null +++ b/script/tool/bin/flutter_plugin_tools.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 'package:flutter_plugin_tools/src/main.dart'; diff --git a/script/tool/lib/src/analyze_command.dart b/script/tool/lib/src/analyze_command.dart new file mode 100644 index 000000000000..076c8f69885d --- /dev/null +++ b/script/tool/lib/src/analyze_command.dart @@ -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 'dart:async'; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +/// A command to run Dart analysis on packages. +class AnalyzeCommand extends PluginCommand { + /// Creates a analysis command instance. + AnalyzeCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, processRunner: processRunner) { + argParser.addMultiOption(_customAnalysisFlag, + help: + 'Directories (comma separated) that are allowed to have their own analysis options.', + defaultsTo: []); + argParser.addOption(_analysisSdk, + valueHelp: 'dart-sdk', + help: 'An optional path to a Dart SDK; this is used to override the ' + 'SDK used to provide analysis.'); + } + + static const String _customAnalysisFlag = 'custom-analysis'; + + static const String _analysisSdk = 'analysis-sdk'; + + @override + final String name = 'analyze'; + + @override + final String description = 'Analyzes all packages using dart analyze.\n\n' + 'This command requires "dart" and "flutter" to be in your path.'; + + @override + Future run() async { + print('Verifying analysis settings...'); + + final List files = packagesDir.listSync(recursive: true); + for (final FileSystemEntity file in files) { + if (file.basename != 'analysis_options.yaml' && + file.basename != '.analysis_options') { + continue; + } + + final bool allowed = (getStringListArg(_customAnalysisFlag)).any( + (String directory) => + directory != null && + directory.isNotEmpty && + p.isWithin(p.join(packagesDir.path, directory), file.path)); + if (allowed) { + continue; + } + + print('Found an extra analysis_options.yaml in ${file.absolute.path}.'); + print( + 'If this was deliberate, pass the package to the analyze command with the --$_customAnalysisFlag flag and try again.'); + throw ToolExit(1); + } + + final List packageDirectories = await getPackages().toList(); + final Set packagePaths = + packageDirectories.map((Directory dir) => dir.path).toSet(); + packageDirectories.removeWhere((Directory directory) { + // We remove the 'example' subdirectories - 'flutter pub 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) { + await processRunner.runAndStream('flutter', ['packages', 'get'], + workingDir: package, exitOnError: true); + } + + // Use the Dart SDK override if one was passed in. + final String? dartSdk = argResults![_analysisSdk] as String?; + final String dartBinary = + dartSdk == null ? 'dart' : p.join(dartSdk, 'bin', 'dart'); + + final List failingPackages = []; + final List pluginDirectories = await getPlugins().toList(); + for (final Directory package in pluginDirectories) { + final int exitCode = await processRunner.runAndStream( + dartBinary, ['analyze', '--fatal-infos'], + workingDir: package); + if (exitCode != 0) { + failingPackages.add(p.basename(package.path)); + } + } + + print('\n\n'); + + if (failingPackages.isNotEmpty) { + print('The following packages have analyzer errors (see above):'); + for (final String package in failingPackages) { + print(' * $package'); + } + throw ToolExit(1); + } + + print('No analyzer errors found!'); + } +} diff --git a/script/tool/lib/src/build_examples_command.dart b/script/tool/lib/src/build_examples_command.dart new file mode 100644 index 000000000000..9590aecef98e --- /dev/null +++ b/script/tool/lib/src/build_examples_command.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 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; + +import 'common.dart'; + +/// Key for IPA. +const String kIpa = 'ipa'; + +/// Key for APK. +const String kApk = 'apk'; + +/// A command to build the example applications for packages. +class BuildExamplesCommand extends PluginCommand { + /// Creates an instance of the build command. + BuildExamplesCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, processRunner: processRunner) { + argParser.addFlag(kPlatformFlagLinux, defaultsTo: false); + argParser.addFlag(kPlatformFlagMacos, defaultsTo: false); + argParser.addFlag(kPlatformFlagWeb, defaultsTo: false); + argParser.addFlag(kPlatformFlagWindows, defaultsTo: false); + argParser.addFlag(kIpa, defaultsTo: io.Platform.isMacOS); + argParser.addFlag(kApk); + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: 'Enables the given Dart SDK experiments.', + ); + } + + @override + final String name = 'build-examples'; + + @override + final String description = + 'Builds all example apps (IPA for iOS and APK for Android).\n\n' + 'This command requires "flutter" to be in your path.'; + + @override + Future run() async { + final List platformSwitches = [ + kApk, + kIpa, + kPlatformFlagLinux, + kPlatformFlagMacos, + kPlatformFlagWeb, + kPlatformFlagWindows, + ]; + if (!platformSwitches.any((String platform) => getBoolArg(platform))) { + print( + 'None of ${platformSwitches.map((String platform) => '--$platform').join(', ')} ' + 'were specified, so not building anything.'); + return; + } + final String flutterCommand = + const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + + final String enableExperiment = getStringArg(kEnableExperiment); + + final List failingPackages = []; + await for (final Directory plugin in getPlugins()) { + for (final Directory example in getExamplesForPlugin(plugin)) { + final String packageName = + p.relative(example.path, from: packagesDir.path); + + if (getBoolArg(kPlatformFlagLinux)) { + print('\nBUILDING Linux for $packageName'); + if (isLinuxPlugin(plugin)) { + final int buildExitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + kPlatformFlagLinux, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (buildExitCode != 0) { + failingPackages.add('$packageName (linux)'); + } + } else { + print('Linux is not supported by this plugin'); + } + } + + if (getBoolArg(kPlatformFlagMacos)) { + print('\nBUILDING macOS for $packageName'); + if (isMacOsPlugin(plugin)) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + kPlatformFlagMacos, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (exitCode != 0) { + failingPackages.add('$packageName (macos)'); + } + } else { + print('macOS is not supported by this plugin'); + } + } + + if (getBoolArg(kPlatformFlagWeb)) { + print('\nBUILDING web for $packageName'); + if (isWebPlugin(plugin)) { + final int buildExitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + kPlatformFlagWeb, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (buildExitCode != 0) { + failingPackages.add('$packageName (web)'); + } + } else { + print('Web is not supported by this plugin'); + } + } + + if (getBoolArg(kPlatformFlagWindows)) { + print('\nBUILDING Windows for $packageName'); + if (isWindowsPlugin(plugin)) { + final int buildExitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + kPlatformFlagWindows, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (buildExitCode != 0) { + failingPackages.add('$packageName (windows)'); + } + } else { + print('Windows is not supported by this plugin'); + } + } + + if (getBoolArg(kIpa)) { + print('\nBUILDING IPA for $packageName'); + if (isIosPlugin(plugin)) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + 'ios', + '--no-codesign', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (exitCode != 0) { + failingPackages.add('$packageName (ipa)'); + } + } else { + print('iOS is not supported by this plugin'); + } + } + + if (getBoolArg(kApk)) { + print('\nBUILDING APK for $packageName'); + if (isAndroidPlugin(plugin)) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + 'apk', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example); + if (exitCode != 0) { + failingPackages.add('$packageName (apk)'); + } + } else { + print('Android is not supported by this plugin'); + } + } + } + } + print('\n\n'); + + if (failingPackages.isNotEmpty) { + print('The following build are failing (see above for details):'); + for (final String package in failingPackages) { + print(' * $package'); + } + throw ToolExit(1); + } + + print('All builds successful!'); + } +} diff --git a/script/tool/lib/src/common.dart b/script/tool/lib/src/common.dart new file mode 100644 index 000000000000..5d653ad0ed20 --- /dev/null +++ b/script/tool/lib/src/common.dart @@ -0,0 +1,781 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; +import 'dart:math'; + +import 'package:args/command_runner.dart'; +import 'package:colorize/colorize.dart'; +import 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:http/http.dart' as http; +import 'package:path/path.dart' as p; +import 'package:pub_semver/pub_semver.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 windows platform. +const String kPlatformFlagWindows = 'windows'; + +/// Key for macos platform. +const String kPlatformFlagMacos = 'macos'; + +/// Key for linux platform. +const String kPlatformFlagLinux = 'linux'; + +/// Key for IPA (iOS) platform. +const String kPlatformFlagIos = 'ios'; + +/// Key for APK (Android) platform. +const String kPlatformFlagAndroid = 'android'; + +/// Key for Web platform. +const String kPlatformFlagWeb = 'web'; + +/// 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; + } + + 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 { + return false; + } +} + +/// Possible plugin support options for a platform. +enum PlatformSupport { + /// The platform has an implementation in the package. + inline, + + /// The platform has an endorsed federated implementation in another package. + federated, +} + +/// Returns whether the given directory contains a Flutter [platform] plugin. +/// +/// It checks this by looking for the following pattern in the pubspec: +/// +/// flutter: +/// plugin: +/// platforms: +/// [platform]: +/// +/// If [requiredMode] is provided, the plugin must have the given type of +/// implementation in order to return true. +bool pluginSupportsPlatform(String platform, FileSystemEntity entity, + {PlatformSupport? requiredMode}) { + assert(platform == kPlatformFlagIos || + platform == kPlatformFlagAndroid || + platform == kPlatformFlagWeb || + platform == kPlatformFlagMacos || + platform == kPlatformFlagWindows || + platform == kPlatformFlagLinux); + if (entity is! Directory) { + return false; + } + + try { + final File pubspecFile = entity.childFile('pubspec.yaml'); + final YamlMap pubspecYaml = + loadYaml(pubspecFile.readAsStringSync()) as YamlMap; + final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?; + if (flutterSection == null) { + return false; + } + final YamlMap? pluginSection = flutterSection['plugin'] as YamlMap?; + if (pluginSection == null) { + return false; + } + final YamlMap? platforms = pluginSection['platforms'] as YamlMap?; + if (platforms == null) { + // Legacy plugin specs are assumed to support iOS and Android. They are + // never federated. + if (requiredMode == PlatformSupport.federated) { + return false; + } + if (!pluginSection.containsKey('platforms')) { + return platform == kPlatformFlagIos || platform == kPlatformFlagAndroid; + } + return false; + } + final YamlMap? platformEntry = platforms[platform] as YamlMap?; + if (platformEntry == null) { + return false; + } + // If the platform entry is present, then it supports the platform. Check + // for required mode if specified. + final bool federated = platformEntry.containsKey('default_package'); + return requiredMode == null || + federated == (requiredMode == PlatformSupport.federated); + } on FileSystemException { + return false; + } on YamlException { + return false; + } +} + +/// Returns whether the given directory contains a Flutter Android plugin. +bool isAndroidPlugin(FileSystemEntity entity) { + return pluginSupportsPlatform(kPlatformFlagAndroid, entity); +} + +/// Returns whether the given directory contains a Flutter iOS plugin. +bool isIosPlugin(FileSystemEntity entity) { + return pluginSupportsPlatform(kPlatformFlagIos, entity); +} + +/// Returns whether the given directory contains a Flutter web plugin. +bool isWebPlugin(FileSystemEntity entity) { + return pluginSupportsPlatform(kPlatformFlagWeb, entity); +} + +/// Returns whether the given directory contains a Flutter Windows plugin. +bool isWindowsPlugin(FileSystemEntity entity) { + return pluginSupportsPlatform(kPlatformFlagWindows, entity); +} + +/// Returns whether the given directory contains a Flutter macOS plugin. +bool isMacOsPlugin(FileSystemEntity entity) { + return pluginSupportsPlatform(kPlatformFlagMacos, entity); +} + +/// Returns whether the given directory contains a Flutter linux plugin. +bool isLinuxPlugin(FileSystemEntity entity) { + return pluginSupportsPlatform(kPlatformFlagLinux, entity); +} + +/// Prints `errorMessage` in red. +void printError(String errorMessage) { + final Colorize redError = Colorize(errorMessage)..red(); + print(redError); +} + +/// Error thrown when a command needs to exit with a non-zero exit code. +class ToolExit extends Error { + /// Creates a tool exit with the given [exitCode]. + ToolExit(this.exitCode); + + /// The code that the process should exit with. + final int exitCode; +} + +/// Interface definition for all commands in this tool. +abstract class PluginCommand extends Command { + /// Creates a command to operate on [packagesDir] with the given environment. + PluginCommand( + this.packagesDir, { + this.processRunner = const ProcessRunner(), + this.gitDir, + }) { + argParser.addMultiOption( + _pluginsArg, + splitCommas: true, + help: + 'Specifies which plugins the command should run on (before sharding).', + valueHelp: 'plugin1,plugin2,...', + ); + argParser.addOption( + _shardIndexArg, + help: 'Specifies the zero-based index of the shard to ' + 'which the command applies.', + valueHelp: 'i', + defaultsTo: '0', + ); + argParser.addOption( + _shardCountArg, + help: 'Specifies the number of shards into which plugins are divided.', + valueHelp: 'n', + defaultsTo: '1', + ); + argParser.addMultiOption( + _excludeArg, + abbr: 'e', + help: 'Exclude packages from this command.', + defaultsTo: [], + ); + argParser.addFlag(_runOnChangedPackagesArg, + help: 'Run the command on changed packages/plugins.\n' + 'If the $_pluginsArg is specified, this flag is ignored.\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.'); + argParser.addOption(_kBaseSha, + 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.'); + } + + static const String _pluginsArg = 'plugins'; + static const String _shardIndexArg = 'shardIndex'; + static const String _shardCountArg = 'shardCount'; + static const String _excludeArg = 'exclude'; + static const String _runOnChangedPackagesArg = 'run-on-changed-packages'; + static const String _kBaseSha = 'base-sha'; + + /// The directory containing the plugin packages. + final Directory packagesDir; + + /// The process runner. + /// + /// This can be overridden for testing. + final ProcessRunner processRunner; + + /// The git directory to use. By default it uses the parent directory. + /// + /// This can be mocked for testing. + final GitDir? gitDir; + + int? _shardIndex; + int? _shardCount; + + /// The shard of the overall command execution that this instance should run. + int get shardIndex { + if (_shardIndex == null) { + _checkSharding(); + } + return _shardIndex!; + } + + /// The number of shards this command is divided into. + int get shardCount { + if (_shardCount == null) { + _checkSharding(); + } + return _shardCount!; + } + + /// Convenience accessor for boolean arguments. + bool getBoolArg(String key) { + return (argResults![key] as bool?) ?? false; + } + + /// Convenience accessor for String arguments. + String getStringArg(String key) { + return (argResults![key] as String?) ?? ''; + } + + /// Convenience accessor for List arguments. + List getStringListArg(String key) { + return (argResults![key] as List?) ?? []; + } + + void _checkSharding() { + final int? shardIndex = int.tryParse(getStringArg(_shardIndexArg)); + final int? shardCount = int.tryParse(getStringArg(_shardCountArg)); + if (shardIndex == null) { + usageException('$_shardIndexArg must be an integer'); + } + if (shardCount == null) { + usageException('$_shardCountArg must be an integer'); + } + if (shardCount < 1) { + usageException('$_shardCountArg must be positive'); + } + if (shardIndex < 0 || shardCount <= shardIndex) { + usageException( + '$_shardIndexArg must be in the half-open range [0..$shardCount['); + } + _shardIndex = shardIndex; + _shardCount = shardCount; + } + + /// Returns the root Dart package folders of the plugins involved in this + /// command execution. + Stream getPlugins() async* { + // To avoid assuming consistency of `Directory.list` across command + // invocations, we collect and sort the plugin folders before sharding. + // This is considered an implementation detail which is why the API still + // uses streams. + final List allPlugins = await _getAllPlugins().toList(); + allPlugins.sort((Directory d1, Directory d2) => d1.path.compareTo(d2.path)); + // Sharding 10 elements into 3 shards should yield shard sizes 4, 4, 2. + // Sharding 9 elements into 3 shards should yield shard sizes 3, 3, 3. + // Sharding 2 elements into 3 shards should yield shard sizes 1, 1, 0. + final int shardSize = allPlugins.length ~/ shardCount + + (allPlugins.length % shardCount == 0 ? 0 : 1); + final int start = min(shardIndex * shardSize, allPlugins.length); + final int end = min(start + shardSize, allPlugins.length); + + for (final Directory plugin in allPlugins.sublist(start, end)) { + yield plugin; + } + } + + /// Returns the root Dart package folders of the plugins involved in this + /// command execution, assuming there is only one shard. + /// + /// Plugin packages can exist in the following places relative to the packages + /// directory: + /// + /// 1. As a Dart package in a directory which is a direct child of the + /// packages directory. This is a plugin where all of the implementations + /// exist in a single Dart package. + /// 2. Several plugin packages may live in a directory which is a direct + /// child of the packages directory. This directory groups several Dart + /// packages which implement a single plugin. This directory contains a + /// "client library" package, which declares the API for the plugin, as + /// well as one or more platform-specific implementations. + /// 3./4. Either of the above, but in a third_party/packages/ directory that + /// is a sibling of the packages directory. This is used for a small number + /// of packages in the flutter/packages repository. + Stream _getAllPlugins() async* { + Set plugins = Set.from(getStringListArg(_pluginsArg)); + final Set excludedPlugins = + Set.from(getStringListArg(_excludeArg)); + final bool runOnChangedPackages = getBoolArg(_runOnChangedPackagesArg); + if (plugins.isEmpty && + runOnChangedPackages && + !(await _changesRequireFullTest())) { + plugins = await _getChangedPackages(); + } + + final Directory thirdPartyPackagesDirectory = packagesDir.parent + .childDirectory('third_party') + .childDirectory('packages'); + + for (final Directory dir in [ + packagesDir, + if (thirdPartyPackagesDirectory.existsSync()) thirdPartyPackagesDirectory, + ]) { + await for (final FileSystemEntity entity + in dir.list(followLinks: false)) { + // A top-level Dart package is a plugin package. + if (_isDartPackage(entity)) { + if (!excludedPlugins.contains(entity.basename) && + (plugins.isEmpty || plugins.contains(p.basename(entity.path)))) { + yield entity as Directory; + } + } else if (entity is Directory) { + // 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 = + p.relative(subdir.path, from: dir.path); + final String packageName = p.basename(subdir.path); + final String basenamePath = p.basename(entity.path); + if (!excludedPlugins.contains(basenamePath) && + !excludedPlugins.contains(packageName) && + !excludedPlugins.contains(relativePath) && + (plugins.isEmpty || + plugins.contains(relativePath) || + plugins.contains(basenamePath))) { + yield subdir as Directory; + } + } + } + } + } + } + } + + /// Returns the example Dart package folders of the plugins involved in this + /// command execution. + Stream getExamples() => + getPlugins().expand(getExamplesForPlugin); + + /// Returns all Dart package folders (typically, plugin + example) of the + /// plugins involved in this command execution. + Stream getPackages() async* { + await for (final Directory plugin in getPlugins()) { + yield plugin; + yield* plugin + .list(recursive: true, followLinks: false) + .where(_isDartPackage) + .cast(); + } + } + + /// Returns the files contained, recursively, within the plugins + /// involved in this command execution. + Stream getFiles() { + return getPlugins().asyncExpand((Directory folder) => folder + .list(recursive: true, followLinks: false) + .where((FileSystemEntity entity) => entity is File) + .cast()); + } + + /// Returns whether the specified entity is a directory containing a + /// `pubspec.yaml` file. + bool _isDartPackage(FileSystemEntity entity) { + return entity is Directory && entity.childFile('pubspec.yaml').existsSync(); + } + + /// Returns the example Dart packages contained in the specified plugin, or + /// an empty List, if the plugin has no examples. + Iterable getExamplesForPlugin(Directory plugin) { + final Directory exampleFolder = plugin.childDirectory('example'); + if (!exampleFolder.existsSync()) { + return []; + } + if (isFlutterPackage(exampleFolder)) { + return [exampleFolder]; + } + // Only look at the subdirectories of the example directory if the example + // directory itself is not a Dart package, and only look one level below the + // example directory for other dart packages. + return exampleFolder + .listSync() + .where((FileSystemEntity entity) => isFlutterPackage(entity)) + .cast(); + } + + /// Retrieve an instance of [GitVersionFinder] based on `_kBaseSha` and [gitDir]. + /// + /// Throws tool exit if [gitDir] nor root directory is a git directory. + Future retrieveVersionFinder() async { + final String rootDir = packagesDir.parent.absolute.path; + final String baseSha = getStringArg(_kBaseSha); + + GitDir? baseGitDir = gitDir; + if (baseGitDir == null) { + if (!await GitDir.isGitDir(rootDir)) { + printError( + '$rootDir is not a valid Git repository.', + ); + throw ToolExit(2); + } + baseGitDir = await GitDir.fromExisting(rootDir); + } + + final GitVersionFinder gitVersionFinder = + GitVersionFinder(baseGitDir, baseSha); + return gitVersionFinder; + } + + // Returns packages that have been changed relative to the git base. + Future> _getChangedPackages() async { + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + + final List allChangedFiles = + await gitVersionFinder.getChangedFiles(); + final Set packages = {}; + for (final String path in allChangedFiles) { + final List pathComponents = path.split('/'); + final int packagesIndex = + pathComponents.indexWhere((String element) => element == 'packages'); + if (packagesIndex != -1) { + packages.add(pathComponents[packagesIndex + 1]); + } + } + if (packages.isEmpty) { + print('No changed packages.'); + } else { + final String changedPackages = packages.join(','); + print('Changed packages: $changedPackages'); + } + return packages; + } + + // Returns true if one or more files changed that have the potential to affect + // any plugin (e.g., CI script changes). + Future _changesRequireFullTest() async { + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + + const List specialFiles = [ + '.ci.yaml', // LUCI config. + '.cirrus.yml', // Cirrus config. + '.clang-format', // ObjC and C/C++ formatting options. + 'analysis_options.yaml', // Dart analysis settings. + ]; + const List specialDirectories = [ + '.ci/', // Support files for CI. + 'script/', // This tool, and its wrapper scripts. + ]; + // Directory entries must end with / to avoid over-matching, since the + // check below is done via string prefixing. + assert(specialDirectories.every((String dir) => dir.endsWith('/'))); + + final List allChangedFiles = + await gitVersionFinder.getChangedFiles(); + return allChangedFiles.any((String path) => + specialFiles.contains(path) || + specialDirectories.any((String dir) => path.startsWith(dir))); + } +} + +/// A class used to run processes. +/// +/// We use this instead of directly running the process so it can be overridden +/// in tests. +class ProcessRunner { + /// Creates a new process runner. + const ProcessRunner(); + + /// Run the [executable] with [args] and stream output to stderr and stdout. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// If [exitOnError] is set to `true`, then this will throw an error if + /// the [executable] terminates with a non-zero exit code. + /// + /// Returns the exit code of the [executable]. + Future runAndStream( + String executable, + List args, { + Directory? workingDir, + bool exitOnError = false, + }) async { + print( + 'Running command: "$executable ${args.join(' ')}" in ${workingDir?.path ?? io.Directory.current.path}'); + final io.Process process = await io.Process.start(executable, args, + workingDirectory: workingDir?.path); + await io.stdout.addStream(process.stdout); + await io.stderr.addStream(process.stderr); + if (exitOnError && await process.exitCode != 0) { + final String error = + _getErrorString(executable, args, workingDir: workingDir); + print('$error See above for details.'); + throw ToolExit(await process.exitCode); + } + return process.exitCode; + } + + /// Run the [executable] with [args]. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// If [exitOnError] is set to `true`, then this will throw an error if + /// the [executable] terminates with a non-zero exit code. + /// Defaults to `false`. + /// + /// If [logOnError] is set to `true`, it will print a formatted message about the error. + /// Defaults to `false` + /// + /// Returns the [io.ProcessResult] of the [executable]. + Future run(String executable, List args, + {Directory? workingDir, + bool exitOnError = false, + bool logOnError = false, + Encoding stdoutEncoding = io.systemEncoding, + Encoding stderrEncoding = io.systemEncoding}) async { + final io.ProcessResult result = await io.Process.run(executable, args, + workingDirectory: workingDir?.path, + stdoutEncoding: stdoutEncoding, + stderrEncoding: stderrEncoding); + if (result.exitCode != 0) { + if (logOnError) { + final String error = + _getErrorString(executable, args, workingDir: workingDir); + print('$error Stderr:\n${result.stdout}'); + } + if (exitOnError) { + throw ToolExit(result.exitCode); + } + } + return result; + } + + /// Starts the [executable] with [args]. + /// + /// The current working directory of [executable] can be overridden by + /// passing [workingDir]. + /// + /// Returns the started [io.Process]. + Future start(String executable, List args, + {Directory? workingDirectory}) async { + final io.Process process = await io.Process.start(executable, args, + workingDirectory: workingDirectory?.path); + return process; + } + + String _getErrorString(String executable, List args, + {Directory? workingDir}) { + final String workdir = workingDir == null ? '' : ' in ${workingDir.path}'; + return 'ERROR: Unable to execute "$executable ${args.join(' ')}"$workdir.'; + } +} + +/// Finding version of [package] that is published on pub. +class PubVersionFinder { + /// Constructor. + /// + /// Note: you should manually close the [httpClient] when done using the finder. + PubVersionFinder({this.pubHost = defaultPubHost, required this.httpClient}); + + /// The default pub host to use. + static const String defaultPubHost = 'https://pub.dev'; + + /// The pub host url, defaults to `https://pub.dev`. + final String pubHost; + + /// The http client. + /// + /// You should manually close this client when done using this finder. + final http.Client httpClient; + + /// Get the package version on pub. + Future getPackageVersion( + {required String package}) async { + assert(package.isNotEmpty); + final Uri pubHostUri = Uri.parse(pubHost); + final Uri url = pubHostUri.replace(path: '/packages/$package.json'); + final http.Response response = await httpClient.get(url); + + if (response.statusCode == 404) { + return PubVersionFinderResponse( + versions: [], + result: PubVersionFinderResult.noPackageFound, + httpResponse: response); + } else if (response.statusCode != 200) { + return PubVersionFinderResponse( + versions: [], + result: PubVersionFinderResult.fail, + httpResponse: response); + } + final List versions = + (json.decode(response.body)['versions'] as List) + .map((final dynamic versionString) => + Version.parse(versionString as String)) + .toList(); + + return PubVersionFinderResponse( + versions: versions, + result: PubVersionFinderResult.success, + httpResponse: response); + } +} + +/// Represents a response for [PubVersionFinder]. +class PubVersionFinderResponse { + /// Constructor. + PubVersionFinderResponse( + {required this.versions, + required this.result, + required this.httpResponse}) { + if (versions.isNotEmpty) { + versions.sort((Version a, Version b) { + // TODO(cyanglaz): Think about how to handle pre-release version with [Version.prioritize]. + // https://github.com/flutter/flutter/issues/82222 + return b.compareTo(a); + }); + } + } + + /// The versions found in [PubVersionFinder]. + /// + /// This is sorted by largest to smallest, so the first element in the list is the largest version. + /// Might be `null` if the [result] is not [PubVersionFinderResult.success]. + final List versions; + + /// The result of the version finder. + final PubVersionFinderResult result; + + /// The response object of the http request. + final http.Response httpResponse; +} + +/// An enum representing the result of [PubVersionFinder]. +enum PubVersionFinderResult { + /// The version finder successfully found a version. + success, + + /// The version finder failed to find a valid version. + /// + /// This might due to http connection errors or user errors. + fail, + + /// The version finder failed to locate the package. + /// + /// This indicates the package is new. + noPackageFound, +} + +/// Finding diffs based on `baseGitDir` and `baseSha`. +class GitVersionFinder { + /// Constructor + GitVersionFinder(this.baseGitDir, this.baseSha); + + /// The top level directory of the git repo. + /// + /// That is where the .git/ folder exists. + final GitDir baseGitDir; + + /// The base sha used to get diff. + final String? baseSha; + + static bool _isPubspec(String file) { + return file.trim().endsWith('pubspec.yaml'); + } + + /// Get a list of all the pubspec.yaml file that is changed. + Future> getChangedPubSpecs() async { + return (await getChangedFiles()).where(_isPubspec).toList(); + } + + /// Get a list of all the changed files. + Future> getChangedFiles() async { + final String baseSha = await _getBaseSha(); + final io.ProcessResult changedFilesCommand = await baseGitDir + .runCommand(['diff', '--name-only', baseSha, 'HEAD']); + print('Determine diff with base sha: $baseSha'); + final String changedFilesStdout = changedFilesCommand.stdout.toString(); + if (changedFilesStdout.isEmpty) { + return []; + } + final List changedFiles = changedFilesStdout.split('\n') + ..removeWhere((String element) => element.isEmpty); + return changedFiles.toList(); + } + + /// Get the package version specified in the pubspec file in `pubspecPath` and + /// at the revision of `gitRef` (defaulting to the base if not provided). + Future getPackageVersion(String pubspecPath, + {String? gitRef}) async { + final String ref = gitRef ?? (await _getBaseSha()); + + io.ProcessResult gitShow; + try { + gitShow = + await baseGitDir.runCommand(['show', '$ref:$pubspecPath']); + } on io.ProcessException { + return null; + } + final String fileContent = gitShow.stdout as String; + final String? versionString = loadYaml(fileContent)['version'] as String?; + return versionString == null ? null : Version.parse(versionString); + } + + Future _getBaseSha() async { + if (baseSha != null && baseSha!.isNotEmpty) { + return baseSha!; + } + + io.ProcessResult baseShaFromMergeBase = await baseGitDir.runCommand( + ['merge-base', '--fork-point', 'FETCH_HEAD', 'HEAD'], + throwOnError: false); + if (baseShaFromMergeBase.stderr != null || + baseShaFromMergeBase.stdout == null) { + baseShaFromMergeBase = await baseGitDir + .runCommand(['merge-base', 'FETCH_HEAD', 'HEAD']); + } + return (baseShaFromMergeBase.stdout as String).trim(); + } +} diff --git a/script/tool/lib/src/create_all_plugins_app_command.dart b/script/tool/lib/src/create_all_plugins_app_command.dart new file mode 100644 index 000000000000..cd5b85e45ac0 --- /dev/null +++ b/script/tool/lib/src/create_all_plugins_app_command.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:io' as io; + +import 'package:file/file.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; + +import 'common.dart'; + +/// A command to create an application that builds all in a single application. +class CreateAllPluginsAppCommand extends PluginCommand { + /// Creates an instance of the builder command. + CreateAllPluginsAppCommand( + Directory packagesDir, { + Directory? pluginsRoot, + }) : pluginsRoot = pluginsRoot ?? packagesDir.fileSystem.currentDirectory, + super(packagesDir) { + appDirectory = this.pluginsRoot.childDirectory('all_plugins'); + } + + /// The root directory of the plugin repository. + Directory pluginsRoot; + + /// The location of the synthesized app project. + late Directory appDirectory; + + @override + String get description => + 'Generate Flutter app that includes all plugins in packages.'; + + @override + String get name => 'all-plugins-app'; + + @override + Future run() async { + final int exitCode = await _createApp(); + if (exitCode != 0) { + throw ToolExit(exitCode); + } + + await Future.wait(>[ + _genPubspecWithAllPlugins(), + _updateAppGradle(), + _updateManifest(), + ]); + } + + Future _createApp() async { + final io.ProcessResult result = io.Process.runSync( + 'flutter', + [ + 'create', + '--template=app', + '--project-name=all_plugins', + '--android-language=java', + appDirectory.path, + ], + ); + + print(result.stdout); + print(result.stderr); + return result.exitCode; + } + + Future _updateAppGradle() async { + final File gradleFile = appDirectory + .childDirectory('android') + .childDirectory('app') + .childFile('build.gradle'); + if (!gradleFile.existsSync()) { + throw ToolExit(64); + } + + final StringBuffer newGradle = StringBuffer(); + for (final String line in gradleFile.readAsLinesSync()) { + if (line.contains('minSdkVersion 16')) { + // Android SDK 20 is required by Google maps. + // Android SDK 19 is required by WebView. + newGradle.writeln('minSdkVersion 20'); + } else { + newGradle.writeln(line); + } + if (line.contains('defaultConfig {')) { + newGradle.writeln(' multiDexEnabled true'); + } else if (line.contains('dependencies {')) { + newGradle.writeln( + ' implementation \'com.google.guava:guava:27.0.1-android\'\n', + ); + // Tests for https://github.com/flutter/flutter/issues/43383 + newGradle.writeln( + " implementation 'androidx.lifecycle:lifecycle-runtime:2.2.0-rc01'\n", + ); + } + } + gradleFile.writeAsStringSync(newGradle.toString()); + } + + Future _updateManifest() async { + final File manifestFile = appDirectory + .childDirectory('android') + .childDirectory('app') + .childDirectory('src') + .childDirectory('main') + .childFile('AndroidManifest.xml'); + if (!manifestFile.existsSync()) { + throw ToolExit(64); + } + + final StringBuffer newManifest = StringBuffer(); + for (final String line in manifestFile.readAsLinesSync()) { + if (line.contains('package="com.example.all_plugins"')) { + newManifest + ..writeln('package="com.example.all_plugins"') + ..writeln('xmlns:tools="http://schemas.android.com/tools">') + ..writeln() + ..writeln( + '', + ); + } else { + newManifest.writeln(line); + } + } + manifestFile.writeAsStringSync(newManifest.toString()); + } + + Future _genPubspecWithAllPlugins() async { + final Map pluginDeps = + await _getValidPathDependencies(); + final Pubspec pubspec = Pubspec( + 'all_plugins', + description: 'Flutter app containing all 1st party plugins.', + version: Version.parse('1.0.0+1'), + environment: { + 'sdk': VersionConstraint.compatibleWith( + Version.parse('2.12.0'), + ), + }, + dependencies: { + 'flutter': SdkDependency('flutter'), + }..addAll(pluginDeps), + devDependencies: { + 'flutter_test': SdkDependency('flutter'), + }, + dependencyOverrides: pluginDeps, + ); + final File pubspecFile = appDirectory.childFile('pubspec.yaml'); + pubspecFile.writeAsStringSync(_pubspecToString(pubspec)); + } + + Future> _getValidPathDependencies() async { + final Map pathDependencies = + {}; + + await for (final Directory package in getPlugins()) { + final String pluginName = package.path.split('/').last; + final File pubspecFile = package.childFile('pubspec.yaml'); + final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + + if (pubspec.publishTo != 'none') { + pathDependencies[pluginName] = PathDependency(package.path); + } + } + return pathDependencies; + } + + String _pubspecToString(Pubspec pubspec) { + return ''' +### Generated file. Do not edit. Run `pub global run flutter_plugin_tools gen-pubspec` to update. +name: ${pubspec.name} +description: ${pubspec.description} + +version: ${pubspec.version} + +environment:${_pubspecMapString(pubspec.environment!)} + +dependencies:${_pubspecMapString(pubspec.dependencies)} + +dependency_overrides:${_pubspecMapString(pubspec.dependencyOverrides)} + +dev_dependencies:${_pubspecMapString(pubspec.devDependencies)} +###'''; + } + + String _pubspecMapString(Map values) { + final StringBuffer buffer = StringBuffer(); + + for (final MapEntry entry in values.entries) { + buffer.writeln(); + if (entry.value is VersionConstraint) { + buffer.write(' ${entry.key}: ${entry.value}'); + } else if (entry.value is SdkDependency) { + final SdkDependency dep = entry.value as SdkDependency; + buffer.write(' ${entry.key}: \n sdk: ${dep.sdk}'); + } else if (entry.value is PathDependency) { + final PathDependency dep = entry.value as PathDependency; + buffer.write(' ${entry.key}: \n path: ${dep.path}'); + } else { + throw UnimplementedError( + 'Not available for type: ${entry.value.runtimeType}', + ); + } + } + + return buffer.toString(); + } +} diff --git a/script/tool/lib/src/drive_examples_command.dart b/script/tool/lib/src/drive_examples_command.dart new file mode 100644 index 000000000000..14dfede5b2f1 --- /dev/null +++ b/script/tool/lib/src/drive_examples_command.dart @@ -0,0 +1,252 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'common.dart'; + +/// A command to run the example applications for packages via Flutter driver. +class DriveExamplesCommand extends PluginCommand { + /// Creates an instance of the drive command. + DriveExamplesCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, processRunner: processRunner) { + argParser.addFlag(kPlatformFlagAndroid, + help: 'Runs the Android implementation of the examples'); + argParser.addFlag(kPlatformFlagIos, + help: 'Runs the iOS implementation of the examples'); + argParser.addFlag(kPlatformFlagLinux, + help: 'Runs the Linux implementation of the examples'); + argParser.addFlag(kPlatformFlagMacos, + help: 'Runs the macOS implementation of the examples'); + argParser.addFlag(kPlatformFlagWeb, + help: 'Runs the web implementation of the examples'); + argParser.addFlag(kPlatformFlagWindows, + help: 'Runs the Windows implementation of the examples'); + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: + 'Runs the driver tests in Dart VM with the given experiments enabled.', + ); + } + + @override + final String name = 'drive-examples'; + + @override + final String description = 'Runs driver tests for plugin example apps.\n\n' + 'For each *_test.dart in test_driver/ it drives an application with a ' + 'corresponding name in the test/ or test_driver/ directories.\n\n' + 'For example, test_driver/app_test.dart would match test/app.dart.\n\n' + 'This command requires "flutter" to be in your path.\n\n' + 'If a file with a corresponding name cannot be found, this driver file' + 'will be used to drive the tests that match ' + 'integration_test/*_test.dart.'; + + @override + Future run() async { + final List failingTests = []; + final List pluginsWithoutTests = []; + final bool isLinux = getBoolArg(kPlatformFlagLinux); + final bool isMacos = getBoolArg(kPlatformFlagMacos); + final bool isWeb = getBoolArg(kPlatformFlagWeb); + final bool isWindows = getBoolArg(kPlatformFlagWindows); + await for (final Directory plugin in getPlugins()) { + final String pluginName = plugin.basename; + if (pluginName.endsWith('_platform_interface') && + !plugin.childDirectory('example').existsSync()) { + // Platform interface packages generally aren't intended to have + // examples, and don't need integration tests, so silently skip them + // unless for some reason there is an example directory. + continue; + } + print('\n==========\nChecking $pluginName...'); + if (!(await _pluginSupportedOnCurrentPlatform(plugin))) { + print('Not supported for the target platform; skipping.'); + continue; + } + int examplesFound = 0; + bool testsRan = false; + final String flutterCommand = + const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + for (final Directory example in getExamplesForPlugin(plugin)) { + ++examplesFound; + final String packageName = + p.relative(example.path, from: packagesDir.path); + final Directory driverTests = example.childDirectory('test_driver'); + if (!driverTests.existsSync()) { + print('No driver tests found for $packageName'); + continue; + } + // Look for driver tests ending in _test.dart in test_driver/ + await for (final FileSystemEntity test in driverTests.list()) { + final String driverTestName = + p.relative(test.path, from: driverTests.path); + if (!driverTestName.endsWith('_test.dart')) { + continue; + } + // Try to find a matching app to drive without the _test.dart + final String deviceTestName = driverTestName.replaceAll( + RegExp(r'_test.dart$'), + '.dart', + ); + String deviceTestPath = p.join('test', deviceTestName); + if (!example.fileSystem + .file(p.join(example.path, deviceTestPath)) + .existsSync()) { + // If the app isn't in test/ folder, look in test_driver/ instead. + deviceTestPath = p.join('test_driver', deviceTestName); + } + + final List targetPaths = []; + if (example.fileSystem + .file(p.join(example.path, deviceTestPath)) + .existsSync()) { + targetPaths.add(deviceTestPath); + } else { + final Directory integrationTests = + example.childDirectory('integration_test'); + + if (await integrationTests.exists()) { + await for (final FileSystemEntity integrationTest + in integrationTests.list()) { + if (!integrationTest.basename.endsWith('_test.dart')) { + continue; + } + targetPaths + .add(p.relative(integrationTest.path, from: example.path)); + } + } + + if (targetPaths.isEmpty) { + print(''' +Unable to infer a target application for $driverTestName to drive. +Tried searching for the following: +1. test/$deviceTestName +2. test_driver/$deviceTestName +3. test_driver/*_test.dart +'''); + failingTests.add(p.relative(test.path, from: example.path)); + continue; + } + } + + final List driveArgs = ['drive']; + + final String enableExperiment = getStringArg(kEnableExperiment); + if (enableExperiment.isNotEmpty) { + driveArgs.add('--enable-experiment=$enableExperiment'); + } + + if (isLinux && isLinuxPlugin(plugin)) { + driveArgs.addAll([ + '-d', + 'linux', + ]); + } + if (isMacos && isMacOsPlugin(plugin)) { + driveArgs.addAll([ + '-d', + 'macos', + ]); + } + if (isWeb && isWebPlugin(plugin)) { + driveArgs.addAll([ + '-d', + 'web-server', + '--web-port=7357', + '--browser-name=chrome', + ]); + } + if (isWindows && isWindowsPlugin(plugin)) { + driveArgs.addAll([ + '-d', + 'windows', + ]); + } + + for (final String targetPath in targetPaths) { + testsRan = true; + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + ...driveArgs, + '--driver', + p.join('test_driver', driverTestName), + '--target', + targetPath, + ], + workingDir: example, + exitOnError: true); + if (exitCode != 0) { + failingTests.add(p.join(packageName, deviceTestPath)); + } + } + } + } + if (!testsRan) { + pluginsWithoutTests.add(pluginName); + print( + 'No driver tests run for $pluginName ($examplesFound examples found)'); + } + } + print('\n\n'); + + if (failingTests.isNotEmpty) { + print('The following driver tests are failing (see above for details):'); + for (final String test in failingTests) { + print(' * $test'); + } + throw ToolExit(1); + } + + if (pluginsWithoutTests.isNotEmpty) { + print('The following plugins did not run any integration tests:'); + for (final String plugin in pluginsWithoutTests) { + print(' * $plugin'); + } + print('If this is intentional, they must be explicitly excluded.'); + throw ToolExit(1); + } + + print('All driver tests successful!'); + } + + Future _pluginSupportedOnCurrentPlatform( + FileSystemEntity plugin) async { + final bool isAndroid = getBoolArg(kPlatformFlagAndroid); + final bool isIOS = getBoolArg(kPlatformFlagIos); + final bool isLinux = getBoolArg(kPlatformFlagLinux); + final bool isMacos = getBoolArg(kPlatformFlagMacos); + final bool isWeb = getBoolArg(kPlatformFlagWeb); + final bool isWindows = getBoolArg(kPlatformFlagWindows); + if (isAndroid) { + return isAndroidPlugin(plugin); + } + if (isIOS) { + return isIosPlugin(plugin); + } + if (isLinux) { + return isLinuxPlugin(plugin); + } + if (isMacos) { + return isMacOsPlugin(plugin); + } + if (isWeb) { + return isWebPlugin(plugin); + } + if (isWindows) { + return isWindowsPlugin(plugin); + } + // When we are here, no flags are specified. Only return true if the plugin + // supports Android for legacy command support. + // TODO(cyanglaz): Make Android flag also required like other platforms + // (breaking change). https://github.com/flutter/flutter/issues/58285 + return isAndroidPlugin(plugin); + } +} diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart new file mode 100644 index 000000000000..741d8569322b --- /dev/null +++ b/script/tool/lib/src/firebase_test_lab_command.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:async'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:uuid/uuid.dart'; + +import 'common.dart'; + +/// A command to run tests via Firebase test lab. +class FirebaseTestLabCommand extends PluginCommand { + /// Creates an instance of the test runner command. + FirebaseTestLabCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Print print = print, + }) : _print = print, + super(packagesDir, processRunner: processRunner) { + argParser.addOption( + 'project', + defaultsTo: 'flutter-infra', + help: 'The Firebase project name.', + ); + final String? homeDir = io.Platform.environment['HOME']; + argParser.addOption('service-key', + defaultsTo: + homeDir == null ? null : p.join(homeDir, 'gcloud-service-key.json'), + help: 'The path to the service key for gcloud authentication.\n' + r'If not provided, \$HOME/gcloud-service-key.json will be ' + r'assumed if $HOME is set.'); + argParser.addOption('test-run-id', + defaultsTo: const Uuid().v4(), + help: + 'Optional string to append to the results path, to avoid conflicts. ' + 'Randomly chosen on each invocation if none is provided. ' + 'The default shown here is just an example.'); + argParser.addOption('build-id', + defaultsTo: + io.Platform.environment['CIRRUS_BUILD_ID'] ?? 'unknown_build', + help: + 'Optional string to append to the results path, to avoid conflicts. ' + r'Defaults to $CIRRUS_BUILD_ID if that is set.'); + argParser.addMultiOption('device', + splitCommas: false, + defaultsTo: [ + 'model=walleye,version=26', + 'model=flame,version=29' + ], + help: + 'Device model(s) to test. See https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run for more info'); + argParser.addOption('results-bucket', + defaultsTo: 'gs://flutter_firebase_testlab'); + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: 'Enables the given Dart SDK experiments.', + ); + } + + @override + final String name = 'firebase-test-lab'; + + @override + final String description = 'Runs the instrumentation tests of the example ' + 'apps on Firebase Test Lab.\n\n' + 'Runs tests in test_instrumentation folder using the ' + 'instrumentation_test package.'; + + static const String _gradleWrapper = 'gradlew'; + + final Print _print; + + Completer? _firebaseProjectConfigured; + + Future _configureFirebaseProject() async { + if (_firebaseProjectConfigured != null) { + return _firebaseProjectConfigured!.future; + } + _firebaseProjectConfigured = Completer(); + + final String serviceKey = getStringArg('service-key'); + if (serviceKey.isEmpty) { + _print('No --service-key provided; skipping gcloud authorization'); + } else { + await processRunner.run( + 'gcloud', + [ + 'auth', + 'activate-service-account', + '--key-file=$serviceKey', + ], + exitOnError: true, + logOnError: true, + ); + final int exitCode = await processRunner.runAndStream('gcloud', [ + 'config', + 'set', + 'project', + getStringArg('project'), + ]); + if (exitCode == 0) { + _print('\nFirebase project configured.'); + return; + } else { + _print( + '\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.'); + } + } + _firebaseProjectConfigured!.complete(null); + } + + @override + Future run() async { + final Stream packagesWithTests = getPackages().where( + (Directory d) => + isFlutterPackage(d) && + d + .childDirectory('example') + .childDirectory('android') + .childDirectory('app') + .childDirectory('src') + .childDirectory('androidTest') + .existsSync()); + + final List failingPackages = []; + final List missingFlutterBuild = []; + int resultsCounter = + 0; // We use a unique GCS bucket for each Firebase Test Lab run + await for (final Directory package in packagesWithTests) { + // See https://github.com/flutter/flutter/issues/38983 + + final Directory exampleDirectory = package.childDirectory('example'); + final String packageName = + p.relative(package.path, from: packagesDir.path); + _print('\nRUNNING FIREBASE TEST LAB TESTS for $packageName'); + + final Directory androidDirectory = + exampleDirectory.childDirectory('android'); + + final String enableExperiment = getStringArg(kEnableExperiment); + final String encodedEnableExperiment = + Uri.encodeComponent('--enable-experiment=$enableExperiment'); + + // Ensures that gradle wrapper exists + if (!androidDirectory.childFile(_gradleWrapper).existsSync()) { + final int exitCode = await processRunner.runAndStream( + 'flutter', + [ + 'build', + 'apk', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: androidDirectory); + + if (exitCode != 0) { + failingPackages.add(packageName); + continue; + } + continue; + } + + await _configureFirebaseProject(); + + int exitCode = await processRunner.runAndStream( + p.join(androidDirectory.path, _gradleWrapper), + [ + 'app:assembleAndroidTest', + '-Pverbose=true', + if (enableExperiment.isNotEmpty) + '-Pextra-front-end-options=$encodedEnableExperiment', + if (enableExperiment.isNotEmpty) + '-Pextra-gen-snapshot-options=$encodedEnableExperiment', + ], + workingDir: androidDirectory); + + if (exitCode != 0) { + failingPackages.add(packageName); + continue; + } + + // Look for tests recursively in folders that start with 'test' and that + // live in the root or example folders. + bool isTestDir(FileSystemEntity dir) { + return dir is Directory && + (p.basename(dir.path).startsWith('test') || + p.basename(dir.path) == 'integration_test'); + } + + final List testDirs = + package.listSync().where(isTestDir).cast().toList(); + final Directory example = package.childDirectory('example'); + testDirs.addAll( + example.listSync().where(isTestDir).cast().toList()); + for (final Directory testDir in testDirs) { + bool isE2ETest(FileSystemEntity file) { + return file.path.endsWith('_e2e.dart') || + (file.parent.basename == 'integration_test' && + file.path.endsWith('_test.dart')); + } + + final List testFiles = testDir + .listSync(recursive: true, followLinks: true) + .where(isE2ETest) + .toList(); + for (final FileSystemEntity test in testFiles) { + exitCode = await processRunner.runAndStream( + p.join(androidDirectory.path, _gradleWrapper), + [ + 'app:assembleDebug', + '-Pverbose=true', + '-Ptarget=${test.path}', + if (enableExperiment.isNotEmpty) + '-Pextra-front-end-options=$encodedEnableExperiment', + if (enableExperiment.isNotEmpty) + '-Pextra-gen-snapshot-options=$encodedEnableExperiment', + ], + workingDir: androidDirectory); + + if (exitCode != 0) { + failingPackages.add(packageName); + continue; + } + final String buildId = getStringArg('build-id'); + final String testRunId = getStringArg('test-run-id'); + final String resultsDir = + 'plugins_android_test/$packageName/$buildId/$testRunId/${resultsCounter++}/'; + final List args = [ + 'firebase', + 'test', + 'android', + 'run', + '--type', + 'instrumentation', + '--app', + 'build/app/outputs/apk/debug/app-debug.apk', + '--test', + 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk', + '--timeout', + '5m', + '--results-bucket=${getStringArg('results-bucket')}', + '--results-dir=$resultsDir', + ]; + for (final String device in getStringListArg('device')) { + args.addAll(['--device', device]); + } + exitCode = await processRunner.runAndStream('gcloud', args, + workingDir: exampleDirectory); + + if (exitCode != 0) { + failingPackages.add(packageName); + continue; + } + } + } + } + + _print('\n\n'); + if (failingPackages.isNotEmpty) { + _print( + 'The instrumentation tests for the following packages are failing (see above for' + 'details):'); + for (final String package in failingPackages) { + _print(' * $package'); + } + } + if (missingFlutterBuild.isNotEmpty) { + _print('Run "pub global run flutter_plugin_tools build-examples --apk" on' + 'the following packages before executing tests again:'); + for (final String package in missingFlutterBuild) { + _print(' * $package'); + } + } + + if (failingPackages.isNotEmpty || missingFlutterBuild.isNotEmpty) { + throw ToolExit(1); + } + + _print('All Firebase Test Lab tests successful!'); + } +} diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart new file mode 100644 index 000000000000..1ef41f82bb2c --- /dev/null +++ b/script/tool/lib/src/format_command.dart @@ -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. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:http/http.dart' as http; +import 'package:path/path.dart' as p; +import 'package:quiver/iterables.dart'; + +import 'common.dart'; + +final Uri _googleFormatterUrl = Uri.https('github.com', + '/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-1.3-all-deps.jar'); + +/// A command to format all package code. +class FormatCommand extends PluginCommand { + /// Creates an instance of the format command. + FormatCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, processRunner: processRunner) { + argParser.addFlag('fail-on-change', hide: true); + argParser.addOption('clang-format', + defaultsTo: 'clang-format', + help: 'Path to executable of clang-format.'); + } + + @override + final String name = 'format'; + + @override + final String description = + 'Formats the code of all packages (Java, Objective-C, C++, and Dart).\n\n' + 'This command requires "git", "flutter" and "clang-format" v5 to be in ' + 'your path.'; + + @override + Future run() async { + final String googleFormatterPath = await _getGoogleFormatterPath(); + + await _formatDart(); + await _formatJava(googleFormatterPath); + await _formatCppAndObjectiveC(); + + if (getBoolArg('fail-on-change')) { + final bool modified = await _didModifyAnything(); + if (modified) { + throw ToolExit(1); + } + } + } + + Future _didModifyAnything() async { + final io.ProcessResult modifiedFiles = await processRunner.run( + 'git', + ['ls-files', '--modified'], + workingDir: packagesDir, + exitOnError: true, + logOnError: true, + ); + + print('\n\n'); + + final String stdout = modifiedFiles.stdout as String; + if (stdout.isEmpty) { + print('All files formatted correctly.'); + return false; + } + + print('These files are not formatted correctly (see diff below):'); + LineSplitter.split(stdout).map((String line) => ' $line').forEach(print); + + print('\nTo fix run "pub global activate flutter_plugin_tools && ' + 'pub global run flutter_plugin_tools format" or copy-paste ' + 'this command into your terminal:'); + + print('patch -p1 <['diff'], + workingDir: packagesDir, + exitOnError: true, + logOnError: true, + ); + print(diff.stdout); + print('DONE'); + return true; + } + + Future _formatCppAndObjectiveC() async { + print('Formatting all .cc, .cpp, .mm, .m, and .h files...'); + final Iterable allFiles = [ + ...await _getFilesWithExtension('.h'), + ...await _getFilesWithExtension('.m'), + ...await _getFilesWithExtension('.mm'), + ...await _getFilesWithExtension('.cc'), + ...await _getFilesWithExtension('.cpp'), + ]; + // Split this into multiple invocations to avoid a + // 'ProcessException: Argument list too long'. + final Iterable> batches = partition(allFiles, 100); + for (final List batch in batches) { + await processRunner.runAndStream(getStringArg('clang-format'), + ['-i', '--style=Google', ...batch], + workingDir: packagesDir, exitOnError: true); + } + } + + Future _formatJava(String googleFormatterPath) async { + print('Formatting all .java files...'); + final Iterable javaFiles = await _getFilesWithExtension('.java'); + await processRunner.runAndStream('java', + ['-jar', googleFormatterPath, '--replace', ...javaFiles], + workingDir: packagesDir, exitOnError: true); + } + + Future _formatDart() async { + // This actually should be fine for non-Flutter Dart projects, no need to + // specifically shell out to dartfmt -w in that case. + print('Formatting all .dart files...'); + final Iterable dartFiles = await _getFilesWithExtension('.dart'); + if (dartFiles.isEmpty) { + print( + 'No .dart files to format. If you set the `--exclude` flag, most likey they were skipped'); + } else { + await processRunner.runAndStream( + 'flutter', ['format', ...dartFiles], + workingDir: packagesDir, exitOnError: true); + } + } + + Future> _getFilesWithExtension(String extension) async => + getFiles() + .where((File file) => p.extension(file.path) == extension) + .map((File file) => file.path) + .toList(); + + Future _getGoogleFormatterPath() async { + final String javaFormatterPath = p.join( + p.dirname(p.fromUri(io.Platform.script)), + 'google-java-format-1.3-all-deps.jar'); + final File javaFormatterFile = + packagesDir.fileSystem.file(javaFormatterPath); + + if (!javaFormatterFile.existsSync()) { + print('Downloading Google Java Format...'); + final http.Response response = await http.get(_googleFormatterUrl); + javaFormatterFile.writeAsBytesSync(response.bodyBytes); + } + + return javaFormatterPath; + } +} diff --git a/script/tool/lib/src/java_test_command.dart b/script/tool/lib/src/java_test_command.dart new file mode 100644 index 000000000000..d1366ea7636a --- /dev/null +++ b/script/tool/lib/src/java_test_command.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:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +/// A command to run the Java tests of Android plugins. +class JavaTestCommand extends PluginCommand { + /// Creates an instance of the test runner. + JavaTestCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, processRunner: processRunner); + + @override + final String name = 'java-test'; + + @override + final String description = 'Runs the Java tests of the example apps.\n\n' + 'Building the apks of the example apps is required before executing this' + 'command.'; + + static const String _gradleWrapper = 'gradlew'; + + @override + Future run() async { + final Stream examplesWithTests = getExamples().where( + (Directory d) => + isFlutterPackage(d) && + (d + .childDirectory('android') + .childDirectory('app') + .childDirectory('src') + .childDirectory('test') + .existsSync() || + d.parent + .childDirectory('android') + .childDirectory('src') + .childDirectory('test') + .existsSync())); + + final List failingPackages = []; + final List missingFlutterBuild = []; + await for (final Directory example in examplesWithTests) { + final String packageName = + p.relative(example.path, from: packagesDir.path); + print('\nRUNNING JAVA TESTS for $packageName'); + + final Directory androidDirectory = example.childDirectory('android'); + if (!androidDirectory.childFile(_gradleWrapper).existsSync()) { + print('ERROR: Run "flutter build apk" on example app of $packageName' + 'before executing tests.'); + missingFlutterBuild.add(packageName); + continue; + } + + final int exitCode = await processRunner.runAndStream( + p.join(androidDirectory.path, _gradleWrapper), + ['testDebugUnitTest', '--info'], + workingDir: androidDirectory); + if (exitCode != 0) { + failingPackages.add(packageName); + } + } + + print('\n\n'); + if (failingPackages.isNotEmpty) { + print( + 'The Java tests for the following packages are failing (see above for' + 'details):'); + for (final String package in failingPackages) { + print(' * $package'); + } + } + if (missingFlutterBuild.isNotEmpty) { + print('Run "pub global run flutter_plugin_tools build-examples --apk" on' + 'the following packages before executing tests again:'); + for (final String package in missingFlutterBuild) { + print(' * $package'); + } + } + + if (failingPackages.isNotEmpty || missingFlutterBuild.isNotEmpty) { + throw ToolExit(1); + } + + print('All Java tests successful!'); + } +} diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart new file mode 100644 index 000000000000..805c3ab9f900 --- /dev/null +++ b/script/tool/lib/src/license_check_command.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 'dart:async'; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +const Set _codeFileExtensions = { + '.c', + '.cc', + '.cpp', + '.dart', + '.h', + '.html', + '.java', + '.m', + '.mm', + '.swift', + '.sh', +}; + +// Basenames without extensions of files to ignore. +const Set _ignoreBasenameList = { + 'flutter_export_environment', + 'GeneratedPluginRegistrant', + 'generated_plugin_registrant', +}; + +// File suffixes that otherwise match _codeFileExtensions to ignore. +const Set _ignoreSuffixList = { + '.g.dart', // Generated API code. + '.mocks.dart', // Generated by Mockito. +}; + +// Full basenames of files to ignore. +const Set _ignoredFullBasenameList = { + 'resource.h', // Generated by VS. +}; + +// Copyright and license regexes for third-party code. +// +// These are intentionally very simple, since there is very little third-party +// code in this repository. Complexity can be added as-needed on a case-by-case +// basis. +// +// When adding license regexes here, include the copyright info to ensure that +// any new additions are flagged for added scrutiny in review. +final List _thirdPartyLicenseBlockRegexes = [ +// Third-party code used in url_launcher_web. + RegExp( + r'^// Copyright 2017 Workiva Inc\..*' + r'^// Licensed under the Apache License, Version 2\.0', + multiLine: true, + dotAll: true), + // bsdiff in flutter/packages. + RegExp(r'// Copyright 2003-2005 Colin Percival\. All rights reserved\.\n' + r'// Use of this source code is governed by a BSD-style license that can be\n' + r'// found in the LICENSE file\.\n'), +]; + +// The exact format of the BSD license that our license files should contain. +// Slight variants are not accepted because they may prevent consolidation in +// tools that assemble all licenses used in distributed applications. +// standardized. +const String _fullBsdLicenseText = ''' +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +'''; + +/// Validates that code files have copyright and license blocks. +class LicenseCheckCommand extends PluginCommand { + /// Creates a new license check command for [packagesDir]. + LicenseCheckCommand( + Directory packagesDir, { + Print print = print, + }) : _print = print, + super(packagesDir); + + final Print _print; + + @override + final String name = 'license-check'; + + @override + final String description = + 'Ensures that all code files have copyright/license blocks.'; + + @override + Future run() async { + final Iterable codeFiles = (await _getAllFiles()).where((File file) => + _codeFileExtensions.contains(p.extension(file.path)) && + !_shouldIgnoreFile(file)); + final Iterable firstPartyLicenseFiles = (await _getAllFiles()).where( + (File file) => + p.basename(file.basename) == 'LICENSE' && !_isThirdParty(file)); + + final bool copyrightCheckSucceeded = await _checkCodeLicenses(codeFiles); + _print('\n=======================================\n'); + final bool licenseCheckSucceeded = + await _checkLicenseFiles(firstPartyLicenseFiles); + + if (!copyrightCheckSucceeded || !licenseCheckSucceeded) { + throw ToolExit(1); + } + } + + // Creates the expected copyright+license block for first-party code. + String _generateLicenseBlock( + String comment, { + String prefix = '', + String suffix = '', + }) { + return '$prefix${comment}Copyright 2013 The Flutter Authors. All rights reserved.\n' + '${comment}Use of this source code is governed by a BSD-style license that can be\n' + '${comment}found in the LICENSE file.$suffix\n'; + } + + // Checks all license blocks for [codeFiles], returning false if any of them + // fail validation. + Future _checkCodeLicenses(Iterable codeFiles) async { + final List incorrectFirstPartyFiles = []; + final List unrecognizedThirdPartyFiles = []; + + // Most code file types in the repository use '//' comments. + final String defaultFirstParyLicenseBlock = _generateLicenseBlock('// '); + // A few file types have a different comment structure. + final Map firstPartyLicenseBlockByExtension = + { + '.sh': _generateLicenseBlock('# '), + '.html': _generateLicenseBlock('', prefix: ''), + }; + + for (final File file in codeFiles) { + _print('Checking ${file.path}'); + final String content = await file.readAsString(); + + final String firstParyLicense = + firstPartyLicenseBlockByExtension[p.extension(file.path)] ?? + defaultFirstParyLicenseBlock; + if (_isThirdParty(file)) { + // Third-party directories allow either known third-party licenses, our + // the first-party license, as there may be local additions. + if (!_thirdPartyLicenseBlockRegexes + .any((RegExp regex) => regex.hasMatch(content)) && + !content.contains(firstParyLicense)) { + unrecognizedThirdPartyFiles.add(file); + } + } else { + if (!content.contains(firstParyLicense)) { + incorrectFirstPartyFiles.add(file); + } + } + } + _print('\n'); + + // Sort by path for more usable output. + final int Function(File, File) pathCompare = + (File a, File b) => a.path.compareTo(b.path); + incorrectFirstPartyFiles.sort(pathCompare); + unrecognizedThirdPartyFiles.sort(pathCompare); + + if (incorrectFirstPartyFiles.isNotEmpty) { + _print('The license block for these files is missing or incorrect:'); + for (final File file in incorrectFirstPartyFiles) { + _print(' ${file.path}'); + } + _print('If this third-party code, move it to a "third_party/" directory, ' + 'otherwise ensure that you are using the exact copyright and license ' + 'text used by all first-party files in this repository.\n'); + } + + if (unrecognizedThirdPartyFiles.isNotEmpty) { + _print( + 'No recognized license was found for the following third-party files:'); + for (final File file in unrecognizedThirdPartyFiles) { + _print(' ${file.path}'); + } + _print('Please check that they have a license at the top of the file. ' + 'If they do, the license check needs to be updated to recognize ' + 'the new third-party license block.\n'); + } + + final bool succeeded = + incorrectFirstPartyFiles.isEmpty && unrecognizedThirdPartyFiles.isEmpty; + if (succeeded) { + _print('All source files passed validation!'); + } + return succeeded; + } + + // Checks all provide LICENSE files, returning false if any of them + // fail validation. + Future _checkLicenseFiles(Iterable files) async { + final List incorrectLicenseFiles = []; + + for (final File file in files) { + _print('Checking ${file.path}'); + if (!file.readAsStringSync().contains(_fullBsdLicenseText)) { + incorrectLicenseFiles.add(file); + } + } + _print('\n'); + + if (incorrectLicenseFiles.isNotEmpty) { + _print('The following LICENSE files do not follow the expected format:'); + for (final File file in incorrectLicenseFiles) { + _print(' ${file.path}'); + } + _print( + 'Please ensure that they use the exact format used in this repository".\n'); + } + + final bool succeeded = incorrectLicenseFiles.isEmpty; + if (succeeded) { + _print('All LICENSE files passed validation!'); + } + return succeeded; + } + + bool _shouldIgnoreFile(File file) { + final String path = file.path; + return _ignoreBasenameList.contains(p.basenameWithoutExtension(path)) || + _ignoreSuffixList.any((String suffix) => + path.endsWith(suffix) || + _ignoredFullBasenameList.contains(p.basename(path))); + } + + bool _isThirdParty(File file) { + return p.split(file.path).contains('third_party'); + } + + Future> _getAllFiles() => packagesDir.parent + .list(recursive: true, followLinks: false) + .where((FileSystemEntity entity) => entity is File) + .map((FileSystemEntity file) => file as File) + .toList(); +} diff --git a/script/tool/lib/src/lint_podspecs_command.dart b/script/tool/lib/src/lint_podspecs_command.dart new file mode 100644 index 000000000000..364653bd13ba --- /dev/null +++ b/script/tool/lib/src/lint_podspecs_command.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 'dart:convert'; +import 'dart:io'; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; + +import 'common.dart'; + +/// Lint the CocoaPod podspecs and run unit tests. +/// +/// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint. +class LintPodspecsCommand extends PluginCommand { + /// Creates an instance of the linter command. + LintPodspecsCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + Print print = print, + }) : _platform = platform, + _print = print, + super(packagesDir, processRunner: processRunner) { + argParser.addMultiOption('skip', + help: + 'Skip all linting for podspecs with this basename (example: federated plugins with placeholder podspecs)', + valueHelp: 'podspec_file_name'); + argParser.addMultiOption('ignore-warnings', + help: + 'Do not pass --allow-warnings flag to "pod lib lint" for podspecs with this basename (example: plugins with known warnings)', + valueHelp: 'podspec_file_name'); + } + + @override + final String name = 'podspecs'; + + @override + List get aliases => ['podspec']; + + @override + final String description = + 'Runs "pod lib lint" on all iOS and macOS plugin podspecs.\n\n' + 'This command requires "pod" and "flutter" to be in your path. Runs on macOS only.'; + + final Platform _platform; + + final Print _print; + + @override + Future run() async { + if (!_platform.isMacOS) { + _print('Detected platform is not macOS, skipping podspec lint'); + return; + } + + await processRunner.run( + 'which', + ['pod'], + workingDir: packagesDir, + exitOnError: true, + logOnError: true, + ); + + _print('Starting podspec lint test'); + + final List failingPlugins = []; + for (final File podspec in await _podspecsToLint()) { + if (!await _lintPodspec(podspec)) { + failingPlugins.add(p.basenameWithoutExtension(podspec.path)); + } + } + + _print('\n\n'); + if (failingPlugins.isNotEmpty) { + _print('The following plugins have podspec errors (see above):'); + for (final String plugin in failingPlugins) { + _print(' * $plugin'); + } + throw ToolExit(1); + } + } + + Future> _podspecsToLint() async { + final List podspecs = await getFiles().where((File entity) { + final String filePath = entity.path; + return p.extension(filePath) == '.podspec' && + !getStringListArg('skip') + .contains(p.basenameWithoutExtension(filePath)); + }).toList(); + + podspecs.sort( + (File a, File b) => p.basename(a.path).compareTo(p.basename(b.path))); + return podspecs; + } + + Future _lintPodspec(File podspec) async { + // Do not run the static analyzer on plugins with known analyzer issues. + final String podspecPath = podspec.path; + + final String podspecBasename = p.basename(podspecPath); + _print('Linting $podspecBasename'); + + // Lint plugin as framework (use_frameworks!). + final ProcessResult frameworkResult = + await _runPodLint(podspecPath, libraryLint: true); + _print(frameworkResult.stdout); + _print(frameworkResult.stderr); + + // Lint plugin as library. + final ProcessResult libraryResult = + await _runPodLint(podspecPath, libraryLint: false); + _print(libraryResult.stdout); + _print(libraryResult.stderr); + + return frameworkResult.exitCode == 0 && libraryResult.exitCode == 0; + } + + Future _runPodLint(String podspecPath, + {required bool libraryLint}) async { + final bool allowWarnings = (getStringListArg('ignore-warnings')) + .contains(p.basenameWithoutExtension(podspecPath)); + final List arguments = [ + 'lib', + 'lint', + podspecPath, + '--configuration=Debug', // Release targets unsupported arm64 simulators. Use Debug to only build against targeted x86_64 simulator devices. + '--skip-tests', + '--use-modular-headers', // Flutter sets use_modular_headers! in its templates. + if (allowWarnings) '--allow-warnings', + if (libraryLint) '--use-libraries' + ]; + + _print('Running "pod ${arguments.join(' ')}"'); + return processRunner.run('pod', arguments, + workingDir: packagesDir, stdoutEncoding: utf8, stderrEncoding: utf8); + } +} diff --git a/script/tool/lib/src/list_command.dart b/script/tool/lib/src/list_command.dart new file mode 100644 index 000000000000..f6b186e7ba2f --- /dev/null +++ b/script/tool/lib/src/list_command.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 'dart:async'; + +import 'package:file/file.dart'; + +import 'common.dart'; + +/// A command to list different types of repository content. +class ListCommand extends PluginCommand { + /// Creates an instance of the list command, whose behavior depends on the + /// 'type' argument it provides. + ListCommand(Directory packagesDir) : super(packagesDir) { + argParser.addOption( + _type, + defaultsTo: _plugin, + allowed: [_plugin, _example, _package, _file], + help: 'What type of file system content to list.', + ); + } + + static const String _type = 'type'; + static const String _plugin = 'plugin'; + static const String _example = 'example'; + static const String _package = 'package'; + static const String _file = 'file'; + + @override + final String name = 'list'; + + @override + final String description = 'Lists packages or files'; + + @override + Future run() async { + switch (getStringArg(_type)) { + case _plugin: + await for (final Directory package in getPlugins()) { + print(package.path); + } + break; + case _example: + await for (final Directory package in getExamples()) { + print(package.path); + } + break; + case _package: + await for (final Directory package in getPackages()) { + print(package.path); + } + break; + case _file: + await for (final File file in getFiles()) { + print(file.path); + } + break; + } + } +} diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart new file mode 100644 index 000000000000..a7603122186a --- /dev/null +++ b/script/tool/lib/src/main.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 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; + +import 'analyze_command.dart'; +import 'build_examples_command.dart'; +import 'common.dart'; +import 'create_all_plugins_app_command.dart'; +import 'drive_examples_command.dart'; +import 'firebase_test_lab_command.dart'; +import 'format_command.dart'; +import 'java_test_command.dart'; +import 'license_check_command.dart'; +import 'lint_podspecs_command.dart'; +import 'list_command.dart'; +import 'publish_check_command.dart'; +import 'publish_plugin_command.dart'; +import 'pubspec_check_command.dart'; +import 'test_command.dart'; +import 'version_check_command.dart'; +import 'xctest_command.dart'; + +void main(List args) { + const FileSystem fileSystem = LocalFileSystem(); + + Directory packagesDir = + fileSystem.currentDirectory.childDirectory('packages'); + + if (!packagesDir.existsSync()) { + if (fileSystem.currentDirectory.basename == 'packages') { + packagesDir = fileSystem.currentDirectory; + } else { + print('Error: Cannot find a "packages" sub-directory'); + io.exit(1); + } + } + + final CommandRunner commandRunner = CommandRunner( + 'pub global run flutter_plugin_tools', + 'Productivity utils for hosting multiple plugins within one repository.') + ..addCommand(AnalyzeCommand(packagesDir)) + ..addCommand(BuildExamplesCommand(packagesDir)) + ..addCommand(CreateAllPluginsAppCommand(packagesDir)) + ..addCommand(DriveExamplesCommand(packagesDir)) + ..addCommand(FirebaseTestLabCommand(packagesDir)) + ..addCommand(FormatCommand(packagesDir)) + ..addCommand(JavaTestCommand(packagesDir)) + ..addCommand(LicenseCheckCommand(packagesDir)) + ..addCommand(LintPodspecsCommand(packagesDir)) + ..addCommand(ListCommand(packagesDir)) + ..addCommand(PublishCheckCommand(packagesDir)) + ..addCommand(PublishPluginCommand(packagesDir)) + ..addCommand(PubspecCheckCommand(packagesDir)) + ..addCommand(TestCommand(packagesDir)) + ..addCommand(VersionCheckCommand(packagesDir)) + ..addCommand(XCTestCommand(packagesDir)); + + commandRunner.run(args).catchError((Object e) { + final ToolExit toolExit = e as ToolExit; + int exitCode = toolExit.exitCode; + // This should never happen; this check is here to guarantee that a ToolExit + // never accidentally has code 0 thus causing CI to pass. + if (exitCode == 0) { + assert(false); + exitCode = 255; + } + io.exit(exitCode); + }, test: (Object e) => e is ToolExit); +} diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart new file mode 100644 index 000000000000..b77eceecbf41 --- /dev/null +++ b/script/tool/lib/src/publish_check_command.dart @@ -0,0 +1,286 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:colorize/colorize.dart'; +import 'package:file/file.dart'; +import 'package:http/http.dart' as http; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; + +import 'common.dart'; + +/// A command to check that packages are publishable via 'dart publish'. +class PublishCheckCommand extends PluginCommand { + /// Creates an instance of the publish command. + PublishCheckCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + http.Client? httpClient, + }) : _pubVersionFinder = + PubVersionFinder(httpClient: httpClient ?? http.Client()), + super(packagesDir, processRunner: processRunner) { + argParser.addFlag( + _allowPrereleaseFlag, + help: 'Allows the pre-release SDK warning to pass.\n' + 'When enabled, a pub warning, which asks to publish the package as a pre-release version when ' + 'the SDK constraint is a pre-release version, is ignored.', + defaultsTo: false, + ); + argParser.addFlag(_machineFlag, + help: 'Switch outputs to a machine readable JSON. \n' + 'The JSON contains a "status" field indicating the final status of the command, the possible values are:\n' + ' $_statusNeedsPublish: There is at least one package need to be published. They also passed all publish checks.\n' + ' $_statusMessageNoPublish: There are no packages needs to be published. Either no pubspec change detected or all versions have already been published.\n' + ' $_statusMessageError: Some error has occurred.', + defaultsTo: false, + negatable: true); + } + + static const String _allowPrereleaseFlag = 'allow-pre-release'; + static const String _machineFlag = 'machine'; + static const String _statusNeedsPublish = 'needs-publish'; + static const String _statusMessageNoPublish = 'no-publish'; + static const String _statusMessageError = 'error'; + static const String _statusKey = 'status'; + static const String _humanMessageKey = 'humanMessage'; + + final List _validStatus = [ + _statusNeedsPublish, + _statusMessageNoPublish, + _statusMessageError + ]; + + @override + final String name = 'publish-check'; + + @override + final String description = + 'Checks to make sure that a plugin *could* be published.'; + + final PubVersionFinder _pubVersionFinder; + + // The output JSON when the _machineFlag is on. + final Map _machineOutput = {}; + + final List _humanMessages = []; + + @override + Future run() async { + final ZoneSpecification logSwitchSpecification = ZoneSpecification( + print: (Zone self, ZoneDelegate parent, Zone zone, String message) { + final bool logMachineMessage = getBoolArg(_machineFlag); + if (logMachineMessage && message != _prettyJson(_machineOutput)) { + _humanMessages.add(message); + } else { + parent.print(zone, message); + } + }); + + await runZoned(_runCommand, zoneSpecification: logSwitchSpecification); + } + + Future _runCommand() async { + final List failedPackages = []; + + String status = _statusMessageNoPublish; + await for (final Directory plugin in getPlugins()) { + final _PublishCheckResult result = await _passesPublishCheck(plugin); + switch (result) { + case _PublishCheckResult._notPublished: + if (failedPackages.isEmpty) { + status = _statusNeedsPublish; + } + break; + case _PublishCheckResult._published: + break; + case _PublishCheckResult._error: + failedPackages.add(plugin); + status = _statusMessageError; + break; + } + } + _pubVersionFinder.httpClient.close(); + + if (failedPackages.isNotEmpty) { + final String error = + 'The following ${failedPackages.length} package(s) failed the ' + 'publishing check:'; + final String joinedFailedPackages = failedPackages.join('\n'); + _printImportantStatusMessage('$error\n$joinedFailedPackages', + isError: true); + } else { + _printImportantStatusMessage('All packages passed publish check!', + isError: false); + } + + if (getBoolArg(_machineFlag)) { + _setStatus(status); + _machineOutput[_humanMessageKey] = _humanMessages; + print(_prettyJson(_machineOutput)); + } + + if (failedPackages.isNotEmpty) { + throw ToolExit(1); + } + } + + Pubspec? _tryParsePubspec(Directory package) { + final File pubspecFile = package.childFile('pubspec.yaml'); + + try { + return Pubspec.parse(pubspecFile.readAsStringSync()); + } on Exception catch (exception) { + print( + 'Failed to parse `pubspec.yaml` at ${pubspecFile.path}: $exception}', + ); + return null; + } + } + + Future _hasValidPublishCheckRun(Directory package) async { + final io.Process process = await processRunner.start( + 'flutter', + ['pub', 'publish', '--', '--dry-run'], + workingDirectory: package, + ); + + final StringBuffer outputBuffer = StringBuffer(); + + final Completer stdOutCompleter = Completer(); + process.stdout.listen( + (List event) { + final String output = String.fromCharCodes(event); + if (output.isNotEmpty) { + print(output); + outputBuffer.write(output); + } + }, + onDone: () => stdOutCompleter.complete(), + ); + + final Completer stdInCompleter = Completer(); + process.stderr.listen( + (List event) { + final String output = String.fromCharCodes(event); + if (output.isNotEmpty) { + // The final result is always printed on stderr, whether success or + // failure. + final bool isError = !output.contains('has 0 warnings'); + _printImportantStatusMessage(output, isError: isError); + outputBuffer.write(output); + } + }, + onDone: () => stdInCompleter.complete(), + ); + + if (await process.exitCode == 0) { + return true; + } + + if (!getBoolArg(_allowPrereleaseFlag)) { + return false; + } + + await stdOutCompleter.future; + await stdInCompleter.future; + + final String output = outputBuffer.toString(); + return output.contains('Package has 1 warning') && + output.contains( + 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'); + } + + Future<_PublishCheckResult> _passesPublishCheck(Directory package) async { + final String packageName = package.basename; + print('Checking that $packageName can be published.'); + + final Pubspec? pubspec = _tryParsePubspec(package); + if (pubspec == null) { + print('no pubspec'); + return _PublishCheckResult._error; + } else if (pubspec.publishTo == 'none') { + print('Package $packageName is marked as unpublishable. Skipping.'); + return _PublishCheckResult._published; + } + + final Version? version = pubspec.version; + final _PublishCheckResult alreadyPublishedResult = + await _checkIfAlreadyPublished( + packageName: packageName, version: version); + if (alreadyPublishedResult == _PublishCheckResult._published) { + print( + 'Package $packageName version: $version has already be published on pub.'); + return alreadyPublishedResult; + } else if (alreadyPublishedResult == _PublishCheckResult._error) { + print('Check pub version failed $packageName'); + return _PublishCheckResult._error; + } + + if (await _hasValidPublishCheckRun(package)) { + print('Package $packageName is able to be published.'); + return _PublishCheckResult._notPublished; + } else { + print('Unable to publish $packageName'); + return _PublishCheckResult._error; + } + } + + // Check if `packageName` already has `version` published on pub. + Future<_PublishCheckResult> _checkIfAlreadyPublished( + {required String packageName, required Version? version}) async { + final PubVersionFinderResponse pubVersionFinderResponse = + await _pubVersionFinder.getPackageVersion(package: packageName); + switch (pubVersionFinderResponse.result) { + case PubVersionFinderResult.success: + return pubVersionFinderResponse.versions.contains(version) + ? _PublishCheckResult._published + : _PublishCheckResult._notPublished; + case PubVersionFinderResult.fail: + print(''' +Error fetching version on pub for $packageName. +HTTP Status ${pubVersionFinderResponse.httpResponse.statusCode} +HTTP response: ${pubVersionFinderResponse.httpResponse.body} +'''); + return _PublishCheckResult._error; + case PubVersionFinderResult.noPackageFound: + return _PublishCheckResult._notPublished; + } + } + + void _setStatus(String status) { + assert(_validStatus.contains(status)); + _machineOutput[_statusKey] = status; + } + + String _prettyJson(Map map) { + return const JsonEncoder.withIndent(' ').convert(_machineOutput); + } + + void _printImportantStatusMessage(String message, {required bool isError}) { + final String statusMessage = '${isError ? 'ERROR' : 'SUCCESS'}: $message'; + if (getBoolArg(_machineFlag)) { + print(statusMessage); + } else { + final Colorize colorizedMessage = Colorize(statusMessage); + if (isError) { + colorizedMessage.red(); + } else { + colorizedMessage.green(); + } + print(colorizedMessage); + } + } +} + +enum _PublishCheckResult { + _notPublished, + + _published, + + _error, +} diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart new file mode 100644 index 000000000000..1e7c15029846 --- /dev/null +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -0,0 +1,565 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:yaml/yaml.dart'; + +import 'common.dart'; + +@immutable +class _RemoteInfo { + const _RemoteInfo({required this.name, required this.url}); + + /// The git name for the remote. + final String name; + + /// The remote's URL. + final String url; +} + +/// Wraps pub publish with a few niceties used by the flutter/plugin team. +/// +/// 1. Checks for any modified files in git and refuses to publish if there's an +/// issue. +/// 2. Tags the release with the format -v. +/// 3. Pushes the release to a remote. +/// +/// Both 2 and 3 are optional, see `plugin_tools help publish-plugin` for full +/// usage information. +/// +/// [processRunner], [print], and [stdin] can be overriden for easier testing. +class PublishPluginCommand extends PluginCommand { + /// Creates an instance of the publish command. + PublishPluginCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Print print = print, + io.Stdin? stdinput, + GitDir? gitDir, + }) : _print = print, + _stdin = stdinput ?? io.stdin, + super(packagesDir, processRunner: processRunner, gitDir: gitDir) { + argParser.addOption( + _packageOption, + help: 'The package to publish.' + 'If the package directory name is different than its pubspec.yaml name, then this should specify the directory.', + ); + argParser.addMultiOption(_pubFlagsOption, + help: + 'A list of options that will be forwarded on to pub. Separate multiple flags with commas.'); + argParser.addFlag( + _tagReleaseOption, + help: 'Whether or not to tag the release.', + defaultsTo: true, + negatable: true, + ); + argParser.addFlag( + _pushTagsOption, + help: + 'Whether or not tags should be pushed to a remote after creation. Ignored if tag-release is false.', + defaultsTo: true, + negatable: true, + ); + argParser.addOption( + _remoteOption, + help: + 'The name of the remote to push the tags to. Ignored if push-tags or tag-release is false.', + // Flutter convention is to use "upstream" for the single source of truth, and "origin" for personal forks. + defaultsTo: 'upstream', + ); + argParser.addFlag( + _allChangedFlag, + help: + 'Release all plugins that contains pubspec changes at the current commit compares to the base-sha.\n' + 'The $_packageOption option is ignored if this is on.', + defaultsTo: false, + ); + argParser.addFlag( + _dryRunFlag, + help: + 'Skips the real `pub publish` and `git tag` commands and assumes both commands are successful.\n' + 'This does not run `pub publish --dry-run`.\n' + 'If you want to run the command with `pub publish --dry-run`, use `pub-publish-flags=--dry-run`', + defaultsTo: false, + negatable: true, + ); + argParser.addFlag(_skipConfirmationFlag, + help: 'Run the command without asking for Y/N inputs.\n' + 'This command will add a `--force` flag to the `pub publish` command if it is not added with $_pubFlagsOption\n' + 'It also skips the y/n inputs when pushing tags to remote.\n', + defaultsTo: false, + negatable: true); + } + + static const String _packageOption = 'package'; + static const String _tagReleaseOption = 'tag-release'; + static const String _pushTagsOption = 'push-tags'; + static const String _pubFlagsOption = 'pub-publish-flags'; + static const String _remoteOption = 'remote'; + static const String _allChangedFlag = 'all-changed'; + static const String _dryRunFlag = 'dry-run'; + static const String _skipConfirmationFlag = 'skip-confirmation'; + + static const String _pubCredentialName = 'PUB_CREDENTIALS'; + + // Version tags should follow -v. For example, + // `flutter_plugin_tools-v0.0.24`. + static const String _tagFormat = '%PACKAGE%-v%VERSION%'; + + @override + final String name = 'publish-plugin'; + + @override + final String description = + 'Attempts to publish the given plugin and tag its release on GitHub.\n' + 'If running this on CI, an environment variable named $_pubCredentialName must be set to a String that represents the pub credential JSON.\n' + 'WARNING: Do not check in the content of pub credential JSON, it should only come from secure sources.'; + + final Print _print; + final io.Stdin _stdin; + StreamSubscription? _stdinSubscription; + + @override + Future run() async { + final String package = getStringArg(_packageOption); + final bool publishAllChanged = getBoolArg(_allChangedFlag); + if (package.isEmpty && !publishAllChanged) { + _print( + 'Must specify a package to publish. See `plugin_tools help publish-plugin`.'); + throw ToolExit(1); + } + + _print('Checking local repo...'); + // Ensure there are no symlinks in the path, as it can break + // GitDir's allowSubdirectory:true. + final String packagesPath = packagesDir.resolveSymbolicLinksSync(); + if (!await GitDir.isGitDir(packagesPath)) { + _print('$packagesPath is not a valid Git repository.'); + throw ToolExit(1); + } + final GitDir baseGitDir = gitDir ?? + await GitDir.fromExisting(packagesPath, allowSubdirectory: true); + + final bool shouldPushTag = getBoolArg(_pushTagsOption); + _RemoteInfo? remote; + if (shouldPushTag) { + final String remoteName = getStringArg(_remoteOption); + final String? remoteUrl = await _verifyRemote(remoteName); + if (remoteUrl == null) { + printError( + 'Unable to find URL for remote $remoteName; cannot push tags'); + throw ToolExit(1); + } + remote = _RemoteInfo(name: remoteName, url: remoteUrl); + } + _print('Local repo is ready!'); + if (getBoolArg(_dryRunFlag)) { + _print('=============== DRY RUN ==============='); + } + + bool successful; + if (publishAllChanged) { + successful = await _publishAllChangedPackages( + baseGitDir: baseGitDir, + remoteForTagPush: remote, + ); + } else { + successful = await _publishAndTagPackage( + packageDir: _getPackageDir(package), + remoteForTagPush: remote, + ); + } + await _finish(successful); + } + + Future _publishAllChangedPackages({ + required GitDir baseGitDir, + _RemoteInfo? remoteForTagPush, + }) async { + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + final List changedPubspecs = + await gitVersionFinder.getChangedPubSpecs(); + if (changedPubspecs.isEmpty) { + _print('No version updates in this commit.'); + return true; + } + _print('Getting existing tags...'); + final io.ProcessResult existingTagsResult = + await baseGitDir.runCommand(['tag', '--sort=-committerdate']); + final List existingTags = (existingTagsResult.stdout as String) + .split('\n') + ..removeWhere((String element) => element.isEmpty); + + final List packagesReleased = []; + final List packagesFailed = []; + + for (final String pubspecPath in changedPubspecs) { + final File pubspecFile = packagesDir.fileSystem + .directory(baseGitDir.path) + .childFile(pubspecPath); + final _CheckNeedsReleaseResult result = await _checkNeedsRelease( + pubspecFile: pubspecFile, + gitVersionFinder: gitVersionFinder, + existingTags: existingTags, + ); + switch (result) { + case _CheckNeedsReleaseResult.release: + break; + case _CheckNeedsReleaseResult.noRelease: + continue; + case _CheckNeedsReleaseResult.failure: + packagesFailed.add(pubspecFile.parent.basename); + continue; + } + _print('\n'); + if (await _publishAndTagPackage( + packageDir: pubspecFile.parent, + remoteForTagPush: remoteForTagPush, + )) { + packagesReleased.add(pubspecFile.parent.basename); + } else { + packagesFailed.add(pubspecFile.parent.basename); + } + _print('\n'); + } + if (packagesReleased.isNotEmpty) { + _print('Packages released: ${packagesReleased.join(', ')}'); + } + if (packagesFailed.isNotEmpty) { + _print( + 'Failed to release the following packages: ${packagesFailed.join(', ')}, see above for details.'); + } + return packagesFailed.isEmpty; + } + + // Publish the package to pub with `pub publish`. + // If `_tagReleaseOption` is on, git tag the release. + // If `remoteForTagPush` is non-null, the tag will be pushed to that remote. + // Returns `true` if publishing and tagging are successful. + Future _publishAndTagPackage({ + required Directory packageDir, + _RemoteInfo? remoteForTagPush, + }) async { + if (!await _publishPlugin(packageDir: packageDir)) { + return false; + } + if (getBoolArg(_tagReleaseOption)) { + if (!await _tagRelease( + packageDir: packageDir, + remoteForPush: remoteForTagPush, + )) { + return false; + } + } + _print('Released [${packageDir.basename}] successfully.'); + return true; + } + + // Returns a [_CheckNeedsReleaseResult] that indicates the result. + Future<_CheckNeedsReleaseResult> _checkNeedsRelease({ + required File pubspecFile, + required GitVersionFinder gitVersionFinder, + required List existingTags, + }) async { + if (!pubspecFile.existsSync()) { + _print(''' +The file at The pubspec file at ${pubspecFile.path} does not exist. Publishing will not happen for ${pubspecFile.parent.basename}. +Safe to ignore if the package is deleted in this commit. +'''); + return _CheckNeedsReleaseResult.noRelease; + } + + final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + if (pubspec.publishTo == 'none') { + return _CheckNeedsReleaseResult.noRelease; + } + + if (pubspec.version == null) { + _print( + 'No version found. A package that intentionally has no version should be marked "publish_to: none"'); + return _CheckNeedsReleaseResult.failure; + } + + // Get latest tagged version and compare with the current version. + // TODO(cyanglaz): Check latest version of the package on pub instead of git + // https://github.com/flutter/flutter/issues/81047 + + final String latestTag = existingTags.firstWhere( + (String tag) => tag.split('-v').first == pubspec.name, + orElse: () => ''); + if (latestTag.isNotEmpty) { + final String latestTaggedVersion = latestTag.split('-v').last; + final Version latestVersion = Version.parse(latestTaggedVersion); + if (pubspec.version! < latestVersion) { + _print( + 'The new version (${pubspec.version}) is lower than the current version ($latestVersion) for ${pubspec.name}.\nThis git commit is a revert, no release is tagged.'); + return _CheckNeedsReleaseResult.noRelease; + } + } + return _CheckNeedsReleaseResult.release; + } + + // Publish the plugin. + // + // Returns `true` if successful, `false` otherwise. + Future _publishPlugin({required Directory packageDir}) async { + final bool gitStatusOK = await _checkGitStatus(packageDir); + if (!gitStatusOK) { + return false; + } + final bool publishOK = await _publish(packageDir); + if (!publishOK) { + return false; + } + _print('Package published!'); + return true; + } + + // Tag the release with -v, and, if [remoteForTagPush] + // is provided, push it to that remote. + // + // Return `true` if successful, `false` otherwise. + Future _tagRelease({ + required Directory packageDir, + _RemoteInfo? remoteForPush, + }) async { + final String tag = _getTag(packageDir); + _print('Tagging release $tag...'); + if (!getBoolArg(_dryRunFlag)) { + final io.ProcessResult result = await processRunner.run( + 'git', + ['tag', tag], + workingDir: packageDir, + exitOnError: false, + logOnError: true, + ); + if (result.exitCode != 0) { + return false; + } + } + + if (remoteForPush == null) { + return true; + } + + _print('Pushing tag to ${remoteForPush.name}...'); + return await _pushTagToRemote( + tag: tag, + remote: remoteForPush, + ); + } + + Future _finish(bool successful) async { + await _stdinSubscription?.cancel(); + _stdinSubscription = null; + if (successful) { + _print('Done!'); + } else { + _print('Failed, see above for details.'); + throw ToolExit(1); + } + } + + // Returns the packageDirectory based on the package name. + // Throws ToolExit if the `package` doesn't exist. + Directory _getPackageDir(String package) { + final Directory packageDir = packagesDir.childDirectory(package); + if (!packageDir.existsSync()) { + _print('${packageDir.absolute.path} does not exist.'); + throw ToolExit(1); + } + return packageDir; + } + + Future _checkGitStatus(Directory packageDir) async { + final io.ProcessResult statusResult = await processRunner.run( + 'git', + ['status', '--porcelain', '--ignored', packageDir.absolute.path], + workingDir: packageDir, + logOnError: true, + exitOnError: false, + ); + if (statusResult.exitCode != 0) { + return false; + } + + final String statusOutput = statusResult.stdout as String; + if (statusOutput.isNotEmpty) { + _print( + "There are files in the package directory that haven't been saved in git. Refusing to publish these files:\n\n" + '$statusOutput\n' + 'If the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.'); + } + return statusOutput.isEmpty; + } + + Future _verifyRemote(String remote) async { + final io.ProcessResult getRemoteUrlResult = await processRunner.run( + 'git', + ['remote', 'get-url', remote], + workingDir: packagesDir, + exitOnError: true, + logOnError: true, + ); + return getRemoteUrlResult.stdout as String?; + } + + Future _publish(Directory packageDir) async { + final List publishFlags = getStringListArg(_pubFlagsOption); + _print( + 'Running `pub publish ${publishFlags.join(' ')}` in ${packageDir.absolute.path}...\n'); + if (getBoolArg(_dryRunFlag)) { + return true; + } + + if (getBoolArg(_skipConfirmationFlag)) { + publishFlags.add('--force'); + } + if (publishFlags.contains('--force')) { + _ensureValidPubCredential(); + } + + final io.Process publish = await processRunner.start( + 'flutter', ['pub', 'publish'] + publishFlags, + workingDirectory: packageDir); + publish.stdout + .transform(utf8.decoder) + .listen((String data) => _print(data)); + publish.stderr + .transform(utf8.decoder) + .listen((String data) => _print(data)); + _stdinSubscription ??= _stdin + .transform(utf8.decoder) + .listen((String data) => publish.stdin.writeln(data)); + final int result = await publish.exitCode; + if (result != 0) { + _print('Publish ${packageDir.basename} failed.'); + return false; + } + return true; + } + + String _getTag(Directory packageDir) { + final File pubspecFile = packageDir.childFile('pubspec.yaml'); + final YamlMap pubspecYaml = + loadYaml(pubspecFile.readAsStringSync()) as YamlMap; + final String name = pubspecYaml['name'] as String; + final String version = pubspecYaml['version'] as String; + // We should have failed to publish if these were unset. + assert(name.isNotEmpty && version.isNotEmpty); + return _tagFormat + .replaceAll('%PACKAGE%', name) + .replaceAll('%VERSION%', version); + } + + // Pushes the `tag` to `remote` + // + // Return `true` if successful, `false` otherwise. + Future _pushTagToRemote({ + required String tag, + required _RemoteInfo remote, + }) async { + assert(remote != null && tag != null); + if (!getBoolArg(_skipConfirmationFlag)) { + _print('Ready to push $tag to ${remote.url} (y/n)?'); + final String? input = _stdin.readLineSync(); + if (input?.toLowerCase() != 'y') { + _print('Tag push canceled.'); + return false; + } + } + if (!getBoolArg(_dryRunFlag)) { + final io.ProcessResult result = await processRunner.run( + 'git', + ['push', remote.name, tag], + workingDir: packagesDir, + exitOnError: false, + logOnError: true, + ); + if (result.exitCode != 0) { + return false; + } + } + return true; + } + + void _ensureValidPubCredential() { + final String credentialsPath = _credentialsPath; + final File credentialFile = packagesDir.fileSystem.file(credentialsPath); + if (credentialFile.existsSync() && + credentialFile.readAsStringSync().isNotEmpty) { + return; + } + final String? credential = io.Platform.environment[_pubCredentialName]; + if (credential == null) { + printError(''' +No pub credential available. Please check if `$credentialsPath` is valid. +If running this command on CI, you can set the pub credential content in the $_pubCredentialName environment variable. +'''); + throw ToolExit(1); + } + credentialFile.openSync(mode: FileMode.writeOnlyAppend) + ..writeStringSync(credential) + ..closeSync(); + } + + /// Returns the correct path where the pub credential is stored. + @visibleForTesting + static String getCredentialPath() { + return _credentialsPath; + } +} + +/// The path in which pub expects to find its credentials file. +final String _credentialsPath = () { + // This follows the same logic as pub: + // https://github.com/dart-lang/pub/blob/d99b0d58f4059d7bb4ac4616fd3d54ec00a2b5d4/lib/src/system_cache.dart#L34-L43 + String? cacheDir; + final String? pubCache = io.Platform.environment['PUB_CACHE']; + print(pubCache); + if (pubCache != null) { + cacheDir = pubCache; + } else if (io.Platform.isWindows) { + final String? appData = io.Platform.environment['APPDATA']; + if (appData == null) { + printError('"APPDATA" environment variable is not set.'); + } else { + cacheDir = p.join(appData, 'Pub', 'Cache'); + } + } else { + final String? home = io.Platform.environment['HOME']; + if (home == null) { + printError('"HOME" environment variable is not set.'); + } else { + cacheDir = p.join(home, '.pub-cache'); + } + } + + if (cacheDir == null) { + printError('Unable to determine pub cache location'); + throw ToolExit(1); + } + + return p.join(cacheDir, 'credentials.json'); +}(); + +enum _CheckNeedsReleaseResult { + // The package needs to be released. + release, + + // The package does not need to be released. + noRelease, + + // There's an error when trying to determine whether the package needs to be released. + failure, +} diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart new file mode 100644 index 000000000000..878b683dbbb8 --- /dev/null +++ b/script/tool/lib/src/pubspec_check_command.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 'dart:async'; + +import 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:path/path.dart' as p; +import 'package:pubspec_parse/pubspec_parse.dart'; + +import 'common.dart'; + +/// A command to enforce pubspec conventions across the repository. +/// +/// This both ensures that repo best practices for which optional fields are +/// used are followed, and that the structure is consistent to make edits +/// across multiple pubspec files easier. +class PubspecCheckCommand extends PluginCommand { + /// Creates an instance of the version check command. + PubspecCheckCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + GitDir? gitDir, + }) : super(packagesDir, processRunner: processRunner, gitDir: gitDir); + + // Section order for plugins. Because the 'flutter' section is critical + // information for plugins, and usually small, it goes near the top unlike in + // a normal app or package. + static const List _majorPluginSections = [ + 'environment:', + 'flutter:', + 'dependencies:', + 'dev_dependencies:', + ]; + + static const List _majorPackageSections = [ + 'environment:', + 'dependencies:', + 'dev_dependencies:', + 'flutter:', + ]; + + static const String _expectedIssueLinkFormat = + 'https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A'; + + @override + final String name = 'pubspec-check'; + + @override + final String description = + 'Checks that pubspecs follow repository conventions.'; + + @override + Future run() async { + final List failingPackages = []; + await for (final Directory package in getPackages()) { + final String relativePackagePath = + p.relative(package.path, from: packagesDir.path); + print('Checking $relativePackagePath...'); + final File pubspec = package.childFile('pubspec.yaml'); + final bool passesCheck = !pubspec.existsSync() || + await _checkPubspec(pubspec, packageName: package.basename); + if (!passesCheck) { + failingPackages.add(relativePackagePath); + } + } + + if (failingPackages.isNotEmpty) { + print('The following packages have pubspec issues:'); + for (final String package in failingPackages) { + print(' $package'); + } + throw ToolExit(1); + } + + print('\nNo pubspec issues found!'); + } + + Future _checkPubspec( + File pubspecFile, { + required String packageName, + }) async { + const String indentation = ' '; + final String contents = pubspecFile.readAsStringSync(); + final Pubspec? pubspec = _tryParsePubspec(contents); + if (pubspec == null) { + return false; + } + + final List pubspecLines = contents.split('\n'); + final List sectionOrder = pubspecLines.contains(' plugin:') + ? _majorPluginSections + : _majorPackageSections; + bool passing = _checkSectionOrder(pubspecLines, sectionOrder); + if (!passing) { + print('${indentation}Major sections should follow standard ' + 'repository ordering:'); + final String listIndentation = indentation * 2; + print('$listIndentation${sectionOrder.join('\n$listIndentation')}'); + } + + if (pubspec.publishTo != 'none') { + final List repositoryErrors = + _checkForRepositoryLinkErrors(pubspec, packageName: packageName); + if (repositoryErrors.isNotEmpty) { + for (final String error in repositoryErrors) { + print('$indentation$error'); + } + passing = false; + } + + if (!_checkIssueLink(pubspec)) { + print( + '${indentation}A package should have an "issue_tracker" link to a ' + 'search for open flutter/flutter bugs with the relevant label:\n' + '${indentation * 2}$_expectedIssueLinkFormat'); + passing = false; + } + } + + return passing; + } + + Pubspec? _tryParsePubspec(String pubspecContents) { + try { + return Pubspec.parse(pubspecContents); + } on Exception catch (exception) { + print(' Cannot parse pubspec.yaml: $exception'); + } + return null; + } + + bool _checkSectionOrder( + List pubspecLines, List sectionOrder) { + int previousSectionIndex = 0; + for (final String line in pubspecLines) { + final int index = sectionOrder.indexOf(line); + if (index == -1) { + continue; + } + if (index < previousSectionIndex) { + return false; + } + previousSectionIndex = index; + } + return true; + } + + List _checkForRepositoryLinkErrors( + Pubspec pubspec, { + required String packageName, + }) { + final List errorMessages = []; + if (pubspec.repository == null) { + errorMessages.add('Missing "repository"'); + } else if (!pubspec.repository!.path.endsWith(packageName)) { + errorMessages + .add('The "repository" link should end with the package name.'); + } + + if (pubspec.homepage != null) { + errorMessages + .add('Found a "homepage" entry; only "repository" should be used.'); + } + + return errorMessages; + } + + bool _checkIssueLink(Pubspec pubspec) { + return pubspec.issueTracker + ?.toString() + .startsWith(_expectedIssueLinkFormat) == + true; + } +} diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart new file mode 100644 index 000000000000..0174b986eb63 --- /dev/null +++ b/script/tool/lib/src/test_command.dart @@ -0,0 +1,102 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +/// A command to run Dart unit tests for packages. +class TestCommand extends PluginCommand { + /// Creates an instance of the test command. + TestCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, processRunner: processRunner) { + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: 'Runs the tests in Dart VM with the given experiments enabled.', + ); + } + + @override + final String name = 'test'; + + @override + final String description = 'Runs the Dart tests for all packages.\n\n' + 'This command requires "flutter" to be in your path.'; + + @override + Future run() async { + final List failingPackages = []; + await for (final Directory packageDir in getPackages()) { + final String packageName = + p.relative(packageDir.path, from: packagesDir.path); + if (!packageDir.childDirectory('test').existsSync()) { + print('SKIPPING $packageName - no test subdirectory'); + continue; + } + + print('RUNNING $packageName tests...'); + + final String enableExperiment = getStringArg(kEnableExperiment); + + // `flutter test` automatically gets packages. `pub run test` does not. :( + int exitCode = 0; + if (isFlutterPackage(packageDir)) { + final List args = [ + 'test', + '--color', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ]; + + if (isWebPlugin(packageDir)) { + args.add('--platform=chrome'); + } + exitCode = await processRunner.runAndStream( + 'flutter', + args, + workingDir: packageDir, + ); + } else { + exitCode = await processRunner.runAndStream( + 'dart', + ['pub', 'get'], + workingDir: packageDir, + ); + if (exitCode == 0) { + exitCode = await processRunner.runAndStream( + 'dart', + [ + 'pub', + 'run', + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + 'test', + ], + workingDir: packageDir, + ); + } + } + if (exitCode != 0) { + failingPackages.add(packageName); + } + } + + print('\n\n'); + if (failingPackages.isNotEmpty) { + print('Tests for the following packages are failing (see above):'); + for (final String package in failingPackages) { + print(' * $package'); + } + throw ToolExit(1); + } + + print('All tests are passing!'); + } +} diff --git a/script/tool/lib/src/version_check_command.dart b/script/tool/lib/src/version_check_command.dart new file mode 100644 index 000000000000..6baa38e465a2 --- /dev/null +++ b/script/tool/lib/src/version_check_command.dart @@ -0,0 +1,343 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:http/http.dart' as http; +import 'package:meta/meta.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; + +import 'common.dart'; + +/// Categories of version change types. +enum NextVersionType { + /// A breaking change. + BREAKING_MAJOR, + + /// A minor change (e.g., added feature). + MINOR, + + /// A bugfix change. + PATCH, + + /// The release of an existing prerelease version. + RELEASE, +} + +/// Returns the set of allowed next versions, with their change type, for +/// [masterVersion]. +/// +/// [headVerison] is used to check whether this is a pre-1.0 version bump, as +/// those have different semver rules. +@visibleForTesting +Map getAllowedNextVersions( + {required Version masterVersion, required Version headVersion}) { + final Map allowedNextVersions = + { + masterVersion.nextMajor: NextVersionType.BREAKING_MAJOR, + masterVersion.nextMinor: NextVersionType.MINOR, + masterVersion.nextPatch: NextVersionType.PATCH, + }; + + if (masterVersion.major < 1 && headVersion.major < 1) { + int nextBuildNumber = -1; + if (masterVersion.build.isEmpty) { + nextBuildNumber = 1; + } else { + final int currentBuildNumber = masterVersion.build.first as int; + nextBuildNumber = currentBuildNumber + 1; + } + final Version preReleaseVersion = Version( + masterVersion.major, + masterVersion.minor, + masterVersion.patch, + build: nextBuildNumber.toString(), + ); + allowedNextVersions.clear(); + allowedNextVersions[masterVersion.nextMajor] = NextVersionType.RELEASE; + allowedNextVersions[masterVersion.nextMinor] = + NextVersionType.BREAKING_MAJOR; + allowedNextVersions[masterVersion.nextPatch] = NextVersionType.MINOR; + allowedNextVersions[preReleaseVersion] = NextVersionType.PATCH; + } + return allowedNextVersions; +} + +/// A command to validate version changes to packages. +class VersionCheckCommand extends PluginCommand { + /// Creates an instance of the version check command. + VersionCheckCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + GitDir? gitDir, + http.Client? httpClient, + }) : _pubVersionFinder = + PubVersionFinder(httpClient: httpClient ?? http.Client()), + super(packagesDir, processRunner: processRunner, gitDir: gitDir) { + argParser.addFlag( + _againstPubFlag, + help: 'Whether the version check should run against the version on pub.\n' + 'Defaults to false, which means the version check only run against the previous version in code.', + defaultsTo: false, + negatable: true, + ); + } + + static const String _againstPubFlag = 'against-pub'; + + @override + final String name = 'version-check'; + + @override + final String description = + 'Checks if the versions of the plugins have been incremented per pub specification.\n' + 'Also checks if the latest version in CHANGELOG matches the version in pubspec.\n\n' + 'This command requires "pub" and "flutter" to be in your path.'; + + final PubVersionFinder _pubVersionFinder; + + @override + Future run() async { + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + + final List changedPubspecs = + await gitVersionFinder.getChangedPubSpecs(); + + final List badVersionChangePubspecs = []; + + const String indentation = ' '; + for (final String pubspecPath in changedPubspecs) { + print('Checking versions for $pubspecPath...'); + final File pubspecFile = packagesDir.fileSystem.file(pubspecPath); + if (!pubspecFile.existsSync()) { + print('${indentation}Deleted; skipping.'); + continue; + } + final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + if (pubspec.publishTo == 'none') { + print('${indentation}Found "publish_to: none"; skipping.'); + continue; + } + + final Version? headVersion = + await gitVersionFinder.getPackageVersion(pubspecPath, gitRef: 'HEAD'); + if (headVersion == null) { + printError('${indentation}No version found. A package that ' + 'intentionally has no version should be marked ' + '"publish_to: none".'); + badVersionChangePubspecs.add(pubspecPath); + continue; + } + Version? sourceVersion; + if (getBoolArg(_againstPubFlag)) { + final String packageName = pubspecFile.parent.basename; + final PubVersionFinderResponse pubVersionFinderResponse = + await _pubVersionFinder.getPackageVersion(package: packageName); + switch (pubVersionFinderResponse.result) { + case PubVersionFinderResult.success: + sourceVersion = pubVersionFinderResponse.versions.first; + print( + '$indentation$packageName: Current largest version on pub: $sourceVersion'); + break; + case PubVersionFinderResult.fail: + printError(''' +${indentation}Error fetching version on pub for $packageName. +${indentation}HTTP Status ${pubVersionFinderResponse.httpResponse.statusCode} +${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} +'''); + badVersionChangePubspecs.add(pubspecPath); + continue; + case PubVersionFinderResult.noPackageFound: + sourceVersion = null; + break; + } + } else { + sourceVersion = await gitVersionFinder.getPackageVersion(pubspecPath); + } + if (sourceVersion == null) { + String safeToIgnoreMessage; + if (getBoolArg(_againstPubFlag)) { + safeToIgnoreMessage = + '${indentation}Unable to find package on pub server.'; + } else { + safeToIgnoreMessage = + '${indentation}Unable to find pubspec in master.'; + } + print('$safeToIgnoreMessage Safe to ignore if the project is new.'); + continue; + } + + if (sourceVersion == headVersion) { + print('${indentation}No version change.'); + continue; + } + + // Check for reverts when doing local validation. + if (!getBoolArg(_againstPubFlag) && headVersion < sourceVersion) { + final Map possibleVersionsFromNewVersion = + getAllowedNextVersions( + masterVersion: headVersion, headVersion: sourceVersion); + // 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(sourceVersion)) { + print('${indentation}New version is lower than previous version. ' + 'This is assumed to be a revert.'); + continue; + } + } + + final Map allowedNextVersions = + getAllowedNextVersions( + masterVersion: sourceVersion, headVersion: headVersion); + + if (!allowedNextVersions.containsKey(headVersion)) { + final String source = (getBoolArg(_againstPubFlag)) ? 'pub' : 'master'; + printError('${indentation}Incorrectly updated version.\n' + '${indentation}HEAD: $headVersion, $source: $sourceVersion.\n' + '${indentation}Allowed versions: $allowedNextVersions'); + badVersionChangePubspecs.add(pubspecPath); + continue; + } else { + print('$indentation$headVersion -> $sourceVersion'); + } + + final bool isPlatformInterface = + pubspec.name.endsWith('_platform_interface'); + if (isPlatformInterface && + allowedNextVersions[headVersion] == NextVersionType.BREAKING_MAJOR) { + printError('$pubspecPath breaking change detected.\n' + 'Breaking changes to platform interfaces are strongly discouraged.\n'); + badVersionChangePubspecs.add(pubspecPath); + continue; + } + } + _pubVersionFinder.httpClient.close(); + + // TODO(stuartmorgan): Unify the way iteration works for these checks; the + // two checks shouldn't be operating independently on different lists. + final List mismatchedVersionPlugins = []; + await for (final Directory plugin in getPlugins()) { + if (!(await _checkVersionsMatch(plugin))) { + mismatchedVersionPlugins.add(plugin.basename); + } + } + + bool passed = true; + if (badVersionChangePubspecs.isNotEmpty) { + passed = false; + printError(''' +The following pubspecs failed validaton: +$indentation${badVersionChangePubspecs.join('\n$indentation')} +'''); + } + if (mismatchedVersionPlugins.isNotEmpty) { + passed = false; + printError(''' +The following pubspecs have different versions in pubspec.yaml and CHANGELOG.md: +$indentation${mismatchedVersionPlugins.join('\n$indentation')} +'''); + } + if (!passed) { + throw ToolExit(1); + } + + print('No version check errors found!'); + } + + /// Returns whether or not the pubspec version and CHANGELOG version for + /// [plugin] match. + Future _checkVersionsMatch(Directory plugin) async { + // get version from pubspec + final String packageName = plugin.basename; + print('-----------------------------------------'); + print( + 'Checking the first version listed in CHANGELOG.md matches the version in pubspec.yaml for $packageName.'); + + final Pubspec? pubspec = _tryParsePubspec(plugin); + if (pubspec == null) { + printError('Cannot parse version from pubspec.yaml'); + return false; + } + final Version? fromPubspec = pubspec.version; + + // get first version from CHANGELOG + final File changelog = plugin.childFile('CHANGELOG.md'); + final List lines = changelog.readAsLinesSync(); + String? firstLineWithText; + final Iterator iterator = lines.iterator; + while (iterator.moveNext()) { + if (iterator.current.trim().isNotEmpty) { + firstLineWithText = iterator.current.trim(); + break; + } + } + // Remove all leading mark down syntax from the version line. + String? versionString = firstLineWithText?.split(' ').last; + + // 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) { + print('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. + while (iterator.moveNext()) { + if (iterator.current.trim().startsWith('## ')) { + versionString = iterator.current.trim().split(' ').last; + break; + } + } + } + + final Version? fromChangeLog = + versionString == null ? null : Version.parse(versionString); + if (fromChangeLog == null) { + printError( + 'Cannot find version on the first line of ${plugin.path}/CHANGELOG.md'); + return false; + } + + if (fromPubspec != fromChangeLog) { + printError(''' +versions for $packageName in CHANGELOG.md and pubspec.yaml do not match. +The version in pubspec.yaml is $fromPubspec. +The first version listed in CHANGELOG.md is $fromChangeLog. +'''); + return false; + } + + // If NEXT wasn't the first section, it should not exist at all. + if (!hasNextSection) { + final RegExp nextRegex = RegExp(r'^#+\s*NEXT\s*$'); + if (lines.any((String line) => nextRegex.hasMatch(line))) { + printError(''' +When bumping the version for release, the NEXT section should be incorporated +into the new version's release notes. +'''); + return false; + } + } + + print('$packageName passed version check'); + return true; + } + + Pubspec? _tryParsePubspec(Directory package) { + final File pubspecFile = package.childFile('pubspec.yaml'); + + try { + final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + return pubspec; + } on Exception catch (exception) { + printError( + 'Failed to parse `pubspec.yaml` at ${pubspecFile.path}: $exception}'); + } + return null; + } +} diff --git a/script/tool/lib/src/xctest_command.dart b/script/tool/lib/src/xctest_command.dart new file mode 100644 index 000000000000..288851ca7edf --- /dev/null +++ b/script/tool/lib/src/xctest_command.dart @@ -0,0 +1,235 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common.dart'; + +const String _kiOSDestination = 'ios-destination'; +const String _kXcodeBuildCommand = 'xcodebuild'; +const String _kXCRunCommand = 'xcrun'; +const String _kFoundNoSimulatorsMessage = + 'Cannot find any available simulators, tests failed'; + +/// The command to run XCTests (XCUnitTest and XCUITest) in plugins. +/// The tests target have to be added to the Xcode project of the example app, +/// usually at "example/{ios,macos}/Runner.xcworkspace". +/// +/// The static analyzer is also run. +class XCTestCommand extends PluginCommand { + /// Creates an instance of the test command. + XCTestCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + }) : super(packagesDir, processRunner: processRunner) { + argParser.addOption( + _kiOSDestination, + help: + 'Specify the destination when running the test, used for -destination flag for xcodebuild command.\n' + 'this is passed to the `-destination` argument in xcodebuild command.\n' + 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT for details on how to specify the destination.', + ); + argParser.addFlag(kPlatformFlagIos, help: 'Runs the iOS tests'); + argParser.addFlag(kPlatformFlagMacos, help: 'Runs the macOS tests'); + } + + @override + final String name = 'xctest'; + + @override + final String description = + 'Runs the xctests in the iOS and/or macOS example apps.\n\n' + 'This command requires "flutter" and "xcrun" to be in your path.'; + + @override + Future run() async { + final bool testIos = getBoolArg(kPlatformFlagIos); + final bool testMacos = getBoolArg(kPlatformFlagMacos); + + if (!(testIos || testMacos)) { + print('At least one platform flag must be provided.'); + throw ToolExit(2); + } + + List iosDestinationFlags = []; + if (testIos) { + String destination = getStringArg(_kiOSDestination); + if (destination.isEmpty) { + final String? simulatorId = await _findAvailableIphoneSimulator(); + if (simulatorId == null) { + print(_kFoundNoSimulatorsMessage); + throw ToolExit(1); + } + destination = 'id=$simulatorId'; + } + iosDestinationFlags = [ + '-destination', + destination, + ]; + } + + final List failingPackages = []; + await for (final Directory plugin in getPlugins()) { + final String packageName = + p.relative(plugin.path, from: packagesDir.path); + print('============================================================'); + print('Start running for $packageName...'); + bool passed = true; + if (testIos) { + passed &= await _testPlugin(plugin, 'iOS', + extraXcrunFlags: iosDestinationFlags); + } + if (testMacos) { + passed &= await _testPlugin(plugin, 'macOS'); + } + if (!passed) { + failingPackages.add(packageName); + } + } + + // Command end, print reports. + if (failingPackages.isEmpty) { + print('All XCTests have passed!'); + } else { + print( + 'The following packages are failing XCTests (see above for details):'); + for (final String package in failingPackages) { + print(' * $package'); + } + throw ToolExit(1); + } + } + + /// Runs all applicable tests for [plugin], printing status and returning + /// success if the tests passed (or did not exist). + Future _testPlugin( + Directory plugin, + String platform, { + List extraXcrunFlags = const [], + }) async { + if (!pluginSupportsPlatform(platform.toLowerCase(), plugin, + requiredMode: PlatformSupport.inline)) { + print('$platform is not implemented by this plugin package.'); + print('\n'); + return true; + } + bool passing = true; + for (final Directory example in getExamplesForPlugin(plugin)) { + // Running tests and static analyzer. + final String examplePath = + p.relative(example.path, from: plugin.parent.path); + print('Running $platform tests and analyzer for $examplePath...'); + int exitCode = + await _runTests(true, example, platform, extraFlags: extraXcrunFlags); + // 66 = there is no test target (this fails fast). Try again with just the analyzer. + if (exitCode == 66) { + print('Tests not found for $examplePath, running analyzer only...'); + exitCode = await _runTests(false, example, platform, + extraFlags: extraXcrunFlags); + } + if (exitCode == 0) { + print('Successfully ran $platform xctest for $examplePath'); + } else { + passing = false; + } + } + return passing; + } + + Future _runTests( + bool runTests, + Directory example, + String platform, { + List extraFlags = const [], + }) { + final List xctestArgs = [ + _kXcodeBuildCommand, + if (runTests) 'test', + 'analyze', + '-workspace', + '${platform.toLowerCase()}/Runner.xcworkspace', + '-configuration', + 'Debug', + '-scheme', + 'Runner', + ...extraFlags, + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ]; + final String completeTestCommand = + '$_kXCRunCommand ${xctestArgs.join(' ')}'; + print(completeTestCommand); + return processRunner.runAndStream(_kXCRunCommand, xctestArgs, + workingDir: example, exitOnError: false); + } + + Future _findAvailableIphoneSimulator() async { + // Find the first available destination if not specified. + final List findSimulatorsArguments = [ + 'simctl', + 'list', + '--json' + ]; + final String findSimulatorCompleteCommand = + '$_kXCRunCommand ${findSimulatorsArguments.join(' ')}'; + print('Looking for available simulators...'); + print(findSimulatorCompleteCommand); + final io.ProcessResult findSimulatorsResult = + await processRunner.run(_kXCRunCommand, findSimulatorsArguments); + if (findSimulatorsResult.exitCode != 0) { + print('Error occurred while running "$findSimulatorCompleteCommand":\n' + '${findSimulatorsResult.stderr}'); + throw ToolExit(1); + } + final Map simulatorListJson = + jsonDecode(findSimulatorsResult.stdout as String) + as Map; + final List> runtimes = + (simulatorListJson['runtimes'] as List) + .cast>(); + final Map devices = + (simulatorListJson['devices'] as Map) + .cast(); + if (runtimes.isEmpty || devices.isEmpty) { + return null; + } + String? id; + // Looking for runtimes, trying to find one with highest OS version. + for (final Map rawRuntimeMap in runtimes.reversed) { + final Map runtimeMap = + rawRuntimeMap.cast(); + if ((runtimeMap['name'] as String?)?.contains('iOS') != true) { + continue; + } + final String? runtimeID = runtimeMap['identifier'] as String?; + if (runtimeID == null) { + continue; + } + final List>? devicesForRuntime = + (devices[runtimeID] as List?)?.cast>(); + if (devicesForRuntime == null || devicesForRuntime.isEmpty) { + continue; + } + // Looking for runtimes, trying to find latest version of device. + for (final Map rawDevice in devicesForRuntime.reversed) { + final Map device = rawDevice.cast(); + if (device['availabilityError'] != null || + (device['isAvailable'] as bool?) == false) { + continue; + } + id = device['udid'] as String?; + if (id == null) { + continue; + } + print('device selected: $device'); + return id; + } + } + return null; + } +} diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml new file mode 100644 index 000000000000..5d2200abcdb0 --- /dev/null +++ b/script/tool/pubspec.yaml @@ -0,0 +1,32 @@ +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.2.0 + +dependencies: + args: ^2.1.0 + async: ^2.6.1 + collection: ^1.15.0 + colorize: ^3.0.0 + file: ^6.1.0 + git: ^2.0.0 + http: ^0.13.3 + http_multi_server: ^3.0.1 + meta: ^1.3.0 + path: ^1.8.0 + platform: ^3.0.0 + pub_semver: ^2.0.0 + pubspec_parse: ^1.0.0 + quiver: ^3.0.1 + test: ^1.17.3 + uuid: ^3.0.4 + yaml: ^3.1.0 + +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 new file mode 100644 index 000000000000..ec627f25864c --- /dev/null +++ b/script/tool/test/analyze_command_test.dart @@ -0,0 +1,177 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/analyze_command.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:test/test.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); + processRunner = RecordingProcessRunner(); + final AnalyzeCommand analyzeCommand = + AnalyzeCommand(packagesDir, processRunner: processRunner); + + runner = CommandRunner('analyze_command', 'Test for analyze_command'); + runner.addCommand(analyzeCommand); + }); + + test('analyzes all packages', () async { + final Directory plugin1Dir = createFakePlugin('a', packagesDir); + final Directory plugin2Dir = createFakePlugin('b', packagesDir); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + await runner.run(['analyze']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', const ['packages', 'get'], plugin1Dir.path), + ProcessCall( + 'flutter', const ['packages', 'get'], plugin2Dir.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], + plugin1Dir.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], + plugin2Dir.path), + ])); + }); + + test('skips flutter pub get for examples', () async { + final Directory plugin1Dir = + createFakePlugin('a', packagesDir, withSingleExample: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + await runner.run(['analyze']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', const ['packages', 'get'], plugin1Dir.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], + plugin1Dir.path), + ])); + }); + + test('don\'t elide a non-contained example package', () async { + final Directory plugin1Dir = createFakePlugin('a', packagesDir); + final Directory plugin2Dir = createFakePlugin('example', packagesDir); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + await runner.run(['analyze']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', const ['packages', 'get'], plugin1Dir.path), + ProcessCall( + 'flutter', const ['packages', 'get'], plugin2Dir.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], + plugin1Dir.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], + plugin2Dir.path), + ])); + }); + + test('uses a separate analysis sdk', () async { + final Directory pluginDir = createFakePlugin('a', packagesDir); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + await runner.run(['analyze', '--analysis-sdk', 'foo/bar/baz']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', + const ['packages', 'get'], + pluginDir.path, + ), + ProcessCall( + 'foo/bar/baz/bin/dart', + const ['analyze', '--fatal-infos'], + pluginDir.path, + ), + ]), + ); + }); + + group('verifies analysis settings', () { + test('fails analysis_options.yaml', () async { + createFakePlugin('foo', packagesDir, withExtraFiles: >[ + ['analysis_options.yaml'] + ]); + + await expectLater(() => runner.run(['analyze']), + throwsA(const TypeMatcher())); + }); + + test('fails .analysis_options', () async { + createFakePlugin('foo', packagesDir, withExtraFiles: >[ + ['.analysis_options'] + ]); + + await expectLater(() => runner.run(['analyze']), + throwsA(const TypeMatcher())); + }); + + test('takes an allow list', () async { + final Directory pluginDir = + createFakePlugin('foo', packagesDir, withExtraFiles: >[ + ['analysis_options.yaml'] + ]); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + await runner.run(['analyze', '--custom-analysis', 'foo']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', const ['packages', 'get'], pluginDir.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], + pluginDir.path), + ])); + }); + + // See: https://github.com/flutter/flutter/issues/78994 + test('takes an empty allow list', () async { + createFakePlugin('foo', packagesDir, withExtraFiles: >[ + ['analysis_options.yaml'] + ]); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + + await expectLater( + () => runner.run(['analyze', '--custom-analysis', '']), + throwsA(const TypeMatcher())); + }); + }); +} diff --git a/script/tool/test/build_examples_command_test.dart b/script/tool/test/build_examples_command_test.dart new file mode 100644 index 000000000000..2ad17b374ba7 --- /dev/null +++ b/script/tool/test/build_examples_command_test.dart @@ -0,0 +1,518 @@ +// Copyright 2013 The Flutter Authors. All 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/build_examples_command.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('test build_example_command', () { + late FileSystem fileSystem; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + final String flutterCommand = + const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final BuildExamplesCommand command = + BuildExamplesCommand(packagesDir, processRunner: processRunner); + + runner = CommandRunner( + 'build_examples_command', 'Test for build_example_command'); + runner.addCommand(command); + }); + + test('building for iOS when plugin is not set up for iOS results in no-op', + () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isLinuxPlugin: false); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--ipa', '--no-macos']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: packagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING IPA for $packageName', + 'iOS is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('building for ios', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'build-examples', + '--ipa', + '--no-macos', + '--enable-experiment=exp1' + ]); + final String packageName = + p.relative(pluginExampleDirectory.path, from: packagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING IPA for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + const [ + 'build', + 'ios', + '--no-codesign', + '--enable-experiment=exp1' + ], + pluginExampleDirectory.path), + ])); + }); + + test( + 'building for Linux when plugin is not set up for Linux results in no-op', + () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isLinuxPlugin: false); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--linux']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: packagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING Linux for $packageName', + 'Linux is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + // Output should be empty since running build-examples --linux with no + // Linux implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('building for Linux', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isLinuxPlugin: true); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--linux']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: packagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING Linux for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, const ['build', 'linux'], + pluginExampleDirectory.path), + ])); + }); + + test('building for macos with no implementation results in no-op', + () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ]); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--macos']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: packagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING macOS for $packageName', + 'macOS is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('building for macos', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ['example', 'macos', 'macos.swift'], + ], + isMacOsPlugin: true); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--macos']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: packagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING macOS for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, const ['build', 'macos'], + pluginExampleDirectory.path), + ])); + }); + + test('building for web with no implementation results in no-op', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ]); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--web']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: packagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING web for $packageName', + 'Web is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('building for web', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ['example', 'web', 'index.html'], + ], + isWebPlugin: true); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--web']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: packagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING web for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, const ['build', 'web'], + pluginExampleDirectory.path), + ])); + }); + + test( + 'building for Windows when plugin is not set up for Windows results in no-op', + () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isWindowsPlugin: false); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--windows']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: packagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING Windows for $packageName', + 'Windows is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('building for windows', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isWindowsPlugin: true); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--no-ipa', '--windows']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: packagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING Windows for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, const ['build', 'windows'], + pluginExampleDirectory.path), + ])); + }); + + test( + 'building for Android when plugin is not set up for Android results in no-op', + () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isLinuxPlugin: false); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--apk', '--no-ipa']); + final String packageName = + p.relative(pluginExampleDirectory.path, from: packagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING APK for $packageName', + 'Android is not supported by this plugin', + '\n\n', + 'All builds successful!', + ]), + ); + + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('building for android', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isAndroidPlugin: true); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'build-examples', + '--apk', + '--no-ipa', + '--no-macos', + ]); + final String packageName = + p.relative(pluginExampleDirectory.path, from: packagesDir.path); + + expect( + output, + orderedEquals([ + '\nBUILDING APK for $packageName', + '\n\n', + 'All builds successful!', + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(flutterCommand, const ['build', 'apk'], + pluginExampleDirectory.path), + ])); + }); + + test('enable-experiment flag for Android', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isAndroidPlugin: true); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await runCapturingPrint(runner, [ + 'build-examples', + '--apk', + '--no-ipa', + '--no-macos', + '--enable-experiment=exp1' + ]); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + const ['build', 'apk', '--enable-experiment=exp1'], + pluginExampleDirectory.path), + ])); + }); + + test('enable-experiment flag for ios', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await runCapturingPrint(runner, [ + 'build-examples', + '--ipa', + '--no-macos', + '--enable-experiment=exp1' + ]); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + const [ + 'build', + 'ios', + '--no-codesign', + '--enable-experiment=exp1' + ], + pluginExampleDirectory.path), + ])); + }); + }); +} diff --git a/script/tool/test/common_test.dart b/script/tool/test/common_test.dart new file mode 100644 index 000000000000..a51182d91ff8 --- /dev/null +++ b/script/tool/test/common_test.dart @@ -0,0 +1,740 @@ +// Copyright 2013 The Flutter Authors. All 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:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:git/git.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:test/test.dart'; + +import 'common_test.mocks.dart'; +import 'util.dart'; + +@GenerateMocks([GitDir]) +void main() { + late RecordingProcessRunner processRunner; + late CommandRunner runner; + late FileSystem fileSystem; + late Directory packagesDir; + late Directory thirdPartyPackagesDir; + late List plugins; + late List?> gitDirCommands; + late String gitDiffResponse; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + thirdPartyPackagesDir = packagesDir.parent + .childDirectory('third_party') + .childDirectory('packages'); + + gitDirCommands = ?>[]; + gitDiffResponse = ''; + final MockGitDir gitDir = MockGitDir(); + 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] == 'diff') { + when(mockProcessResult.stdout as String?) + .thenReturn(gitDiffResponse); + } + return Future.value(mockProcessResult); + }); + processRunner = RecordingProcessRunner(); + plugins = []; + final SamplePluginCommand samplePluginCommand = SamplePluginCommand( + plugins, + packagesDir, + processRunner: processRunner, + gitDir: gitDir, + ); + runner = + CommandRunner('common_command', 'Test for common functionality'); + runner.addCommand(samplePluginCommand); + }); + + group('plugin iteration', () { + test('all plugins from file system', () async { + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runner.run(['sample']); + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('all plugins includes third_party/packages', () async { + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + final Directory plugin3 = + createFakePlugin('plugin3', thirdPartyPackagesDir); + await runner.run(['sample']); + expect(plugins, + unorderedEquals([plugin1.path, plugin2.path, plugin3.path])); + }); + + test('exclude plugins when plugins flag is specified', () async { + createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runner.run( + ['sample', '--plugins=plugin1,plugin2', '--exclude=plugin1']); + expect(plugins, unorderedEquals([plugin2.path])); + }); + + test('exclude plugins when plugins flag isn\'t specified', () async { + createFakePlugin('plugin1', packagesDir); + createFakePlugin('plugin2', packagesDir); + await runner.run(['sample', '--exclude=plugin1,plugin2']); + expect(plugins, unorderedEquals([])); + }); + + test('exclude federated plugins when plugins flag is specified', () async { + createFakePlugin('plugin1', packagesDir, + parentDirectoryName: 'federated'); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runner.run([ + 'sample', + '--plugins=federated/plugin1,plugin2', + '--exclude=federated/plugin1' + ]); + expect(plugins, unorderedEquals([plugin2.path])); + }); + + test('exclude entire federated plugins when plugins flag is specified', + () async { + createFakePlugin('plugin1', packagesDir, + parentDirectoryName: 'federated'); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runner.run([ + 'sample', + '--plugins=federated/plugin1,plugin2', + '--exclude=federated' + ]); + expect(plugins, unorderedEquals([plugin2.path])); + }); + + 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 runner.run([ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test( + 'all plugins should be tested if there are no plugin related changes.', + () async { + gitDiffResponse = 'AUTHORS'; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runner.run([ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('all plugins should be tested if .cirrus.yml changes.', () async { + gitDiffResponse = ''' +.cirrus.yml +packages/plugin1/CHANGELOG +'''; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runner.run([ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('all plugins should be tested if .ci.yaml changes', () async { + gitDiffResponse = ''' +.ci.yaml +packages/plugin1/CHANGELOG +'''; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runner.run([ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('all plugins should be tested if anything in .ci/ changes', + () async { + gitDiffResponse = ''' +.ci/Dockerfile +packages/plugin1/CHANGELOG +'''; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runner.run([ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('all plugins should be tested if anything in script changes.', + () async { + gitDiffResponse = ''' +script/tool_runner.sh +packages/plugin1/CHANGELOG +'''; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runner.run([ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('all plugins should be tested if the root analysis options change.', + () async { + gitDiffResponse = ''' +analysis_options.yaml +packages/plugin1/CHANGELOG +'''; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runner.run([ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('all plugins should be tested if formatting options change.', + () async { + gitDiffResponse = ''' +.clang-format +packages/plugin1/CHANGELOG +'''; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runner.run([ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('Only changed plugin should be tested.', () async { + gitDiffResponse = 'packages/plugin1/plugin1.dart'; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + createFakePlugin('plugin2', packagesDir); + await runner.run([ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path])); + }); + + test('multiple files in one plugin should also test the plugin', + () async { + gitDiffResponse = ''' +packages/plugin1/plugin1.dart +packages/plugin1/ios/plugin1.m +'''; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + createFakePlugin('plugin2', packagesDir); + await runner.run([ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path])); + }); + + test('multiple plugins changed should test all the changed plugins', + () async { + gitDiffResponse = ''' +packages/plugin1/plugin1.dart +packages/plugin2/ios/plugin2.m +'''; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + createFakePlugin('plugin3', packagesDir); + await runner.run([ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test( + 'multiple plugins inside the same plugin group changed should output the plugin group name', + () async { + gitDiffResponse = ''' +packages/plugin1/plugin1/plugin1.dart +packages/plugin1/plugin1_platform_interface/plugin1_platform_interface.dart +packages/plugin1/plugin1_web/plugin1_web.dart +'''; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir, + parentDirectoryName: 'plugin1'); + createFakePlugin('plugin2', packagesDir); + createFakePlugin('plugin3', packagesDir); + await runner.run([ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path])); + }); + + test('--plugins flag overrides the behavior of --run-on-changed-packages', + () async { + gitDiffResponse = ''' +packages/plugin1/plugin1.dart +packages/plugin2/ios/plugin2.m +packages/plugin3/plugin3.dart +'''; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir, + parentDirectoryName: 'plugin1'); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + createFakePlugin('plugin3', packagesDir); + await runner.run([ + 'sample', + '--plugins=plugin1,plugin2', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('--exclude flag works with --run-on-changed-packages', () async { + gitDiffResponse = ''' +packages/plugin1/plugin1.dart +packages/plugin2/ios/plugin2.m +packages/plugin3/plugin3.dart +'''; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir, + parentDirectoryName: 'plugin1'); + createFakePlugin('plugin2', packagesDir); + createFakePlugin('plugin3', packagesDir); + await runner.run([ + 'sample', + '--exclude=plugin2,plugin3', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(plugins, unorderedEquals([plugin1.path])); + }); + }); + }); + + group('$GitVersionFinder', () { + late FileSystem fileSystem; + late List?> gitDirCommands; + late String gitDiffResponse; + String? mergeBaseResponse; + late MockGitDir gitDir; + + setUp(() { + fileSystem = MemoryFileSystem(); + createPackagesDirectory(fileSystem: fileSystem); + gitDirCommands = ?>[]; + gitDiffResponse = ''; + gitDir = MockGitDir(); + 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] == 'diff') { + when(mockProcessResult.stdout as String?) + .thenReturn(gitDiffResponse); + } else if (invocation.positionalArguments[0][0] == 'merge-base') { + when(mockProcessResult.stdout as String?) + .thenReturn(mergeBaseResponse); + } + return Future.value(mockProcessResult); + }); + processRunner = RecordingProcessRunner(); + }); + + test('No git diff should result no files changed', () async { + final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); + final List changedFiles = await finder.getChangedFiles(); + + expect(changedFiles, isEmpty); + }); + + test('get correct files changed based on git diff', () async { + gitDiffResponse = ''' +file1/file1.cc +file2/file2.cc +'''; + final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); + final List changedFiles = await finder.getChangedFiles(); + + expect( + changedFiles, equals(['file1/file1.cc', 'file2/file2.cc'])); + }); + + test('get correct pubspec change based on git diff', () async { + gitDiffResponse = ''' +file1/pubspec.yaml +file2/file2.cc +'''; + final GitVersionFinder finder = GitVersionFinder(gitDir, 'some base sha'); + final List changedFiles = await finder.getChangedPubSpecs(); + + expect(changedFiles, equals(['file1/pubspec.yaml'])); + }); + + test('use correct base sha if not specified', () async { + mergeBaseResponse = 'shaqwiueroaaidf12312jnadf123nd'; + gitDiffResponse = ''' +file1/pubspec.yaml +file2/file2.cc +'''; + + final GitVersionFinder finder = GitVersionFinder(gitDir, null); + await finder.getChangedFiles(); + verify(gitDir.runCommand( + ['diff', '--name-only', mergeBaseResponse!, 'HEAD'])); + }); + + test('use correct base sha if specified', () async { + const String customBaseSha = 'aklsjdcaskf12312'; + gitDiffResponse = ''' +file1/pubspec.yaml +file2/file2.cc +'''; + final GitVersionFinder finder = GitVersionFinder(gitDir, customBaseSha); + await finder.getChangedFiles(); + verify(gitDir + .runCommand(['diff', '--name-only', customBaseSha, 'HEAD'])); + }); + }); + + group('$PubVersionFinder', () { + test('Package does not exist.', () async { + final MockClient mockClient = MockClient((http.Request request) async { + return http.Response('', 404); + }); + final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); + final PubVersionFinderResponse response = + await finder.getPackageVersion(package: 'some_package'); + + expect(response.versions, isEmpty); + expect(response.result, PubVersionFinderResult.noPackageFound); + expect(response.httpResponse.statusCode, 404); + expect(response.httpResponse.body, ''); + }); + + test('HTTP error when getting versions from pub', () async { + final MockClient mockClient = MockClient((http.Request request) async { + return http.Response('', 400); + }); + final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); + final PubVersionFinderResponse response = + await finder.getPackageVersion(package: 'some_package'); + + expect(response.versions, isEmpty); + expect(response.result, PubVersionFinderResult.fail); + expect(response.httpResponse.statusCode, 400); + expect(response.httpResponse.body, ''); + }); + + test('Get a correct list of versions when http response is OK.', () async { + const Map httpResponse = { + 'name': 'some_package', + 'versions': [ + '0.0.1', + '0.0.2', + '0.0.2+2', + '0.1.1', + '0.0.1+1', + '0.1.0', + '0.2.0', + '0.1.0+1', + '0.0.2+1', + '2.0.0', + '1.2.0', + '1.0.0', + ], + }; + final MockClient mockClient = MockClient((http.Request request) async { + return http.Response(json.encode(httpResponse), 200); + }); + final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); + final PubVersionFinderResponse response = + await finder.getPackageVersion(package: 'some_package'); + + expect(response.versions, [ + Version.parse('2.0.0'), + Version.parse('1.2.0'), + Version.parse('1.0.0'), + Version.parse('0.2.0'), + Version.parse('0.1.1'), + Version.parse('0.1.0+1'), + Version.parse('0.1.0'), + Version.parse('0.0.2+2'), + Version.parse('0.0.2+1'), + Version.parse('0.0.2'), + Version.parse('0.0.1+1'), + Version.parse('0.0.1'), + ]); + expect(response.result, PubVersionFinderResult.success); + expect(response.httpResponse.statusCode, 200); + expect(response.httpResponse.body, json.encode(httpResponse)); + }); + }); + + group('pluginSupportsPlatform', () { + test('no platforms', () async { + final Directory plugin = createFakePlugin('plugin', packagesDir); + + expect(pluginSupportsPlatform('android', plugin), isFalse); + expect(pluginSupportsPlatform('ios', plugin), isFalse); + expect(pluginSupportsPlatform('linux', plugin), isFalse); + expect(pluginSupportsPlatform('macos', plugin), isFalse); + expect(pluginSupportsPlatform('web', plugin), isFalse); + expect(pluginSupportsPlatform('windows', plugin), isFalse); + }); + + test('all platforms', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + isAndroidPlugin: true, + isIosPlugin: true, + isLinuxPlugin: true, + isMacOsPlugin: true, + isWebPlugin: true, + isWindowsPlugin: true, + ); + + expect(pluginSupportsPlatform('android', plugin), isTrue); + expect(pluginSupportsPlatform('ios', plugin), isTrue); + expect(pluginSupportsPlatform('linux', plugin), isTrue); + expect(pluginSupportsPlatform('macos', plugin), isTrue); + expect(pluginSupportsPlatform('web', plugin), isTrue); + expect(pluginSupportsPlatform('windows', plugin), isTrue); + }); + + test('some platforms', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + isAndroidPlugin: true, + isIosPlugin: false, + isLinuxPlugin: true, + isMacOsPlugin: false, + isWebPlugin: true, + isWindowsPlugin: false, + ); + + expect(pluginSupportsPlatform('android', plugin), isTrue); + expect(pluginSupportsPlatform('ios', plugin), isFalse); + expect(pluginSupportsPlatform('linux', plugin), isTrue); + expect(pluginSupportsPlatform('macos', plugin), isFalse); + expect(pluginSupportsPlatform('web', plugin), isTrue); + expect(pluginSupportsPlatform('windows', plugin), isFalse); + }); + + test('inline plugins are only detected as inline', () async { + // createFakePlugin makes non-federated pubspec entries. + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + isAndroidPlugin: true, + isIosPlugin: true, + isLinuxPlugin: true, + isMacOsPlugin: true, + isWebPlugin: true, + isWindowsPlugin: true, + ); + + expect( + pluginSupportsPlatform('android', plugin, + requiredMode: PlatformSupport.inline), + isTrue); + expect( + pluginSupportsPlatform('android', plugin, + requiredMode: PlatformSupport.federated), + isFalse); + expect( + pluginSupportsPlatform('ios', plugin, + requiredMode: PlatformSupport.inline), + isTrue); + expect( + pluginSupportsPlatform('ios', plugin, + requiredMode: PlatformSupport.federated), + isFalse); + expect( + pluginSupportsPlatform('linux', plugin, + requiredMode: PlatformSupport.inline), + isTrue); + expect( + pluginSupportsPlatform('linux', plugin, + requiredMode: PlatformSupport.federated), + isFalse); + expect( + pluginSupportsPlatform('macos', plugin, + requiredMode: PlatformSupport.inline), + isTrue); + expect( + pluginSupportsPlatform('macos', plugin, + requiredMode: PlatformSupport.federated), + isFalse); + expect( + pluginSupportsPlatform('web', plugin, + requiredMode: PlatformSupport.inline), + isTrue); + expect( + pluginSupportsPlatform('web', plugin, + requiredMode: PlatformSupport.federated), + isFalse); + expect( + pluginSupportsPlatform('windows', plugin, + requiredMode: PlatformSupport.inline), + isTrue); + expect( + pluginSupportsPlatform('windows', plugin, + requiredMode: PlatformSupport.federated), + isFalse); + }); + + test('federated plugins are only detected as federated', () async { + const String pluginName = 'plugin'; + final Directory plugin = createFakePlugin( + pluginName, + packagesDir, + isAndroidPlugin: true, + isIosPlugin: true, + isLinuxPlugin: true, + isMacOsPlugin: true, + isWebPlugin: true, + isWindowsPlugin: true, + ); + + createFakePubspec( + plugin, + name: pluginName, + androidSupport: PlatformSupport.federated, + iosSupport: PlatformSupport.federated, + linuxSupport: PlatformSupport.federated, + macosSupport: PlatformSupport.federated, + webSupport: PlatformSupport.federated, + windowsSupport: PlatformSupport.federated, + ); + + expect( + pluginSupportsPlatform('android', plugin, + requiredMode: PlatformSupport.federated), + isTrue); + expect( + pluginSupportsPlatform('android', plugin, + requiredMode: PlatformSupport.inline), + isFalse); + expect( + pluginSupportsPlatform('ios', plugin, + requiredMode: PlatformSupport.federated), + isTrue); + expect( + pluginSupportsPlatform('ios', plugin, + requiredMode: PlatformSupport.inline), + isFalse); + expect( + pluginSupportsPlatform('linux', plugin, + requiredMode: PlatformSupport.federated), + isTrue); + expect( + pluginSupportsPlatform('linux', plugin, + requiredMode: PlatformSupport.inline), + isFalse); + expect( + pluginSupportsPlatform('macos', plugin, + requiredMode: PlatformSupport.federated), + isTrue); + expect( + pluginSupportsPlatform('macos', plugin, + requiredMode: PlatformSupport.inline), + isFalse); + expect( + pluginSupportsPlatform('web', plugin, + requiredMode: PlatformSupport.federated), + isTrue); + expect( + pluginSupportsPlatform('web', plugin, + requiredMode: PlatformSupport.inline), + isFalse); + expect( + pluginSupportsPlatform('windows', plugin, + requiredMode: PlatformSupport.federated), + isTrue); + expect( + pluginSupportsPlatform('windows', plugin, + requiredMode: PlatformSupport.inline), + isFalse); + }); + }); +} + +class SamplePluginCommand extends PluginCommand { + SamplePluginCommand( + this._plugins, + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + GitDir? gitDir, + }) : super(packagesDir, processRunner: processRunner, gitDir: gitDir); + + final List _plugins; + + @override + final String name = 'sample'; + + @override + final String description = 'sample command'; + + @override + Future run() async { + await for (final Directory package in getPlugins()) { + _plugins.add(package.path); + } + } +} + +class MockProcessResult extends Mock implements ProcessResult {} diff --git a/script/tool/test/common_test.mocks.dart b/script/tool/test/common_test.mocks.dart new file mode 100644 index 000000000000..b7f7807b3b05 --- /dev/null +++ b/script/tool/test/common_test.mocks.dart @@ -0,0 +1,143 @@ +// Mocks generated by Mockito 5.0.7 from annotations +// in flutter_plugin_tools/test/common_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i6; +import 'dart:io' as _i4; + +import 'package:git/src/branch_reference.dart' as _i3; +import 'package:git/src/commit.dart' as _i2; +import 'package:git/src/commit_reference.dart' as _i8; +import 'package:git/src/git_dir.dart' as _i5; +import 'package:git/src/tag.dart' as _i7; +import 'package:git/src/tree_entry.dart' as _i9; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: comment_references +// ignore_for_file: unnecessary_parenthesis + +// ignore_for_file: prefer_const_constructors + +// ignore_for_file: avoid_redundant_argument_values + +class _FakeCommit extends _i1.Fake implements _i2.Commit {} + +class _FakeBranchReference extends _i1.Fake implements _i3.BranchReference {} + +class _FakeProcessResult extends _i1.Fake implements _i4.ProcessResult {} + +/// A class which mocks [GitDir]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockGitDir extends _i1.Mock implements _i5.GitDir { + MockGitDir() { + _i1.throwOnMissingStub(this); + } + + @override + String get path => + (super.noSuchMethod(Invocation.getter(#path), returnValue: '') as String); + @override + _i6.Future commitCount([String? branchName = r'HEAD']) => + (super.noSuchMethod(Invocation.method(#commitCount, [branchName]), + returnValue: Future.value(0)) as _i6.Future); + @override + _i6.Future<_i2.Commit> commitFromRevision(String? revision) => + (super.noSuchMethod(Invocation.method(#commitFromRevision, [revision]), + returnValue: Future<_i2.Commit>.value(_FakeCommit())) + as _i6.Future<_i2.Commit>); + @override + _i6.Future> commits([String? branchName = r'HEAD']) => + (super.noSuchMethod(Invocation.method(#commits, [branchName]), + returnValue: + Future>.value({})) + as _i6.Future>); + @override + _i6.Future<_i3.BranchReference?> branchReference(String? branchName) => + (super.noSuchMethod(Invocation.method(#branchReference, [branchName]), + returnValue: + Future<_i3.BranchReference?>.value(_FakeBranchReference())) + as _i6.Future<_i3.BranchReference?>); + @override + _i6.Future> branches() => (super.noSuchMethod( + Invocation.method(#branches, []), + returnValue: + Future>.value(<_i3.BranchReference>[])) + as _i6.Future>); + @override + _i6.Stream<_i7.Tag> tags() => + (super.noSuchMethod(Invocation.method(#tags, []), + returnValue: Stream<_i7.Tag>.empty()) as _i6.Stream<_i7.Tag>); + @override + _i6.Future> showRef( + {bool? heads = false, bool? tags = false}) => + (super.noSuchMethod( + Invocation.method(#showRef, [], {#heads: heads, #tags: tags}), + returnValue: Future>.value( + <_i8.CommitReference>[])) + as _i6.Future>); + @override + _i6.Future<_i3.BranchReference> currentBranch() => + (super.noSuchMethod(Invocation.method(#currentBranch, []), + returnValue: + Future<_i3.BranchReference>.value(_FakeBranchReference())) + as _i6.Future<_i3.BranchReference>); + @override + _i6.Future> lsTree(String? treeish, + {bool? subTreesOnly = false, String? path}) => + (super.noSuchMethod( + Invocation.method(#lsTree, [treeish], + {#subTreesOnly: subTreesOnly, #path: path}), + returnValue: Future>.value(<_i9.TreeEntry>[])) + as _i6.Future>); + @override + _i6.Future createOrUpdateBranch( + String? branchName, String? treeSha, String? commitMessage) => + (super.noSuchMethod( + Invocation.method( + #createOrUpdateBranch, [branchName, treeSha, commitMessage]), + returnValue: Future.value('')) as _i6.Future); + @override + _i6.Future commitTree(String? treeSha, String? commitMessage, + {List? parentCommitShas}) => + (super.noSuchMethod( + Invocation.method(#commitTree, [treeSha, commitMessage], + {#parentCommitShas: parentCommitShas}), + returnValue: Future.value('')) as _i6.Future); + @override + _i6.Future> writeObjects(List? paths) => + (super.noSuchMethod(Invocation.method(#writeObjects, [paths]), + returnValue: + Future>.value({})) + as _i6.Future>); + @override + _i6.Future<_i4.ProcessResult> runCommand(Iterable? args, + {bool? throwOnError = true}) => + (super.noSuchMethod( + Invocation.method(#runCommand, [args], {#throwOnError: throwOnError}), + returnValue: + Future<_i4.ProcessResult>.value(_FakeProcessResult())) as _i6 + .Future<_i4.ProcessResult>); + @override + _i6.Future isWorkingTreeClean() => + (super.noSuchMethod(Invocation.method(#isWorkingTreeClean, []), + returnValue: Future.value(false)) as _i6.Future); + @override + _i6.Future<_i2.Commit?> updateBranch( + String? branchName, + _i6.Future Function(_i4.Directory)? populater, + String? commitMessage) => + (super.noSuchMethod( + Invocation.method( + #updateBranch, [branchName, populater, commitMessage]), + returnValue: Future<_i2.Commit?>.value(_FakeCommit())) + as _i6.Future<_i2.Commit?>); + @override + _i6.Future<_i2.Commit?> updateBranchWithDirectoryContents(String? branchName, + String? sourceDirectoryPath, String? commitMessage) => + (super.noSuchMethod( + Invocation.method(#updateBranchWithDirectoryContents, + [branchName, sourceDirectoryPath, commitMessage]), + returnValue: Future<_i2.Commit?>.value(_FakeCommit())) + as _i6.Future<_i2.Commit?>); +} diff --git a/script/tool/test/create_all_plugins_app_command_test.dart b/script/tool/test/create_all_plugins_app_command_test.dart new file mode 100644 index 000000000000..5bde5e0dc004 --- /dev/null +++ b/script/tool/test/create_all_plugins_app_command_test.dart @@ -0,0 +1,90 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '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:test/test.dart'; + +import 'util.dart'; + +void main() { + group('$CreateAllPluginsAppCommand', () { + late CommandRunner runner; + FileSystem fileSystem; + late Directory testRoot; + late Directory packagesDir; + late Directory appDir; + + setUp(() { + // Since the core of this command is a call to 'flutter create', the test + // has to use the real filesystem. Put everything possible in a unique + // temporary to minimize affect on the host system. + fileSystem = const LocalFileSystem(); + testRoot = fileSystem.systemTempDirectory.createTempSync(); + packagesDir = testRoot.childDirectory('packages'); + + final CreateAllPluginsAppCommand command = CreateAllPluginsAppCommand( + packagesDir, + pluginsRoot: testRoot, + ); + appDir = command.appDirectory; + runner = CommandRunner( + 'create_all_test', 'Test for $CreateAllPluginsAppCommand'); + runner.addCommand(command); + }); + + tearDown(() { + testRoot.deleteSync(recursive: true); + }); + + test('pubspec includes all plugins', () async { + createFakePlugin('plugina', packagesDir); + createFakePlugin('pluginb', packagesDir); + createFakePlugin('pluginc', packagesDir); + + await runner.run(['all-plugins-app']); + final List pubspec = + appDir.childFile('pubspec.yaml').readAsLinesSync(); + + expect( + pubspec, + containsAll([ + contains(RegExp('path: .*/packages/plugina')), + contains(RegExp('path: .*/packages/pluginb')), + contains(RegExp('path: .*/packages/pluginc')), + ])); + }); + + test('pubspec has overrides for all plugins', () async { + createFakePlugin('plugina', packagesDir); + createFakePlugin('pluginb', packagesDir); + createFakePlugin('pluginc', packagesDir); + + await runner.run(['all-plugins-app']); + final List pubspec = + appDir.childFile('pubspec.yaml').readAsLinesSync(); + + expect( + pubspec, + containsAllInOrder([ + contains('dependency_overrides:'), + contains(RegExp('path: .*/packages/plugina')), + contains(RegExp('path: .*/packages/pluginb')), + contains(RegExp('path: .*/packages/pluginc')), + ])); + }); + + test('pubspec is compatible with null-safe app code', () async { + createFakePlugin('plugina', packagesDir); + + await runner.run(['all-plugins-app']); + final String pubspec = + appDir.childFile('pubspec.yaml').readAsStringSync(); + + expect(pubspec, contains(RegExp('sdk:\\s*(?:["\']>=|[^])2\\.12\\.'))); + }); + }); +} diff --git a/script/tool/test/drive_examples_command_test.dart b/script/tool/test/drive_examples_command_test.dart new file mode 100644 index 000000000000..c9a8b9d90a83 --- /dev/null +++ b/script/tool/test/drive_examples_command_test.dart @@ -0,0 +1,635 @@ +// Copyright 2013 The Flutter Authors. All 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.dart'; +import 'package:flutter_plugin_tools/src/drive_examples_command.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('test drive_example_command', () { + late FileSystem fileSystem; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + final String flutterCommand = + const LocalPlatform().isWindows ? 'flutter.bat' : 'flutter'; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final DriveExamplesCommand command = + DriveExamplesCommand(packagesDir, processRunner: processRunner); + + runner = CommandRunner( + 'drive_examples_command', 'Test for drive_example_command'); + runner.addCommand(command); + }); + + test('driving under folder "test"', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test', 'plugin.dart'], + ], + isIosPlugin: true, + isAndroidPlugin: true); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n==========\nChecking plugin...', + '\n\n', + 'All driver tests successful!', + ]), + ); + + final String deviceTestPath = p.join('test', 'plugin.dart'); + final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving under folder "test_driver"', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isAndroidPlugin: true, + isIosPlugin: true); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n==========\nChecking plugin...', + '\n\n', + 'All driver tests successful!', + ]), + ); + + final String deviceTestPath = p.join('test_driver', 'plugin.dart'); + final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving under folder "test_driver" when test files are missing"', + () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ], + isAndroidPlugin: true, + isIosPlugin: true); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await expectLater( + () => runCapturingPrint(runner, ['drive-examples']), + throwsA(const TypeMatcher())); + }); + + test('a plugin without any integration test files is reported as an error', + () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'lib', 'main.dart'], + ], + isAndroidPlugin: true, + isIosPlugin: true); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await expectLater( + () => runCapturingPrint(runner, ['drive-examples']), + throwsA(const TypeMatcher())); + }); + + test( + 'driving under folder "test_driver" when targets are under "integration_test"', + () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test_driver', 'integration_test.dart'], + ['example', 'integration_test', 'bar_test.dart'], + ['example', 'integration_test', 'foo_test.dart'], + ['example', 'integration_test', 'ignore_me.dart'], + ], + isAndroidPlugin: true, + isIosPlugin: true); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n==========\nChecking plugin...', + '\n\n', + 'All driver tests successful!', + ]), + ); + + final String driverTestPath = + p.join('test_driver', 'integration_test.dart'); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '--driver', + driverTestPath, + '--target', + p.join('integration_test', 'bar_test.dart'), + ], + pluginExampleDirectory.path), + ProcessCall( + flutterCommand, + [ + 'drive', + '--driver', + driverTestPath, + '--target', + p.join('integration_test', 'foo_test.dart'), + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not support Linux is a no-op', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isMacOsPlugin: false); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--linux', + ]); + + expect( + output, + orderedEquals([ + '\n==========\nChecking plugin...', + 'Not supported for the target platform; skipping.', + '\n\n', + 'All driver tests successful!', + ]), + ); + + // Output should be empty since running drive-examples --linux on a non-Linux + // plugin is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('driving on a Linux plugin', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isLinuxPlugin: true); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--linux', + ]); + + expect( + output, + orderedEquals([ + '\n==========\nChecking plugin...', + '\n\n', + 'All driver tests successful!', + ]), + ); + + final String deviceTestPath = p.join('test_driver', 'plugin.dart'); + final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + 'linux', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not suppport macOS is a no-op', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ]); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--macos', + ]); + + expect( + output, + orderedEquals([ + '\n==========\nChecking plugin...', + 'Not supported for the target platform; skipping.', + '\n\n', + 'All driver tests successful!', + ]), + ); + + // Output should be empty since running drive-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, []); + }); + test('driving on a macOS plugin', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ['example', 'macos', 'macos.swift'], + ], + isMacOsPlugin: true); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--macos', + ]); + + expect( + output, + orderedEquals([ + '\n==========\nChecking plugin...', + '\n\n', + 'All driver tests successful!', + ]), + ); + + final String deviceTestPath = p.join('test_driver', 'plugin.dart'); + final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + 'macos', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not suppport web is a no-op', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isWebPlugin: false); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--web', + ]); + + expect( + output, + orderedEquals([ + '\n==========\nChecking plugin...', + 'Not supported for the target platform; skipping.', + '\n\n', + 'All driver tests successful!', + ]), + ); + + // Output should be empty since running drive-examples --web on a non-web + // plugin is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('driving a web plugin', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isWebPlugin: true); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--web', + ]); + + expect( + output, + orderedEquals([ + '\n==========\nChecking plugin...', + '\n\n', + 'All driver tests successful!', + ]), + ); + + final String deviceTestPath = p.join('test_driver', 'plugin.dart'); + final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + 'web-server', + '--web-port=7357', + '--browser-name=chrome', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not suppport Windows is a no-op', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isWindowsPlugin: false); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--windows', + ]); + + expect( + output, + orderedEquals([ + '\n==========\nChecking plugin...', + 'Not supported for the target platform; skipping.', + '\n\n', + 'All driver tests successful!', + ]), + ); + + // Output should be empty since running drive-examples --windows on a + // non-Windows plugin is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('driving on a Windows plugin', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isWindowsPlugin: true); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--windows', + ]); + + expect( + output, + orderedEquals([ + '\n==========\nChecking plugin...', + '\n\n', + 'All driver tests successful!', + ]), + ); + + final String deviceTestPath = p.join('test_driver', 'plugin.dart'); + final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '-d', + 'windows', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not support mobile is no-op', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test_driver', 'plugin.dart'], + ], + isMacOsPlugin: true); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n==========\nChecking plugin...', + 'Not supported for the target platform; skipping.', + '\n\n', + 'All driver tests successful!', + ]), + ); + + // Output should be empty since running drive-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('platform interface plugins are silently skipped', () async { + createFakePlugin('aplugin_platform_interface', packagesDir); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + ]); + + expect( + output, + orderedEquals([ + '\n\n', + 'All driver tests successful!', + ]), + ); + + // Output should be empty since running drive-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('enable-experiment flag', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test_driver', 'plugin_test.dart'], + ['example', 'test', 'plugin.dart'], + ], + isIosPlugin: true, + isAndroidPlugin: true); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + await runCapturingPrint(runner, [ + 'drive-examples', + '--enable-experiment=exp1', + ]); + + final String deviceTestPath = p.join('test', 'plugin.dart'); + final String driverTestPath = p.join('test_driver', 'plugin_test.dart'); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + flutterCommand, + [ + 'drive', + '--enable-experiment=exp1', + '--driver', + driverTestPath, + '--target', + deviceTestPath + ], + pluginExampleDirectory.path), + ])); + }); + }); +} diff --git a/script/tool/test/firebase_test_lab_test.dart b/script/tool/test/firebase_test_lab_test.dart new file mode 100644 index 000000000000..aa8be17d6794 --- /dev/null +++ b/script/tool/test/firebase_test_lab_test.dart @@ -0,0 +1,263 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/firebase_test_lab_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('$FirebaseTestLabCommand', () { + FileSystem fileSystem; + late Directory packagesDir; + late List printedMessages; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + printedMessages = []; + processRunner = RecordingProcessRunner(); + final FirebaseTestLabCommand command = FirebaseTestLabCommand(packagesDir, + processRunner: processRunner, + print: (Object? message) => printedMessages.add(message.toString())); + + runner = CommandRunner( + 'firebase_test_lab_command', 'Test for $FirebaseTestLabCommand'); + runner.addCommand(command); + }); + + test('retries gcloud set', () async { + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(1); + processRunner.processToReturn = mockProcess; + createFakePlugin('plugin', packagesDir, withExtraFiles: >[ + ['lib/test/should_not_run_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e_test.dart'], + ['example', 'android', 'gradlew'], + ['example', 'should_not_run_e2e.dart'], + [ + 'example', + 'android', + 'app', + 'src', + 'androidTest', + 'MainActivityTest.java' + ], + ]); + await expectLater( + () => runCapturingPrint(runner, ['firebase-test-lab']), + throwsA(const TypeMatcher())); + expect( + printedMessages, + contains( + '\nWarning: gcloud config set returned a non-zero exit code. Continuing anyway.')); + }); + + test('runs e2e tests', () async { + createFakePlugin('plugin', packagesDir, withExtraFiles: >[ + ['test', 'plugin_test.dart'], + ['test', 'plugin_e2e.dart'], + ['should_not_run_e2e.dart'], + ['lib/test/should_not_run_e2e.dart'], + ['example', 'test', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e_test.dart'], + ['example', 'integration_test', 'foo_test.dart'], + ['example', 'integration_test', 'should_not_run.dart'], + ['example', 'android', 'gradlew'], + ['example', 'should_not_run_e2e.dart'], + [ + 'example', + 'android', + 'app', + 'src', + 'androidTest', + 'MainActivityTest.java' + ], + ]); + + await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--device', + 'model=seoul,version=26', + '--test-run-id', + 'testRunId', + '--build-id', + 'buildId', + ]); + + expect( + printedMessages, + orderedEquals([ + '\nRUNNING FIREBASE TEST LAB TESTS for plugin', + '\nFirebase project configured.', + '\n\n', + 'All Firebase Test Lab tests successful!', + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'gcloud', + 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' + .split(' '), + null), + ProcessCall( + 'gcloud', 'config set project flutter-infra'.split(' '), null), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleAndroidTest -Pverbose=true'.split(' '), + '/packages/plugin/example/android'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/test/plugin_e2e.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --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' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test/plugin_e2e.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --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' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test_driver/plugin_e2e.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/2/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/3/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin/example'), + ]), + ); + }); + + test('experimental flag', () async { + createFakePlugin('plugin', packagesDir, withExtraFiles: >[ + ['test', 'plugin_test.dart'], + ['test', 'plugin_e2e.dart'], + ['should_not_run_e2e.dart'], + ['lib/test/should_not_run_e2e.dart'], + ['example', 'test', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e.dart'], + ['example', 'test_driver', 'plugin_e2e_test.dart'], + ['example', 'integration_test', 'foo_test.dart'], + ['example', 'integration_test', 'should_not_run.dart'], + ['example', 'android', 'gradlew'], + ['example', 'should_not_run_e2e.dart'], + [ + 'example', + 'android', + 'app', + 'src', + 'androidTest', + 'MainActivityTest.java' + ], + ]); + + await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--test-run-id', + 'testRunId', + '--build-id', + 'buildId', + '--enable-experiment=exp1', + ]); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'gcloud', + 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' + .split(' '), + null), + ProcessCall( + 'gcloud', 'config set project flutter-infra'.split(' '), null), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleAndroidTest -Pverbose=true -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/test/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/1/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/test_driver/plugin_e2e.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/2/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 5m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/3/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ]), + ); + }); + }); +} diff --git a/script/tool/test/java_test_command_test.dart b/script/tool/test/java_test_command_test.dart new file mode 100644 index 000000000000..a1c2d3b864c4 --- /dev/null +++ b/script/tool/test/java_test_command_test.dart @@ -0,0 +1,87 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/java_test_command.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('$JavaTestCommand', () { + late FileSystem fileSystem; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final JavaTestCommand command = + JavaTestCommand(packagesDir, processRunner: processRunner); + + runner = + CommandRunner('java_test_test', 'Test for $JavaTestCommand'); + runner.addCommand(command); + }); + + test('Should run Java tests in Android implementation folder', () async { + final Directory plugin = createFakePlugin( + 'plugin1', + packagesDir, + isAndroidPlugin: true, + isFlutter: true, + withSingleExample: true, + withExtraFiles: >[ + ['example/android', 'gradlew'], + ['android/src/test', 'example_test.java'], + ], + ); + + await runner.run(['java-test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + p.join(plugin.path, 'example/android/gradlew'), + const ['testDebugUnitTest', '--info'], + p.join(plugin.path, 'example/android'), + ), + ]), + ); + }); + + test('Should run Java tests in example folder', () async { + final Directory plugin = createFakePlugin( + 'plugin1', + packagesDir, + isAndroidPlugin: true, + isFlutter: true, + withSingleExample: true, + withExtraFiles: >[ + ['example/android', 'gradlew'], + ['example/android/app/src/test', 'example_test.java'], + ], + ); + + await runner.run(['java-test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + p.join(plugin.path, 'example/android/gradlew'), + const ['testDebugUnitTest', '--info'], + p.join(plugin.path, 'example/android'), + ), + ]), + ); + }); + }); +} diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart new file mode 100644 index 000000000000..a874d7db17b7 --- /dev/null +++ b/script/tool/test/license_check_command_test.dart @@ -0,0 +1,443 @@ +// Copyright 2013 The Flutter Authors. All 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.dart'; +import 'package:flutter_plugin_tools/src/license_check_command.dart'; +import 'package:test/test.dart'; + +void main() { + group('$LicenseCheckCommand', () { + late CommandRunner runner; + late FileSystem fileSystem; + late List printedMessages; + late Directory root; + + setUp(() { + fileSystem = MemoryFileSystem(); + final Directory packagesDir = + fileSystem.currentDirectory.childDirectory('packages'); + root = packagesDir.parent; + + printedMessages = []; + final LicenseCheckCommand command = LicenseCheckCommand( + packagesDir, + print: (Object? message) => printedMessages.add(message.toString()), + ); + runner = + CommandRunner('license_test', 'Test for $LicenseCheckCommand'); + runner.addCommand(command); + }); + + /// Writes a copyright+license block to [file], defaulting to a standard + /// block for this repository. + /// + /// [commentString] is added to the start of each line. + /// [prefix] is added to the start of the entire block. + /// [suffix] is added to the end of the entire block. + void _writeLicense( + File file, { + String comment = '// ', + String prefix = '', + String suffix = '', + String copyright = + 'Copyright 2013 The Flutter Authors. All rights reserved.', + List license = const [ + 'Use of this source code is governed by a BSD-style license that can be', + 'found in the LICENSE file.', + ], + }) { + final List lines = ['$prefix$comment$copyright']; + for (final String line in license) { + lines.add('$comment$line'); + } + file.writeAsStringSync(lines.join('\n') + suffix + '\n'); + } + + test('looks at only expected extensions', () async { + final Map extensions = { + 'c': true, + 'cc': true, + 'cpp': true, + 'dart': true, + 'h': true, + 'html': true, + 'java': true, + 'json': false, + 'm': true, + 'md': false, + 'mm': true, + 'png': false, + 'swift': true, + 'sh': true, + 'yaml': false, + }; + + const String filenameBase = 'a_file'; + for (final String fileExtension in extensions.keys) { + root.childFile('$filenameBase.$fileExtension').createSync(); + } + + try { + await runner.run(['license-check']); + } on ToolExit { + // Ignore failure; the files are empty so the check is expected to fail, + // but this test isn't for that behavior. + } + + extensions.forEach((String fileExtension, bool shouldCheck) { + final Matcher logLineMatcher = + contains('Checking $filenameBase.$fileExtension'); + expect(printedMessages, + shouldCheck ? logLineMatcher : isNot(logLineMatcher)); + }); + }); + + test('ignore list overrides extension matches', () async { + final List ignoredFiles = [ + // Ignored base names. + 'flutter_export_environment.sh', + 'GeneratedPluginRegistrant.java', + 'GeneratedPluginRegistrant.m', + 'generated_plugin_registrant.cc', + 'generated_plugin_registrant.cpp', + // Ignored path suffixes. + 'foo.g.dart', + 'foo.mocks.dart', + // Ignored files. + 'resource.h', + ]; + + for (final String name in ignoredFiles) { + root.childFile(name).createSync(); + } + + await runner.run(['license-check']); + + for (final String name in ignoredFiles) { + expect(printedMessages, isNot(contains('Checking $name'))); + } + }); + + test('passes if all checked files have license blocks', () async { + final File checked = root.childFile('checked.cc'); + checked.createSync(); + _writeLicense(checked); + final File notChecked = root.childFile('not_checked.md'); + notChecked.createSync(); + + await runner.run(['license-check']); + + // Sanity check that the test did actually check a file. + expect(printedMessages, contains('Checking checked.cc')); + expect(printedMessages, contains('All source files passed validation!')); + }); + + test('handles the comment styles for all supported languages', () async { + final File fileA = root.childFile('file_a.cc'); + fileA.createSync(); + _writeLicense(fileA, comment: '// '); + final File fileB = root.childFile('file_b.sh'); + fileB.createSync(); + _writeLicense(fileB, comment: '# '); + final File fileC = root.childFile('file_c.html'); + fileC.createSync(); + _writeLicense(fileC, comment: '', prefix: ''); + + await runner.run(['license-check']); + + // Sanity check that the test did actually check the files. + expect(printedMessages, contains('Checking file_a.cc')); + expect(printedMessages, contains('Checking file_b.sh')); + expect(printedMessages, contains('Checking file_c.html')); + expect(printedMessages, contains('All source files passed validation!')); + }); + + test('fails if any checked files are missing license blocks', () async { + final File goodA = root.childFile('good.cc'); + goodA.createSync(); + _writeLicense(goodA); + final File goodB = root.childFile('good.h'); + goodB.createSync(); + _writeLicense(goodB); + root.childFile('bad.cc').createSync(); + root.childFile('bad.h').createSync(); + + await expectLater(() => runner.run(['license-check']), + throwsA(const TypeMatcher())); + + // Failure should give information about the problematic files. + expect( + printedMessages, + contains( + 'The license block for these files is missing or incorrect:')); + expect(printedMessages, contains(' bad.cc')); + expect(printedMessages, contains(' bad.h')); + // Failure shouldn't print the success message. + expect(printedMessages, + isNot(contains('All source files passed validation!'))); + }); + + test('fails if any checked files are missing just the copyright', () async { + final File good = root.childFile('good.cc'); + good.createSync(); + _writeLicense(good); + final File bad = root.childFile('bad.cc'); + bad.createSync(); + _writeLicense(bad, copyright: ''); + + await expectLater(() => runner.run(['license-check']), + throwsA(const TypeMatcher())); + + // Failure should give information about the problematic files. + expect( + printedMessages, + contains( + 'The license block for these files is missing or incorrect:')); + expect(printedMessages, contains(' bad.cc')); + // Failure shouldn't print the success message. + expect(printedMessages, + isNot(contains('All source files passed validation!'))); + }); + + test('fails if any checked files are missing just the license', () async { + final File good = root.childFile('good.cc'); + good.createSync(); + _writeLicense(good); + final File bad = root.childFile('bad.cc'); + bad.createSync(); + _writeLicense(bad, license: []); + + await expectLater(() => runner.run(['license-check']), + throwsA(const TypeMatcher())); + + // Failure should give information about the problematic files. + expect( + printedMessages, + contains( + 'The license block for these files is missing or incorrect:')); + expect(printedMessages, contains(' bad.cc')); + // Failure shouldn't print the success message. + expect(printedMessages, + isNot(contains('All source files passed validation!'))); + }); + + test('fails if any third-party code is not in a third_party directory', + () async { + final File thirdPartyFile = root.childFile('third_party.cc'); + thirdPartyFile.createSync(); + _writeLicense(thirdPartyFile, copyright: 'Copyright 2017 Someone Else'); + + await expectLater(() => runner.run(['license-check']), + throwsA(const TypeMatcher())); + + // Failure should give information about the problematic files. + expect( + printedMessages, + contains( + 'The license block for these files is missing or incorrect:')); + expect(printedMessages, contains(' third_party.cc')); + // Failure shouldn't print the success message. + expect(printedMessages, + isNot(contains('All source files passed validation!'))); + }); + + test('succeeds for third-party code in a third_party directory', () async { + final File thirdPartyFile = root + .childDirectory('a_plugin') + .childDirectory('lib') + .childDirectory('src') + .childDirectory('third_party') + .childFile('file.cc'); + thirdPartyFile.createSync(recursive: true); + _writeLicense(thirdPartyFile, + copyright: 'Copyright 2017 Workiva Inc.', + license: [ + 'Licensed under the Apache License, Version 2.0 (the "License");', + 'you may not use this file except in compliance with the License.' + ]); + + await runner.run(['license-check']); + + // Sanity check that the test did actually check the file. + expect(printedMessages, + contains('Checking a_plugin/lib/src/third_party/file.cc')); + expect(printedMessages, contains('All source files passed validation!')); + }); + + test('allows first-party code in a third_party directory', () async { + final File firstPartyFileInThirdParty = root + .childDirectory('a_plugin') + .childDirectory('lib') + .childDirectory('src') + .childDirectory('third_party') + .childFile('first_party.cc'); + firstPartyFileInThirdParty.createSync(recursive: true); + _writeLicense(firstPartyFileInThirdParty); + + await runner.run(['license-check']); + + // Sanity check that the test did actually check the file. + expect(printedMessages, + contains('Checking a_plugin/lib/src/third_party/first_party.cc')); + expect(printedMessages, contains('All source files passed validation!')); + }); + + test('fails for licenses that the tool does not expect', () async { + final File good = root.childFile('good.cc'); + good.createSync(); + _writeLicense(good); + final File bad = root.childDirectory('third_party').childFile('bad.cc'); + bad.createSync(recursive: true); + _writeLicense(bad, license: [ + 'This program is free software: you can redistribute it and/or modify', + 'it under the terms of the GNU General Public License', + ]); + + await expectLater(() => runner.run(['license-check']), + throwsA(const TypeMatcher())); + + // Failure should give information about the problematic files. + expect( + printedMessages, + contains( + 'No recognized license was found for the following third-party files:')); + expect(printedMessages, contains(' third_party/bad.cc')); + // Failure shouldn't print the success message. + expect(printedMessages, + isNot(contains('All source files passed validation!'))); + }); + + test('Apache is not recognized for new authors without validation changes', + () async { + final File good = root.childFile('good.cc'); + good.createSync(); + _writeLicense(good); + final File bad = root.childDirectory('third_party').childFile('bad.cc'); + bad.createSync(recursive: true); + _writeLicense( + bad, + copyright: 'Copyright 2017 Some New Authors.', + license: [ + 'Licensed under the Apache License, Version 2.0 (the "License");', + 'you may not use this file except in compliance with the License.' + ], + ); + + await expectLater(() => runner.run(['license-check']), + throwsA(const TypeMatcher())); + + // Failure should give information about the problematic files. + expect( + printedMessages, + contains( + 'No recognized license was found for the following third-party files:')); + expect(printedMessages, contains(' third_party/bad.cc')); + // Failure shouldn't print the success message. + expect(printedMessages, + isNot(contains('All source files passed validation!'))); + }); + + test('passes if all first-party LICENSE files are correctly formatted', + () async { + final File license = root.childFile('LICENSE'); + license.createSync(); + license.writeAsStringSync(_correctLicenseFileText); + + await runner.run(['license-check']); + + // Sanity check that the test did actually check the file. + expect(printedMessages, contains('Checking LICENSE')); + expect(printedMessages, contains('All LICENSE files passed validation!')); + }); + + test('fails if any first-party LICENSE files are incorrectly formatted', + () async { + final File license = root.childFile('LICENSE'); + license.createSync(); + license.writeAsStringSync(_incorrectLicenseFileText); + + await expectLater(() => runner.run(['license-check']), + throwsA(const TypeMatcher())); + + expect(printedMessages, + isNot(contains('All LICENSE files passed validation!'))); + }); + + test('ignores third-party LICENSE format', () async { + final File license = + root.childDirectory('third_party').childFile('LICENSE'); + license.createSync(recursive: true); + license.writeAsStringSync(_incorrectLicenseFileText); + + await runner.run(['license-check']); + + // The file shouldn't be checked. + expect(printedMessages, isNot(contains('Checking third_party/LICENSE'))); + expect(printedMessages, contains('All LICENSE files passed validation!')); + }); + }); +} + +const String _correctLicenseFileText = ''' +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +'''; + +// A common incorrect version created by copying text intended for a code file, +// with comment markers. +const String _incorrectLicenseFileText = ''' +// Copyright 2013 The Flutter Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +'''; diff --git a/script/tool/test/lint_podspecs_command_test.dart b/script/tool/test/lint_podspecs_command_test.dart new file mode 100644 index 000000000000..0183704f72c3 --- /dev/null +++ b/script/tool/test/lint_podspecs_command_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 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/lint_podspecs_command.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('$LintPodspecsCommand', () { + FileSystem fileSystem; + late Directory packagesDir; + late CommandRunner runner; + late MockPlatform mockPlatform; + late RecordingProcessRunner processRunner; + late List printedMessages; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + + printedMessages = []; + mockPlatform = MockPlatform(isMacOS: true); + processRunner = RecordingProcessRunner(); + final LintPodspecsCommand command = LintPodspecsCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + print: (Object? message) => printedMessages.add(message.toString()), + ); + + runner = + CommandRunner('podspec_test', 'Test for $LintPodspecsCommand'); + runner.addCommand(command); + final MockProcess mockLintProcess = MockProcess(); + mockLintProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockLintProcess; + processRunner.recordedCalls.clear(); + }); + + test('only runs on macOS', () async { + createFakePlugin('plugin1', packagesDir, withExtraFiles: >[ + ['plugin1.podspec'], + ]); + + mockPlatform.isMacOS = false; + await runner.run(['podspecs']); + + expect( + processRunner.recordedCalls, + equals([]), + ); + }); + + test('runs pod lib lint on a podspec', () async { + final Directory plugin1Dir = createFakePlugin('plugin1', packagesDir, + withExtraFiles: >[ + ['ios', 'plugin1.podspec'], + ['bogus.dart'], // Ignore non-podspecs. + ]); + + processRunner.resultStdout = 'Foo'; + processRunner.resultStderr = 'Bar'; + + await runner.run(['podspecs']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', const ['pod'], packagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), + '--configuration=Debug', + '--skip-tests', + '--use-modular-headers', + '--use-libraries' + ], + packagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'ios', 'plugin1.podspec'), + '--configuration=Debug', + '--skip-tests', + '--use-modular-headers', + ], + packagesDir.path), + ]), + ); + + expect(printedMessages, contains('Linting plugin1.podspec')); + expect(printedMessages, contains('Foo')); + expect(printedMessages, contains('Bar')); + }); + + test('skips podspecs with known issues', () async { + createFakePlugin('plugin1', packagesDir, withExtraFiles: >[ + ['plugin1.podspec'] + ]); + createFakePlugin('plugin2', packagesDir, withExtraFiles: >[ + ['plugin2.podspec'] + ]); + + await runner + .run(['podspecs', '--skip=plugin1', '--skip=plugin2']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', const ['pod'], packagesDir.path), + ]), + ); + }); + + test('allow warnings for podspecs with known warnings', () async { + final Directory plugin1Dir = createFakePlugin('plugin1', packagesDir, + withExtraFiles: >[ + ['plugin1.podspec'], + ]); + + await runner.run(['podspecs', '--ignore-warnings=plugin1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', const ['pod'], packagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'plugin1.podspec'), + '--configuration=Debug', + '--skip-tests', + '--use-modular-headers', + '--allow-warnings', + '--use-libraries' + ], + packagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + p.join(plugin1Dir.path, 'plugin1.podspec'), + '--configuration=Debug', + '--skip-tests', + '--use-modular-headers', + '--allow-warnings', + ], + packagesDir.path), + ]), + ); + + expect(printedMessages, contains('Linting plugin1.podspec')); + }); + }); +} diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart new file mode 100644 index 000000000000..02b898c5c3fc --- /dev/null +++ b/script/tool/test/list_command_test.dart @@ -0,0 +1,196 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/list_command.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('$ListCommand', () { + late FileSystem fileSystem; + late Directory packagesDir; + late CommandRunner runner; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + final ListCommand command = ListCommand(packagesDir); + + runner = CommandRunner('list_test', 'Test for $ListCommand'); + runner.addCommand(command); + }); + + test('lists plugins', () async { + createFakePlugin('plugin1', packagesDir); + createFakePlugin('plugin2', packagesDir); + + final List plugins = + await runCapturingPrint(runner, ['list', '--type=plugin']); + + expect( + plugins, + orderedEquals([ + '/packages/plugin1', + '/packages/plugin2', + ]), + ); + }); + + test('lists examples', () async { + createFakePlugin('plugin1', packagesDir, withSingleExample: true); + createFakePlugin('plugin2', packagesDir, + withExamples: ['example1', 'example2']); + createFakePlugin('plugin3', packagesDir); + + final List examples = + await runCapturingPrint(runner, ['list', '--type=example']); + + expect( + examples, + orderedEquals([ + '/packages/plugin1/example', + '/packages/plugin2/example/example1', + '/packages/plugin2/example/example2', + ]), + ); + }); + + test('lists packages', () async { + createFakePlugin('plugin1', packagesDir, withSingleExample: true); + createFakePlugin('plugin2', packagesDir, + withExamples: ['example1', 'example2']); + createFakePlugin('plugin3', packagesDir); + + final List packages = + await runCapturingPrint(runner, ['list', '--type=package']); + + expect( + packages, + unorderedEquals([ + '/packages/plugin1', + '/packages/plugin1/example', + '/packages/plugin2', + '/packages/plugin2/example/example1', + '/packages/plugin2/example/example2', + '/packages/plugin3', + ]), + ); + }); + + test('lists files', () async { + createFakePlugin('plugin1', packagesDir, withSingleExample: true); + createFakePlugin('plugin2', packagesDir, + withExamples: ['example1', 'example2']); + createFakePlugin('plugin3', packagesDir); + + final List examples = + await runCapturingPrint(runner, ['list', '--type=file']); + + expect( + examples, + unorderedEquals([ + '/packages/plugin1/pubspec.yaml', + '/packages/plugin1/example/pubspec.yaml', + '/packages/plugin2/pubspec.yaml', + '/packages/plugin2/example/example1/pubspec.yaml', + '/packages/plugin2/example/example2/pubspec.yaml', + '/packages/plugin3/pubspec.yaml', + ]), + ); + }); + + test('lists plugins using federated plugin layout', () async { + createFakePlugin('plugin1', packagesDir); + + // Create a federated plugin by creating a directory under the packages + // directory with several packages underneath. + final Directory 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); + + // Test without specifying `--type`. + final List plugins = + await runCapturingPrint(runner, ['list']); + + expect( + plugins, + unorderedEquals([ + '/packages/plugin1', + '/packages/my_plugin/my_plugin', + '/packages/my_plugin/my_plugin_web', + '/packages/my_plugin/my_plugin_macos', + ]), + ); + }); + + test('can filter plugins with the --plugins argument', () async { + createFakePlugin('plugin1', packagesDir); + + // 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); + + List plugins = await runCapturingPrint( + runner, ['list', '--plugins=plugin1']); + expect( + plugins, + unorderedEquals([ + '/packages/plugin1', + ]), + ); + + plugins = await runCapturingPrint( + runner, ['list', '--plugins=my_plugin']); + expect( + plugins, + unorderedEquals([ + '/packages/my_plugin/my_plugin', + '/packages/my_plugin/my_plugin_web', + '/packages/my_plugin/my_plugin_macos', + ]), + ); + + plugins = await runCapturingPrint( + runner, ['list', '--plugins=my_plugin/my_plugin_web']); + expect( + plugins, + unorderedEquals([ + '/packages/my_plugin/my_plugin_web', + ]), + ); + + plugins = await runCapturingPrint(runner, + ['list', '--plugins=my_plugin/my_plugin_web,plugin1']); + expect( + plugins, + unorderedEquals([ + '/packages/plugin1', + '/packages/my_plugin/my_plugin_web', + ]), + ); + }); + }); +} diff --git a/script/tool/test/mocks.dart b/script/tool/test/mocks.dart new file mode 100644 index 000000000000..ba6a03da7bcf --- /dev/null +++ b/script/tool/test/mocks.dart @@ -0,0 +1,48 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:mockito/mockito.dart'; +import 'package:platform/platform.dart'; + +class MockPlatform extends Mock implements Platform { + MockPlatform({this.isMacOS = false}); + + @override + bool isMacOS; +} + +class MockProcess extends Mock implements io.Process { + final Completer exitCodeCompleter = Completer(); + final StreamController> stdoutController = + StreamController>(); + final StreamController> stderrController = + StreamController>(); + final MockIOSink stdinMock = MockIOSink(); + + @override + int get pid => 99; + + @override + Future get exitCode => exitCodeCompleter.future; + + @override + Stream> get stdout => stdoutController.stream; + + @override + Stream> get stderr => stderrController.stream; + + @override + IOSink get stdin => stdinMock; +} + +class MockIOSink extends Mock implements IOSink { + List lines = []; + + @override + void writeln([Object? obj = '']) => lines.add(obj.toString()); +} diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart new file mode 100644 index 000000000000..e5722567f20c --- /dev/null +++ b/script/tool/test/publish_check_command_test.dart @@ -0,0 +1,366 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/publish_check_command.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('$PublishCheckProcessRunner tests', () { + FileSystem fileSystem; + late Directory packagesDir; + late PublishCheckProcessRunner processRunner; + late CommandRunner runner; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = PublishCheckProcessRunner(); + final PublishCheckCommand publishCheckCommand = + PublishCheckCommand(packagesDir, processRunner: processRunner); + + runner = CommandRunner( + 'publish_check_command', + 'Test for publish-check command.', + ); + runner.addCommand(publishCheckCommand); + }); + + test('publish check all packages', () async { + final Directory plugin1Dir = createFakePlugin('a', packagesDir); + final Directory plugin2Dir = createFakePlugin('b', packagesDir); + + processRunner.processesToReturn.add( + MockProcess()..exitCodeCompleter.complete(0), + ); + processRunner.processesToReturn.add( + MockProcess()..exitCodeCompleter.complete(0), + ); + await runner.run(['publish-check']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', + const ['pub', 'publish', '--', '--dry-run'], + plugin1Dir.path), + ProcessCall( + 'flutter', + const ['pub', 'publish', '--', '--dry-run'], + plugin2Dir.path), + ])); + }); + + test('fail on negative test', () async { + createFakePlugin('a', packagesDir); + + final MockProcess process = MockProcess(); + process.stdoutController.close(); // ignore: unawaited_futures + process.stderrController.close(); // ignore: unawaited_futures + process.exitCodeCompleter.complete(1); + + processRunner.processesToReturn.add(process); + + expect( + () => runner.run(['publish-check']), + throwsA(isA()), + ); + }); + + test('fail on bad pubspec', () async { + final Directory dir = createFakePlugin('c', packagesDir); + await dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); + + final MockProcess process = MockProcess(); + processRunner.processesToReturn.add(process); + + expect(() => runner.run(['publish-check']), + throwsA(isA())); + }); + + test('pass on prerelease if --allow-pre-release flag is on', () async { + createFakePlugin('d', packagesDir); + + const String preReleaseOutput = 'Package has 1 warning.' + 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'; + + final MockProcess process = MockProcess(); + process.stdoutController.add(preReleaseOutput.codeUnits); + process.stdoutController.close(); // ignore: unawaited_futures + process.stderrController.close(); // ignore: unawaited_futures + + process.exitCodeCompleter.complete(1); + + processRunner.processesToReturn.add(process); + + expect(runner.run(['publish-check', '--allow-pre-release']), + completes); + }); + + test('fail on prerelease if --allow-pre-release flag is off', () async { + createFakePlugin('d', packagesDir); + + const String preReleaseOutput = 'Package has 1 warning.' + 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'; + + final MockProcess process = MockProcess(); + process.stdoutController.add(preReleaseOutput.codeUnits); + process.stdoutController.close(); // ignore: unawaited_futures + process.stderrController.close(); // ignore: unawaited_futures + + process.exitCodeCompleter.complete(1); + + processRunner.processesToReturn.add(process); + + expect(runner.run(['publish-check']), throwsA(isA())); + }); + + test('Success message on stderr is not printed as an error', () async { + createFakePlugin('d', packagesDir); + + const String publishOutput = 'Package has 0 warnings.'; + + final MockProcess process = MockProcess(); + process.stderrController.add(publishOutput.codeUnits); + process.stdoutController.close(); // ignore: unawaited_futures + process.stderrController.close(); // ignore: unawaited_futures + + process.exitCodeCompleter.complete(0); + + processRunner.processesToReturn.add(process); + + final List output = + await runCapturingPrint(runner, ['publish-check']); + + expect(output, isNot(contains(contains('ERROR:')))); + }); + + test( + '--machine: Log JSON with status:no-publish and correct human message, if there are no packages need to be published. ', + () async { + const Map httpResponseA = { + 'name': 'a', + 'versions': [ + '0.0.1', + '0.1.0', + ], + }; + + const Map httpResponseB = { + 'name': 'b', + 'versions': [ + '0.0.1', + '0.1.0', + '0.2.0', + ], + }; + + final MockClient mockClient = MockClient((http.Request request) async { + if (request.url.pathSegments.last == 'no_publish_a.json') { + return http.Response(json.encode(httpResponseA), 200); + } else if (request.url.pathSegments.last == 'no_publish_b.json') { + return http.Response(json.encode(httpResponseB), 200); + } + return http.Response('', 500); + }); + final PublishCheckCommand command = PublishCheckCommand(packagesDir, + processRunner: processRunner, httpClient: mockClient); + + runner = CommandRunner( + 'publish_check_command', + 'Test for publish-check command.', + ); + runner.addCommand(command); + + final Directory plugin1Dir = + createFakePlugin('no_publish_a', packagesDir, includeVersion: true); + final Directory plugin2Dir = + createFakePlugin('no_publish_b', packagesDir, includeVersion: true); + + createFakePubspec(plugin1Dir, name: 'no_publish_a', version: '0.1.0'); + createFakePubspec(plugin2Dir, name: 'no_publish_b', version: '0.2.0'); + + processRunner.processesToReturn.add( + MockProcess()..exitCodeCompleter.complete(0), + ); + final List output = await runCapturingPrint( + runner, ['publish-check', '--machine']); + + // ignore: use_raw_strings + expect(output.first, ''' +{ + "status": "no-publish", + "humanMessage": [ + "Checking that no_publish_a can be published.", + "Package no_publish_a version: 0.1.0 has already be published on pub.", + "Checking that no_publish_b can be published.", + "Package no_publish_b version: 0.2.0 has already be published on pub.", + "SUCCESS: All packages passed publish check!" + ] +}'''); + }); + + test( + '--machine: Log JSON with status:needs-publish and correct human message, if there is at least 1 plugin needs to be published.', + () async { + const Map httpResponseA = { + 'name': 'a', + 'versions': [ + '0.0.1', + '0.1.0', + ], + }; + + const Map httpResponseB = { + 'name': 'b', + 'versions': [ + '0.0.1', + '0.1.0', + ], + }; + + final MockClient mockClient = MockClient((http.Request request) async { + if (request.url.pathSegments.last == 'no_publish_a.json') { + return http.Response(json.encode(httpResponseA), 200); + } else if (request.url.pathSegments.last == 'no_publish_b.json') { + return http.Response(json.encode(httpResponseB), 200); + } + return http.Response('', 500); + }); + final PublishCheckCommand command = PublishCheckCommand(packagesDir, + processRunner: processRunner, httpClient: mockClient); + + runner = CommandRunner( + 'publish_check_command', + 'Test for publish-check command.', + ); + runner.addCommand(command); + + final Directory plugin1Dir = + createFakePlugin('no_publish_a', packagesDir, includeVersion: true); + final Directory plugin2Dir = + createFakePlugin('no_publish_b', packagesDir, includeVersion: true); + + createFakePubspec(plugin1Dir, name: 'no_publish_a', version: '0.1.0'); + createFakePubspec(plugin2Dir, name: 'no_publish_b', version: '0.2.0'); + + processRunner.processesToReturn.add( + MockProcess()..exitCodeCompleter.complete(0), + ); + + final List output = await runCapturingPrint( + runner, ['publish-check', '--machine']); + + // ignore: use_raw_strings + expect(output.first, ''' +{ + "status": "needs-publish", + "humanMessage": [ + "Checking that no_publish_a can be published.", + "Package no_publish_a version: 0.1.0 has already be published on pub.", + "Checking that no_publish_b can be published.", + "Package no_publish_b is able to be published.", + "SUCCESS: All packages passed publish check!" + ] +}'''); + }); + + test( + '--machine: Log correct JSON, if there is at least 1 plugin contains error.', + () async { + const Map httpResponseA = { + 'name': 'a', + 'versions': [ + '0.0.1', + '0.1.0', + ], + }; + + const Map httpResponseB = { + 'name': 'b', + 'versions': [ + '0.0.1', + '0.1.0', + ], + }; + + final MockClient mockClient = MockClient((http.Request request) async { + print('url ${request.url}'); + print(request.url.pathSegments.last); + if (request.url.pathSegments.last == 'no_publish_a.json') { + return http.Response(json.encode(httpResponseA), 200); + } else if (request.url.pathSegments.last == 'no_publish_b.json') { + return http.Response(json.encode(httpResponseB), 200); + } + return http.Response('', 500); + }); + final PublishCheckCommand command = PublishCheckCommand(packagesDir, + processRunner: processRunner, httpClient: mockClient); + + runner = CommandRunner( + 'publish_check_command', + 'Test for publish-check command.', + ); + runner.addCommand(command); + + final Directory plugin1Dir = + createFakePlugin('no_publish_a', packagesDir, includeVersion: true); + final Directory plugin2Dir = + createFakePlugin('no_publish_b', packagesDir, includeVersion: true); + + createFakePubspec(plugin1Dir, name: 'no_publish_a', version: '0.1.0'); + createFakePubspec(plugin2Dir, name: 'no_publish_b', version: '0.2.0'); + await plugin1Dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); + + processRunner.processesToReturn.add( + MockProcess()..exitCodeCompleter.complete(0), + ); + + bool hasError = false; + final List output = await runCapturingPrint( + runner, ['publish-check', '--machine'], + errorHandler: (Error error) { + expect(error, isA()); + hasError = true; + }); + expect(hasError, isTrue); + + // ignore: use_raw_strings + expect(output.first, ''' +{ + "status": "error", + "humanMessage": [ + "Checking that no_publish_a can be published.", + "Failed to parse `pubspec.yaml` at /packages/no_publish_a/pubspec.yaml: ParsedYamlException: line 1, column 1: Not a map\\n ╷\\n1 │ bad-yaml\\n │ ^^^^^^^^\\n ╵}", + "no pubspec", + "Checking that no_publish_b can be published.", + "url https://pub.dev/packages/no_publish_b.json", + "no_publish_b.json", + "Package no_publish_b is able to be published.", + "ERROR: The following 1 package(s) failed the publishing check:\\nMemoryDirectory: '/packages/no_publish_a'" + ] +}'''); + }); + }); +} + +class PublishCheckProcessRunner extends RecordingProcessRunner { + final Queue processesToReturn = Queue(); + + @override + io.Process get processToReturn => processesToReturn.removeFirst(); +} diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart new file mode 100644 index 000000000000..1cb4245fdb73 --- /dev/null +++ b/script/tool/test/publish_plugin_command_test.dart @@ -0,0 +1,938 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; +import 'package:git/git.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + const String testPluginName = 'foo'; + late List printedMessages; + + late Directory testRoot; + late Directory packagesDir; + late Directory pluginDir; + late GitDir gitDir; + late TestProcessRunner processRunner; + late CommandRunner commandRunner; + late MockStdin mockStdin; + // This test uses a local file system instead of an in memory one throughout + // so that git actually works. In setup we initialize a mono repo of plugins + // with one package and commit everything to Git. + const FileSystem fileSystem = LocalFileSystem(); + + void _createMockCredentialFile() { + final String credentialPath = PublishPluginCommand.getCredentialPath(); + fileSystem.file(credentialPath) + ..createSync(recursive: true) + ..writeAsStringSync('some credential'); + } + + setUp(() async { + testRoot = fileSystem.systemTempDirectory + .createTempSync('publish_plugin_command_test-'); + // The temp directory can have symbolic links, which won't match git output; + // use a fully resolved version to avoid potential path comparison issues. + testRoot = fileSystem.directory(testRoot.resolveSymbolicLinksSync()); + packagesDir = createPackagesDirectory(parentDir: testRoot); + pluginDir = + createFakePlugin(testPluginName, packagesDir, withSingleExample: false); + assert(pluginDir != null && pluginDir.existsSync()); + createFakePubspec(pluginDir, version: '0.0.1'); + io.Process.runSync('git', ['init'], + workingDirectory: testRoot.path); + gitDir = await GitDir.fromExisting(testRoot.path); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Initial commit']); + processRunner = TestProcessRunner(); + mockStdin = MockStdin(); + printedMessages = []; + commandRunner = CommandRunner('tester', '') + ..addCommand(PublishPluginCommand(packagesDir, + processRunner: processRunner, + print: (Object? message) => printedMessages.add(message.toString()), + stdinput: mockStdin, + gitDir: gitDir)); + }); + + tearDown(() { + testRoot.deleteSync(recursive: true); + }); + + group('Initial validation', () { + test('requires a package flag', () async { + await expectLater(() => commandRunner.run(['publish-plugin']), + throwsA(const TypeMatcher())); + expect( + printedMessages.last, contains('Must specify a package to publish.')); + }); + + test('requires an existing flag', () async { + await expectLater( + () => commandRunner.run([ + 'publish-plugin', + '--package', + 'iamerror', + '--no-push-tags' + ]), + throwsA(const TypeMatcher())); + + expect(printedMessages.last, contains('iamerror does not exist')); + }); + + test('refuses to proceed with dirty files', () async { + pluginDir.childFile('tmp').createSync(); + + await expectLater( + () => commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags' + ]), + throwsA(const TypeMatcher())); + + expect( + printedMessages, + containsAllInOrder([ + '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\nIf the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.', + 'Failed, see above for details.', + ])); + }); + + test('fails immediately if the remote doesn\'t exist', () async { + await expectLater( + () => commandRunner + .run(['publish-plugin', '--package', testPluginName]), + throwsA(const TypeMatcher())); + expect(processRunner.results.last.stderr, contains('No such remote')); + }); + + test("doesn't validate the remote if it's not pushing tags", () async { + // Immediately return 0 when running `pub publish`. + processRunner.mockPublishCompleteCode = 0; + + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release' + ]); + + expect(printedMessages.last, 'Done!'); + }); + + test('can publish non-flutter package', () async { + createFakePubspec(pluginDir, version: '0.0.1', isFlutter: false); + io.Process.runSync('git', ['init'], + workingDirectory: testRoot.path); + gitDir = await GitDir.fromExisting(testRoot.path); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Initial commit']); + // Immediately return 0 when running `pub publish`. + processRunner.mockPublishCompleteCode = 0; + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release' + ]); + expect(printedMessages.last, 'Done!'); + }); + }); + + group('Publishes package', () { + test('while showing all output from pub publish to the user', () async { + final Future publishCommand = commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release' + ]); + + processRunner.mockPublishStdout = 'Foo'; + processRunner.mockPublishStderr = 'Bar'; + processRunner.mockPublishCompleteCode = 0; + + await publishCommand; + + expect(printedMessages, contains('Foo')); + expect(printedMessages, contains('Bar')); + }); + + test('forwards input from the user to `pub publish`', () async { + final Future publishCommand = commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release' + ]); + mockStdin.mockUserInputs.add(utf8.encode('user input')); + processRunner.mockPublishCompleteCode = 0; + + await publishCommand; + + expect(processRunner.mockPublishProcess.stdinMock.lines, + contains('user input')); + }); + + test('forwards --pub-publish-flags to pub publish', () async { + processRunner.mockPublishCompleteCode = 0; + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release', + '--pub-publish-flags', + '--dry-run,--server=foo' + ]); + + expect(processRunner.mockPublishArgs.length, 4); + expect(processRunner.mockPublishArgs[0], 'pub'); + expect(processRunner.mockPublishArgs[1], 'publish'); + expect(processRunner.mockPublishArgs[2], '--dry-run'); + expect(processRunner.mockPublishArgs[3], '--server=foo'); + }); + + test( + '--skip-confirmation flag automatically adds --force to --pub-publish-flags', + () async { + processRunner.mockPublishCompleteCode = 0; + _createMockCredentialFile(); + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release', + '--skip-confirmation', + '--pub-publish-flags', + '--server=foo' + ]); + + expect(processRunner.mockPublishArgs.length, 4); + expect(processRunner.mockPublishArgs[0], 'pub'); + expect(processRunner.mockPublishArgs[1], 'publish'); + expect(processRunner.mockPublishArgs[2], '--server=foo'); + expect(processRunner.mockPublishArgs[3], '--force'); + }); + + test('throws if pub publish fails', () async { + processRunner.mockPublishCompleteCode = 128; + await expectLater( + () => commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + '--no-tag-release', + ]), + throwsA(const TypeMatcher())); + + expect(printedMessages, contains('Publish foo failed.')); + }); + + test('publish, dry run', () async { + // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. + processRunner.mockPublishCompleteCode = 1; + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--dry-run', + '--no-push-tags', + '--no-tag-release', + ]); + + expect(processRunner.pushTagsArgs, isEmpty); + expect( + printedMessages, + containsAllInOrder([ + '=============== DRY RUN ===============', + 'Running `pub publish ` in ${pluginDir.path}...\n', + 'Done!' + ])); + }); + }); + + group('Tags release', () { + test('with the version and name from the pubspec.yaml', () async { + processRunner.mockPublishCompleteCode = 0; + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + ]); + + final String? tag = + (await gitDir.runCommand(['show-ref', 'fake_package-v0.0.1'])) + .stdout as String?; + expect(tag, isNotEmpty); + }); + + test('only if publishing succeeded', () async { + processRunner.mockPublishCompleteCode = 128; + await expectLater( + () => commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-push-tags', + ]), + throwsA(const TypeMatcher())); + + expect(printedMessages, contains('Publish foo failed.')); + final String? tag = (await gitDir.runCommand( + ['show-ref', 'fake_package-v0.0.1'], + throwOnError: false)) + .stdout as String?; + expect(tag, isEmpty); + }); + }); + + group('Pushes tags', () { + setUp(() async { + await gitDir.runCommand( + ['remote', 'add', 'upstream', 'http://localhost:8000']); + }); + + test('requires user confirmation', () async { + processRunner.mockPublishCompleteCode = 0; + mockStdin.readLineOutput = 'help'; + await expectLater( + () => commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + ]), + throwsA(const TypeMatcher())); + + expect(printedMessages, contains('Tag push canceled.')); + }); + + test('to upstream by default', () async { + await gitDir.runCommand(['tag', 'garbage']); + processRunner.mockPublishCompleteCode = 0; + mockStdin.readLineOutput = 'y'; + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + ]); + + expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); + expect(processRunner.pushTagsArgs[1], 'upstream'); + expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); + expect(printedMessages.last, 'Done!'); + }); + + test('does not ask for user input if the --skip-confirmation flag is on', + () async { + await gitDir.runCommand(['tag', 'garbage']); + processRunner.mockPublishCompleteCode = 0; + _createMockCredentialFile(); + await commandRunner.run([ + 'publish-plugin', + '--skip-confirmation', + '--package', + testPluginName, + ]); + + expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); + expect(processRunner.pushTagsArgs[1], 'upstream'); + expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); + expect(printedMessages.last, 'Done!'); + }); + + test('to upstream by default, dry run', () async { + await gitDir.runCommand(['tag', 'garbage']); + // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. + processRunner.mockPublishCompleteCode = 1; + mockStdin.readLineOutput = 'y'; + await commandRunner.run( + ['publish-plugin', '--package', testPluginName, '--dry-run']); + + expect(processRunner.pushTagsArgs, isEmpty); + expect( + printedMessages, + containsAllInOrder([ + '=============== DRY RUN ===============', + 'Running `pub publish ` in ${pluginDir.path}...\n', + 'Tagging release fake_package-v0.0.1...', + 'Pushing tag to upstream...', + 'Done!' + ])); + }); + + test('to different remotes based on a flag', () async { + await gitDir.runCommand( + ['remote', 'add', 'origin', 'http://localhost:8001']); + processRunner.mockPublishCompleteCode = 0; + mockStdin.readLineOutput = 'y'; + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--remote', + 'origin', + ]); + + expect(processRunner.pushTagsArgs.isNotEmpty, isTrue); + expect(processRunner.pushTagsArgs[1], 'origin'); + expect(processRunner.pushTagsArgs[2], 'fake_package-v0.0.1'); + expect(printedMessages.last, 'Done!'); + }); + + test('only if tagging and pushing to remotes are both enabled', () async { + processRunner.mockPublishCompleteCode = 0; + await commandRunner.run([ + 'publish-plugin', + '--package', + testPluginName, + '--no-tag-release', + ]); + + expect(processRunner.pushTagsArgs.isEmpty, isTrue); + expect(printedMessages.last, 'Done!'); + }); + }); + + group('Auto release (all-changed flag)', () { + setUp(() async { + io.Process.runSync('git', ['init'], + workingDirectory: testRoot.path); + gitDir = await GitDir.fromExisting(testRoot.path); + await gitDir.runCommand( + ['remote', 'add', 'upstream', 'http://localhost:8000']); + }); + + test('can release newly created plugins', () async { + // Non-federated + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, withSingleExample: true); + // federated + final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, + withSingleExample: true, parentDirectoryName: 'plugin2'); + createFakePubspec(pluginDir1, + name: 'plugin1', isFlutter: false, version: '0.0.1'); + createFakePubspec(pluginDir2, + name: 'plugin2', isFlutter: false, version: '0.0.1'); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Add plugins']); + // Immediately return 0 when running `pub publish`. + processRunner.mockPublishCompleteCode = 0; + mockStdin.readLineOutput = 'y'; + await commandRunner + .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( + printedMessages, + containsAllInOrder([ + 'Checking local repo...', + 'Local repo is ready!', + 'Getting existing tags...', + 'Running `pub publish ` in ${pluginDir1.path}...\n', + 'Running `pub publish ` in ${pluginDir2.path}...\n', + 'Packages released: plugin1, plugin2', + 'Done!' + ])); + expect(processRunner.pushTagsArgs, isNotEmpty); + expect(processRunner.pushTagsArgs[0], 'push'); + expect(processRunner.pushTagsArgs[1], 'upstream'); + expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1'); + expect(processRunner.pushTagsArgs[3], 'push'); + expect(processRunner.pushTagsArgs[4], 'upstream'); + expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); + }); + + test('can release newly created plugins, while there are existing plugins', + () async { + // Prepare an exiting plugin and tag it + final Directory pluginDir0 = + createFakePlugin('plugin0', packagesDir, withSingleExample: true); + createFakePubspec(pluginDir0, + name: 'plugin0', isFlutter: false, version: '0.0.1'); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Add plugins']); + // Immediately return 0 when running `pub publish`. + processRunner.mockPublishCompleteCode = 0; + mockStdin.readLineOutput = 'y'; + await commandRunner + .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + processRunner.pushTagsArgs.clear(); + + // Non-federated + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, withSingleExample: true); + // federated + final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, + withSingleExample: true, parentDirectoryName: 'plugin2'); + createFakePubspec(pluginDir1, + name: 'plugin1', isFlutter: false, version: '0.0.1'); + createFakePubspec(pluginDir2, + name: 'plugin2', isFlutter: false, version: '0.0.1'); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Add plugins']); + // Immediately return 0 when running `pub publish`. + await commandRunner + .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( + printedMessages, + containsAllInOrder([ + 'Checking local repo...', + 'Local repo is ready!', + 'Getting existing tags...', + 'Running `pub publish ` in ${pluginDir1.path}...\n', + 'Running `pub publish ` in ${pluginDir2.path}...\n', + 'Packages released: plugin1, plugin2', + 'Done!' + ])); + expect(processRunner.pushTagsArgs, isNotEmpty); + expect(processRunner.pushTagsArgs[0], 'push'); + expect(processRunner.pushTagsArgs[1], 'upstream'); + expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1'); + expect(processRunner.pushTagsArgs[3], 'push'); + expect(processRunner.pushTagsArgs[4], 'upstream'); + expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); + }); + + test('can release newly created plugins, dry run', () async { + // Non-federated + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, withSingleExample: true); + // federated + final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, + withSingleExample: true, parentDirectoryName: 'plugin2'); + createFakePubspec(pluginDir1, + name: 'plugin1', isFlutter: false, version: '0.0.1'); + createFakePubspec(pluginDir2, + name: 'plugin2', isFlutter: false, version: '0.0.1'); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Add plugins']); + // Immediately return 1 when running `pub publish`. If dry-run does not work, test should throw. + processRunner.mockPublishCompleteCode = 1; + mockStdin.readLineOutput = 'y'; + await commandRunner.run([ + 'publish-plugin', + '--all-changed', + '--base-sha=HEAD~', + '--dry-run' + ]); + expect( + printedMessages, + containsAllInOrder([ + 'Checking local repo...', + 'Local repo is ready!', + '=============== DRY RUN ===============', + 'Getting existing tags...', + 'Running `pub publish ` in ${pluginDir1.path}...\n', + 'Tagging release plugin1-v0.0.1...', + 'Pushing tag to upstream...', + 'Running `pub publish ` in ${pluginDir2.path}...\n', + 'Tagging release plugin2-v0.0.1...', + 'Pushing tag to upstream...', + 'Packages released: plugin1, plugin2', + 'Done!' + ])); + expect(processRunner.pushTagsArgs, isEmpty); + }); + + test('version change triggers releases.', () async { + // Non-federated + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, withSingleExample: true); + // federated + final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, + withSingleExample: true, parentDirectoryName: 'plugin2'); + createFakePubspec(pluginDir1, + name: 'plugin1', isFlutter: false, version: '0.0.1'); + createFakePubspec(pluginDir2, + name: 'plugin2', isFlutter: false, version: '0.0.1'); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Add plugins']); + // Immediately return 0 when running `pub publish`. + processRunner.mockPublishCompleteCode = 0; + mockStdin.readLineOutput = 'y'; + await commandRunner + .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( + printedMessages, + containsAllInOrder([ + 'Checking local repo...', + 'Local repo is ready!', + 'Getting existing tags...', + 'Running `pub publish ` in ${pluginDir1.path}...\n', + 'Running `pub publish ` in ${pluginDir2.path}...\n', + 'Packages released: plugin1, plugin2', + 'Done!' + ])); + expect(processRunner.pushTagsArgs, isNotEmpty); + expect(processRunner.pushTagsArgs[0], 'push'); + expect(processRunner.pushTagsArgs[1], 'upstream'); + expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1'); + expect(processRunner.pushTagsArgs[3], 'push'); + expect(processRunner.pushTagsArgs[4], 'upstream'); + expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); + + processRunner.pushTagsArgs.clear(); + printedMessages.clear(); + + final List plugin1Pubspec = + pluginDir1.childFile('pubspec.yaml').readAsLinesSync(); + plugin1Pubspec[plugin1Pubspec.indexWhere( + (String element) => element.contains('version:'))] = 'version: 0.0.2'; + pluginDir1 + .childFile('pubspec.yaml') + .writeAsStringSync(plugin1Pubspec.join('\n')); + final List plugin2Pubspec = + pluginDir2.childFile('pubspec.yaml').readAsLinesSync(); + plugin2Pubspec[plugin2Pubspec.indexWhere( + (String element) => element.contains('version:'))] = 'version: 0.0.2'; + pluginDir2 + .childFile('pubspec.yaml') + .writeAsStringSync(plugin2Pubspec.join('\n')); + await gitDir.runCommand(['add', '-A']); + await gitDir + .runCommand(['commit', '-m', 'Update versions to 0.0.2']); + + await commandRunner + .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( + printedMessages, + containsAllInOrder([ + 'Checking local repo...', + 'Local repo is ready!', + 'Getting existing tags...', + 'Running `pub publish ` in ${pluginDir1.path}...\n', + 'Running `pub publish ` in ${pluginDir2.path}...\n', + 'Packages released: plugin1, plugin2', + 'Done!' + ])); + + expect(processRunner.pushTagsArgs, isNotEmpty); + expect(processRunner.pushTagsArgs[0], 'push'); + expect(processRunner.pushTagsArgs[1], 'upstream'); + expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.2'); + expect(processRunner.pushTagsArgs[3], 'push'); + expect(processRunner.pushTagsArgs[4], 'upstream'); + expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.2'); + }); + + test( + 'delete package will not trigger publish but exit the command successfully.', + () async { + // Non-federated + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, withSingleExample: true); + // federated + final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, + withSingleExample: true, parentDirectoryName: 'plugin2'); + createFakePubspec(pluginDir1, + name: 'plugin1', isFlutter: false, version: '0.0.1'); + createFakePubspec(pluginDir2, + name: 'plugin2', isFlutter: false, version: '0.0.1'); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Add plugins']); + // Immediately return 0 when running `pub publish`. + processRunner.mockPublishCompleteCode = 0; + mockStdin.readLineOutput = 'y'; + await commandRunner + .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( + printedMessages, + containsAllInOrder([ + 'Checking local repo...', + 'Local repo is ready!', + 'Getting existing tags...', + 'Running `pub publish ` in ${pluginDir1.path}...\n', + 'Running `pub publish ` in ${pluginDir2.path}...\n', + 'Packages released: plugin1, plugin2', + 'Done!' + ])); + expect(processRunner.pushTagsArgs, isNotEmpty); + expect(processRunner.pushTagsArgs[0], 'push'); + expect(processRunner.pushTagsArgs[1], 'upstream'); + expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.1'); + expect(processRunner.pushTagsArgs[3], 'push'); + expect(processRunner.pushTagsArgs[4], 'upstream'); + expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.1'); + + processRunner.pushTagsArgs.clear(); + printedMessages.clear(); + + final List plugin1Pubspec = + pluginDir1.childFile('pubspec.yaml').readAsLinesSync(); + plugin1Pubspec[plugin1Pubspec.indexWhere( + (String element) => element.contains('version:'))] = 'version: 0.0.2'; + pluginDir1 + .childFile('pubspec.yaml') + .writeAsStringSync(plugin1Pubspec.join('\n')); + + pluginDir2.deleteSync(recursive: true); + + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand([ + 'commit', + '-m', + 'Update plugin1 versions to 0.0.2, delete plugin2' + ]); + + await commandRunner + .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( + printedMessages, + containsAllInOrder([ + 'Checking local repo...', + 'Local repo is ready!', + 'Getting existing tags...', + 'Running `pub publish ` in ${pluginDir1.path}...\n', + 'The file at 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', + 'Packages released: plugin1', + 'Done!' + ])); + + expect(processRunner.pushTagsArgs, isNotEmpty); + expect(processRunner.pushTagsArgs.length, 3); + expect(processRunner.pushTagsArgs[0], 'push'); + expect(processRunner.pushTagsArgs[1], 'upstream'); + expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.2'); + }); + + test( + 'versions revert do not trigger releases. Also prints out warning message.', + () async { + // Non-federated + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, withSingleExample: true); + // federated + final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, + withSingleExample: true, parentDirectoryName: 'plugin2'); + createFakePubspec(pluginDir1, + name: 'plugin1', isFlutter: false, version: '0.0.2'); + createFakePubspec(pluginDir2, + name: 'plugin2', isFlutter: false, version: '0.0.2'); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Add plugins']); + // Immediately return 0 when running `pub publish`. + processRunner.mockPublishCompleteCode = 0; + mockStdin.readLineOutput = 'y'; + await commandRunner + .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( + printedMessages, + containsAllInOrder([ + 'Checking local repo...', + 'Local repo is ready!', + 'Getting existing tags...', + 'Running `pub publish ` in ${pluginDir1.path}...\n', + 'Running `pub publish ` in ${pluginDir2.path}...\n', + 'Packages released: plugin1, plugin2', + 'Done!' + ])); + expect(processRunner.pushTagsArgs, isNotEmpty); + expect(processRunner.pushTagsArgs[0], 'push'); + expect(processRunner.pushTagsArgs[1], 'upstream'); + expect(processRunner.pushTagsArgs[2], 'plugin1-v0.0.2'); + expect(processRunner.pushTagsArgs[3], 'push'); + expect(processRunner.pushTagsArgs[4], 'upstream'); + expect(processRunner.pushTagsArgs[5], 'plugin2-v0.0.2'); + + processRunner.pushTagsArgs.clear(); + printedMessages.clear(); + + final List plugin1Pubspec = + pluginDir1.childFile('pubspec.yaml').readAsLinesSync(); + plugin1Pubspec[plugin1Pubspec.indexWhere( + (String element) => element.contains('version:'))] = 'version: 0.0.1'; + pluginDir1 + .childFile('pubspec.yaml') + .writeAsStringSync(plugin1Pubspec.join('\n')); + final List plugin2Pubspec = + pluginDir2.childFile('pubspec.yaml').readAsLinesSync(); + plugin2Pubspec[plugin2Pubspec.indexWhere( + (String element) => element.contains('version:'))] = 'version: 0.0.1'; + pluginDir2 + .childFile('pubspec.yaml') + .writeAsStringSync(plugin2Pubspec.join('\n')); + await gitDir.runCommand(['add', '-A']); + await gitDir + .runCommand(['commit', '-m', 'Update versions to 0.0.1']); + + await commandRunner + .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( + printedMessages, + containsAllInOrder([ + 'Checking local repo...', + 'Local repo is ready!', + 'Getting existing tags...', + 'The new version (0.0.1) is lower than the current version (0.0.2) for plugin1.\nThis git commit is a revert, no release is tagged.', + 'The new version (0.0.1) is lower than the current version (0.0.2) for plugin2.\nThis git commit is a revert, no release is tagged.', + 'Done!' + ])); + + expect(processRunner.pushTagsArgs, isEmpty); + }); + + test('No version change does not release any plugins', () async { + // Non-federated + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, withSingleExample: true); + // federated + final Directory pluginDir2 = createFakePlugin('plugin2', packagesDir, + withSingleExample: true, parentDirectoryName: 'plugin2'); + createFakePubspec(pluginDir1, + name: 'plugin1', isFlutter: false, version: '0.0.1'); + createFakePubspec(pluginDir2, + name: 'plugin2', isFlutter: false, version: '0.0.1'); + + io.Process.runSync('git', ['init'], + workingDirectory: testRoot.path); + gitDir = await GitDir.fromExisting(testRoot.path); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Add plugins']); + + pluginDir1.childFile('plugin1.dart').createSync(); + pluginDir2.childFile('plugin2.dart').createSync(); + await gitDir.runCommand(['add', '-A']); + await gitDir.runCommand(['commit', '-m', 'Add dart files']); + + // Immediately return 0 when running `pub publish`. + processRunner.mockPublishCompleteCode = 0; + mockStdin.readLineOutput = 'y'; + await commandRunner + .run(['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( + printedMessages, + containsAllInOrder([ + 'Checking local repo...', + 'Local repo is ready!', + 'No version updates in this commit.', + 'Done!' + ])); + expect(processRunner.pushTagsArgs, isEmpty); + }); + }); +} + +class TestProcessRunner extends ProcessRunner { + final List results = []; + // Most recent returned publish process. + late MockProcess mockPublishProcess; + final List mockPublishArgs = []; + final MockProcessResult mockPushTagsResult = MockProcessResult(); + final List pushTagsArgs = []; + + String? mockPublishStdout; + String? mockPublishStderr; + int? mockPublishCompleteCode; + + @override + Future run( + String executable, + List args, { + Directory? workingDir, + bool exitOnError = false, + bool logOnError = false, + Encoding stdoutEncoding = io.systemEncoding, + Encoding stderrEncoding = io.systemEncoding, + }) async { + // Don't ever really push tags. + if (executable == 'git' && args.isNotEmpty && args[0] == 'push') { + pushTagsArgs.addAll(args); + return mockPushTagsResult; + } + + final io.ProcessResult result = io.Process.runSync(executable, args, + workingDirectory: workingDir?.path); + results.add(result); + if (result.exitCode != 0) { + throw ToolExit(result.exitCode); + } + return result; + } + + @override + Future start(String executable, List args, + {Directory? workingDirectory}) async { + /// Never actually publish anything. Start is always and only used for this + /// since it returns something we can route stdin through. + assert(executable == 'flutter' && + args.isNotEmpty && + args[0] == 'pub' && + args[1] == 'publish'); + mockPublishArgs.addAll(args); + mockPublishProcess = MockProcess(); + if (mockPublishStdout != null) { + mockPublishProcess.stdoutController.add(utf8.encode(mockPublishStdout!)); + } + if (mockPublishStderr != null) { + mockPublishProcess.stderrController.add(utf8.encode(mockPublishStderr!)); + } + if (mockPublishCompleteCode != null) { + mockPublishProcess.exitCodeCompleter.complete(mockPublishCompleteCode); + } + + return mockPublishProcess; + } +} + +class MockStdin extends Mock implements io.Stdin { + List> mockUserInputs = >[]; + late StreamController> _controller; + String? readLineOutput; + + @override + Stream transform(StreamTransformer, S> streamTransformer) { + // In the test context, only one `PublishPluginCommand` object is created for a single test case. + // However, sometimes, we need to run multiple commands in a single test case. + // In such situation, this `MockStdin`'s StreamController might be listened to more than once, which is not allowed. + // + // Create a new controller every time so this Stdin could be listened to multiple times. + _controller = StreamController>(); + mockUserInputs.forEach(_addUserInputsToSteam); + return _controller.stream.transform(streamTransformer); + } + + @override + StreamSubscription> listen(void onData(List event)?, + {Function? onError, void onDone()?, bool? cancelOnError}) { + return _controller.stream.listen(onData, + onError: onError, onDone: onDone, cancelOnError: cancelOnError); + } + + @override + String? readLineSync( + {Encoding encoding = io.systemEncoding, + bool retainNewlines = false}) => + readLineOutput; + + void _addUserInputsToSteam(List input) => _controller.add(input); +} + +class MockProcessResult extends Mock implements io.ProcessResult { + MockProcessResult({int exitCode = 0}) : _exitCode = exitCode; + + final int _exitCode; + + @override + int get exitCode => _exitCode; +} diff --git a/script/tool/test/pubspec_check_command_test.dart b/script/tool/test/pubspec_check_command_test.dart new file mode 100644 index 000000000000..576060d23a9d --- /dev/null +++ b/script/tool/test/pubspec_check_command_test.dart @@ -0,0 +1,333 @@ +// Copyright 2013 The Flutter Authors. All 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.dart'; +import 'package:flutter_plugin_tools/src/pubspec_check_command.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('test pubspec_check_command', () { + late CommandRunner runner; + late RecordingProcessRunner processRunner; + late FileSystem fileSystem; + late Directory packagesDir; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = fileSystem.currentDirectory.childDirectory('packages'); + createPackagesDirectory(parentDir: packagesDir.parent); + processRunner = RecordingProcessRunner(); + final PubspecCheckCommand command = + PubspecCheckCommand(packagesDir, processRunner: processRunner); + + runner = CommandRunner( + 'pubspec_check_command', 'Test for pubspec_check_command'); + runner.addCommand(command); + }); + + String headerSection( + String name, { + bool isPlugin = false, + bool includeRepository = true, + bool includeHomepage = false, + bool includeIssueTracker = true, + }) { + final String repoLink = 'https://github.com/flutter/' + '${isPlugin ? 'plugins' : 'packages'}/tree/master/packages/$name'; + final String issueTrackerLink = + 'https://github.com/flutter/flutter/issues?' + 'q=is%3Aissue+is%3Aopen+label%3A%22p%3A+$name%22'; + return ''' +name: $name +${includeRepository ? 'repository: $repoLink' : ''} +${includeHomepage ? 'homepage: $repoLink' : ''} +${includeIssueTracker ? 'issue_tracker: $issueTrackerLink' : ''} +version: 1.0.0 +'''; + } + + String environmentSection() { + return ''' +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" +'''; + } + + String flutterSection({bool isPlugin = false}) { + const String pluginEntry = ''' + plugin: + platforms: +'''; + return ''' +flutter: +${isPlugin ? pluginEntry : ''} +'''; + } + + String dependenciesSection() { + return ''' +dependencies: + flutter: + sdk: flutter +'''; + } + + String devDependenciesSection() { + return ''' +dev_dependencies: + flutter_test: + sdk: flutter +'''; + } + + test('passes for a plugin following conventions', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, withSingleExample: true); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final List output = await runCapturingPrint(runner, [ + 'pubspec-check', + ]); + + expect( + output, + containsAllInOrder([ + 'Checking plugin...', + 'Checking plugin/example...', + '\nNo pubspec issues found!', + ]), + ); + }); + + test('passes for a Flutter package following conventions', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, withSingleExample: true); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin')} +${environmentSection()} +${dependenciesSection()} +${devDependenciesSection()} +${flutterSection()} +'''); + + final List output = await runCapturingPrint(runner, [ + 'pubspec-check', + ]); + + expect( + output, + containsAllInOrder([ + 'Checking plugin...', + 'Checking plugin/example...', + '\nNo pubspec issues found!', + ]), + ); + }); + + test('passes for a minimal package following conventions', () async { + final Directory packageDirectory = packagesDir.childDirectory('package'); + packageDirectory.createSync(recursive: true); + + packageDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('package')} +${environmentSection()} +${dependenciesSection()} +'''); + + final List output = await runCapturingPrint(runner, [ + 'pubspec-check', + ]); + + expect( + output, + containsAllInOrder([ + 'Checking package...', + '\nNo pubspec issues found!', + ]), + ); + }); + + test('fails when homepage is included', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, withSingleExample: true); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, includeHomepage: true)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final Future> result = + runCapturingPrint(runner, ['pubspec-check']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + }); + + test('fails when repository is missing', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, withSingleExample: true); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, includeRepository: false)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final Future> result = + runCapturingPrint(runner, ['pubspec-check']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + }); + + test('fails when homepage is given instead of repository', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, withSingleExample: true); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, includeHomepage: true, includeRepository: false)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final Future> result = + runCapturingPrint(runner, ['pubspec-check']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + }); + + test('fails when issue tracker is missing', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, withSingleExample: true); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, includeIssueTracker: false)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final Future> result = + runCapturingPrint(runner, ['pubspec-check']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + }); + + test('fails when environment section is out of order', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, withSingleExample: true); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true)} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +${environmentSection()} +'''); + + final Future> result = + runCapturingPrint(runner, ['pubspec-check']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + }); + + test('fails when flutter section is out of order', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, withSingleExample: true); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true)} +${flutterSection(isPlugin: true)} +${environmentSection()} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final Future> result = + runCapturingPrint(runner, ['pubspec-check']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + }); + + test('fails when dependencies section is out of order', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, withSingleExample: true); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${devDependenciesSection()} +${dependenciesSection()} +'''); + + final Future> result = + runCapturingPrint(runner, ['pubspec-check']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + }); + + test('fails when devDependencies section is out of order', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, withSingleExample: true); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true)} +${environmentSection()} +${devDependenciesSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +'''); + + final Future> result = + runCapturingPrint(runner, ['pubspec-check']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + }); + }); +} diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart new file mode 100644 index 000000000000..5cbbdf5b8d46 --- /dev/null +++ b/script/tool/test/test_command_test.dart @@ -0,0 +1,156 @@ +// Copyright 2013 The Flutter Authors. All 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/test_command.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('$TestCommand', () { + late FileSystem fileSystem; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final TestCommand command = + TestCommand(packagesDir, processRunner: processRunner); + + runner = CommandRunner('test_test', 'Test for $TestCommand'); + runner.addCommand(command); + }); + + test('runs flutter test on each plugin', () async { + final Directory plugin1Dir = createFakePlugin('plugin1', packagesDir, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + final Directory plugin2Dir = createFakePlugin('plugin2', packagesDir, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + + await runner.run(['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', const ['test', '--color'], plugin1Dir.path), + ProcessCall( + 'flutter', const ['test', '--color'], plugin2Dir.path), + ]), + ); + }); + + test('skips testing plugins without test directory', () async { + createFakePlugin('plugin1', packagesDir); + final Directory plugin2Dir = createFakePlugin('plugin2', packagesDir, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + + await runner.run(['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', const ['test', '--color'], plugin2Dir.path), + ]), + ); + }); + + test('runs pub run test on non-Flutter packages', () async { + final Directory plugin1Dir = createFakePlugin('plugin1', packagesDir, + isFlutter: true, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + final Directory plugin2Dir = createFakePlugin('plugin2', packagesDir, + isFlutter: false, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + + await runner.run(['test', '--enable-experiment=exp1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', + const ['test', '--color', '--enable-experiment=exp1'], + plugin1Dir.path), + ProcessCall('dart', const ['pub', 'get'], plugin2Dir.path), + ProcessCall( + 'dart', + const ['pub', 'run', '--enable-experiment=exp1', 'test'], + plugin2Dir.path), + ]), + ); + }); + + test('runs on Chrome for web plugins', () async { + final Directory pluginDir = createFakePlugin( + 'plugin', + packagesDir, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ], + isFlutter: true, + isWebPlugin: true, + ); + + await runner.run(['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', + const ['test', '--color', '--platform=chrome'], + pluginDir.path), + ]), + ); + }); + + test('enable-experiment flag', () async { + final Directory plugin1Dir = createFakePlugin('plugin1', packagesDir, + isFlutter: true, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + final Directory plugin2Dir = createFakePlugin('plugin2', packagesDir, + isFlutter: false, + withExtraFiles: >[ + ['test', 'empty_test.dart'], + ]); + + await runner.run(['test', '--enable-experiment=exp1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', + const ['test', '--color', '--enable-experiment=exp1'], + plugin1Dir.path), + ProcessCall('dart', const ['pub', 'get'], plugin2Dir.path), + ProcessCall( + 'dart', + const ['pub', 'run', '--enable-experiment=exp1', 'test'], + plugin2Dir.path), + ]), + ); + }); + }); +} diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart new file mode 100644 index 000000000000..c590d8a4bb04 --- /dev/null +++ b/script/tool/test/util.dart @@ -0,0 +1,329 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:meta/meta.dart'; +import 'package:quiver/collection.dart'; + +/// Creates a packages directory in the given location. +/// +/// If [parentDir] is set the packages directory will be created there, +/// otherwise [fileSystem] must be provided and it will be created an arbitrary +/// location in that filesystem. +Directory createPackagesDirectory( + {Directory? parentDir, FileSystem? fileSystem}) { + assert(parentDir != null || fileSystem != null, + 'One of parentDir or fileSystem must be provided'); + assert(fileSystem == null || fileSystem is MemoryFileSystem, + 'If using a real filesystem, parentDir must be provided'); + final Directory packagesDir = + (parentDir ?? fileSystem!.currentDirectory).childDirectory('packages'); + packagesDir.createSync(); + return packagesDir; +} + +/// Creates a plugin package with the given [name] in [packagesDirectory]. +Directory createFakePlugin( + String name, + Directory packagesDirectory, { + bool withSingleExample = false, + List withExamples = const [], + List> withExtraFiles = const >[], + bool isFlutter = true, + // TODO(stuartmorgan): Change these platform switches to support type enums. + bool isAndroidPlugin = false, + bool isIosPlugin = false, + bool isWebPlugin = false, + bool isLinuxPlugin = false, + bool isMacOsPlugin = false, + bool isWindowsPlugin = false, + bool includeChangeLog = false, + bool includeVersion = false, + String version = '0.0.1', + String parentDirectoryName = '', +}) { + assert(!(withSingleExample && withExamples.isNotEmpty), + 'cannot pass withSingleExample and withExamples simultaneously'); + + Directory parentDirectory = packagesDirectory; + if (parentDirectoryName != '') { + parentDirectory = parentDirectory.childDirectory(parentDirectoryName); + } + final Directory pluginDirectory = parentDirectory.childDirectory(name); + pluginDirectory.createSync(recursive: true); + + createFakePubspec(pluginDirectory, + name: name, + isFlutter: isFlutter, + androidSupport: isAndroidPlugin ? PlatformSupport.inline : null, + iosSupport: isIosPlugin ? PlatformSupport.inline : null, + webSupport: isWebPlugin ? PlatformSupport.inline : null, + linuxSupport: isLinuxPlugin ? PlatformSupport.inline : null, + macosSupport: isMacOsPlugin ? PlatformSupport.inline : null, + windowsSupport: isWindowsPlugin ? PlatformSupport.inline : null, + version: includeVersion ? version : null); + if (includeChangeLog) { + createFakeCHANGELOG(pluginDirectory, ''' +## 0.0.1 + * Some changes. + '''); + } + + if (withSingleExample) { + final Directory exampleDir = pluginDirectory.childDirectory('example') + ..createSync(); + createFakePubspec(exampleDir, + name: '${name}_example', isFlutter: isFlutter, publishTo: 'none'); + } else if (withExamples.isNotEmpty) { + final Directory exampleDir = pluginDirectory.childDirectory('example') + ..createSync(); + for (final String example in withExamples) { + final Directory currentExample = exampleDir.childDirectory(example) + ..createSync(); + createFakePubspec(currentExample, + name: example, isFlutter: isFlutter, publishTo: 'none'); + } + } + + final FileSystem fileSystem = pluginDirectory.fileSystem; + for (final List file in withExtraFiles) { + final List newFilePath = [pluginDirectory.path, ...file]; + final File newFile = fileSystem.file(fileSystem.path.joinAll(newFilePath)); + newFile.createSync(recursive: true); + } + + return pluginDirectory; +} + +void createFakeCHANGELOG(Directory parent, String texts) { + parent.childFile('CHANGELOG.md').createSync(); + parent.childFile('CHANGELOG.md').writeAsStringSync(texts); +} + +/// Creates a `pubspec.yaml` file with a flutter dependency. +void createFakePubspec( + Directory parent, { + String name = 'fake_package', + bool isFlutter = true, + PlatformSupport? androidSupport, + PlatformSupport? iosSupport, + PlatformSupport? linuxSupport, + PlatformSupport? macosSupport, + PlatformSupport? webSupport, + PlatformSupport? windowsSupport, + String publishTo = 'http://no_pub_server.com', + String? version, +}) { + parent.childFile('pubspec.yaml').createSync(); + String yaml = ''' +name: $name +flutter: + plugin: + platforms: +'''; + if (androidSupport != null) { + yaml += _pluginPlatformSection('android', androidSupport, name); + } + if (iosSupport != null) { + yaml += _pluginPlatformSection('ios', iosSupport, name); + } + if (webSupport != null) { + yaml += _pluginPlatformSection('web', webSupport, name); + } + if (linuxSupport != null) { + yaml += _pluginPlatformSection('linux', linuxSupport, name); + } + if (macosSupport != null) { + yaml += _pluginPlatformSection('macos', macosSupport, name); + } + if (windowsSupport != null) { + yaml += _pluginPlatformSection('windows', windowsSupport, name); + } + if (isFlutter) { + 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. +'''; + } + parent.childFile('pubspec.yaml').writeAsStringSync(yaml); +} + +String _pluginPlatformSection( + String platform, PlatformSupport type, String packageName) { + if (type == PlatformSupport.federated) { + return ''' + $platform: + default_package: ${packageName}_$platform +'''; + } + switch (platform) { + case 'android': + return ''' + android: + package: io.flutter.plugins.fake + pluginClass: FakePlugin +'''; + case 'ios': + return ''' + ios: + pluginClass: FLTFakePlugin +'''; + case 'linux': + return ''' + linux: + pluginClass: FakePlugin +'''; + case 'macos': + return ''' + macos: + pluginClass: FakePlugin +'''; + case 'web': + return ''' + web: + pluginClass: FakePlugin + fileName: ${packageName}_web.dart +'''; + case 'windows': + return ''' + windows: + pluginClass: FakePlugin +'''; + default: + assert(false); + return ''; + } +} + +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 { + final List prints = []; + final ZoneSpecification spec = ZoneSpecification( + print: (_, __, ___, String message) { + prints.add(message); + }, + ); + try { + await Zone.current + .fork(specification: spec) + .run>(() => runner.run(args)); + } on Error catch (e) { + if (errorHandler == null) { + rethrow; + } + errorHandler(e); + } + + return prints; +} + +/// A mock [ProcessRunner] which records process calls. +class RecordingProcessRunner extends ProcessRunner { + io.Process? processToReturn; + final List recordedCalls = []; + + /// Populate for [io.ProcessResult] to use a String [stdout] instead of a [List] of [int]. + String? resultStdout; + + /// Populate for [io.ProcessResult] to use a String [stderr] instead of a [List] of [int]. + String? resultStderr; + + @override + Future runAndStream( + String executable, + List args, { + Directory? workingDir, + bool exitOnError = false, + }) async { + recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); + return Future.value( + processToReturn == null ? 0 : await processToReturn!.exitCode); + } + + /// Returns [io.ProcessResult] created from [processToReturn], [resultStdout], and [resultStderr]. + @override + Future run( + String executable, + List args, { + Directory? workingDir, + bool exitOnError = false, + bool logOnError = false, + Encoding stdoutEncoding = io.systemEncoding, + Encoding stderrEncoding = io.systemEncoding, + }) async { + recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); + + final io.Process? process = processToReturn; + final io.ProcessResult result = process == null + ? io.ProcessResult(1, 1, '', '') + : io.ProcessResult(process.pid, await process.exitCode, + resultStdout ?? process.stdout, resultStderr ?? process.stderr); + + return Future.value(result); + } + + @override + Future start(String executable, List args, + {Directory? workingDirectory}) async { + recordedCalls.add(ProcessCall(executable, args, workingDirectory?.path)); + return Future.value(processToReturn); + } +} + +/// A recorded process call. +@immutable +class ProcessCall { + const ProcessCall(this.executable, this.args, this.workingDir); + + /// The executable that was called. + final String executable; + + /// The arguments passed to [executable] in the call. + final List args; + + /// The working directory this process was called from. + final String? workingDir; + + @override + bool operator ==(dynamic other) { + return other is ProcessCall && + executable == other.executable && + listsEqual(args, other.args) && + workingDir == other.workingDir; + } + + @override + int get hashCode => + (executable.hashCode) ^ (args.hashCode) ^ (workingDir?.hashCode ?? 0); + + @override + String toString() { + final List command = [executable, ...args]; + return '"${command.join(' ')}" in $workingDir'; + } +} diff --git a/script/tool/test/version_check_test.dart b/script/tool/test/version_check_test.dart new file mode 100644 index 000000000000..1199c270642f --- /dev/null +++ b/script/tool/test/version_check_test.dart @@ -0,0 +1,830 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/version_check_command.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:mockito/mockito.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:test/test.dart'; + +import 'common_test.mocks.dart'; +import 'util.dart'; + +void testAllowedVersion( + String masterVersion, + String headVersion, { + bool allowed = true, + NextVersionType? nextVersionType, +}) { + final Version master = Version.parse(masterVersion); + final Version head = Version.parse(headVersion); + final Map allowedVersions = + getAllowedNextVersions(masterVersion: master, headVersion: head); + if (allowed) { + expect(allowedVersions, contains(head)); + if (nextVersionType != null) { + expect(allowedVersions[head], equals(nextVersionType)); + } + } else { + expect(allowedVersions, isNot(contains(head))); + } +} + +class MockProcessResult extends Mock implements io.ProcessResult {} + +const String _redColorMessagePrefix = '\x1B[31m'; +const String _redColorMessagePostfix = '\x1B[0m'; + +// Some error message was printed in a "Colorized" red message. So `\x1B[31m` and `\x1B[0m` needs to be included. +String _redColorString(String string) { + return '$_redColorMessagePrefix$string$_redColorMessagePostfix'; +} + +void main() { + const String indentation = ' '; + group('$VersionCheckCommand', () { + FileSystem fileSystem; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + late List> gitDirCommands; + String gitDiffResponse; + Map gitShowResponses; + late MockGitDir gitDir; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + gitDirCommands = >[]; + gitDiffResponse = ''; + gitShowResponses = {}; + gitDir = MockGitDir(); + 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] == 'diff') { + when(mockProcessResult.stdout as String?) + .thenReturn(gitDiffResponse); + } else 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); + }); + processRunner = RecordingProcessRunner(); + final VersionCheckCommand command = VersionCheckCommand(packagesDir, + processRunner: processRunner, gitDir: gitDir); + + runner = CommandRunner( + 'version_check_command', 'Test for $VersionCheckCommand'); + runner.addCommand(command); + }); + + test('allows valid version', () async { + createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); + gitDiffResponse = 'packages/plugin/pubspec.yaml'; + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', + }; + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + + expect( + output, + containsAllInOrder([ + 'No version check errors found!', + ]), + ); + expect(gitDirCommands.length, equals(3)); + expect( + gitDirCommands, + containsAll([ + equals(['diff', '--name-only', 'master', 'HEAD']), + equals(['show', 'master:packages/plugin/pubspec.yaml']), + equals(['show', 'HEAD:packages/plugin/pubspec.yaml']), + ])); + }); + + test('denies invalid version', () async { + createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); + gitDiffResponse = 'packages/plugin/pubspec.yaml'; + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 0.0.1', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 0.2.0', + }; + final Future> result = runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + expect(gitDirCommands.length, equals(3)); + expect( + gitDirCommands, + containsAll([ + equals(['diff', '--name-only', 'master', 'HEAD']), + equals(['show', 'master:packages/plugin/pubspec.yaml']), + equals(['show', 'HEAD:packages/plugin/pubspec.yaml']), + ])); + }); + + test('allows valid version without explicit base-sha', () async { + createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); + gitDiffResponse = 'packages/plugin/pubspec.yaml'; + gitShowResponses = { + 'abc123:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', + }; + final List output = + await runCapturingPrint(runner, ['version-check']); + + expect( + output, + containsAllInOrder([ + 'No version check errors found!', + ]), + ); + }); + + test('allows valid version for new package.', () async { + createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); + gitDiffResponse = 'packages/plugin/pubspec.yaml'; + gitShowResponses = { + 'HEAD:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + final List output = + await runCapturingPrint(runner, ['version-check']); + + expect( + output, + containsAllInOrder([ + '${indentation}Unable to find pubspec in master. Safe to ignore if the project is new.', + 'No version check errors found!', + ]), + ); + }); + + test('allows likely reverts.', () async { + createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); + gitDiffResponse = 'packages/plugin/pubspec.yaml'; + gitShowResponses = { + 'abc123:packages/plugin/pubspec.yaml': 'version: 0.6.2', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 0.6.1', + }; + final List output = + await runCapturingPrint(runner, ['version-check']); + + expect( + output, + containsAllInOrder([ + '${indentation}New version is lower than previous version. This is assumed to be a revert.', + ]), + ); + }); + + test('denies lower version that could not be a simple revert', () async { + createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); + gitDiffResponse = 'packages/plugin/pubspec.yaml'; + gitShowResponses = { + 'abc123:packages/plugin/pubspec.yaml': 'version: 0.6.2', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 0.5.1', + }; + final Future> result = + runCapturingPrint(runner, ['version-check']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + }); + + test('denies invalid version without explicit base-sha', () async { + createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); + gitDiffResponse = 'packages/plugin/pubspec.yaml'; + gitShowResponses = { + 'abc123:packages/plugin/pubspec.yaml': 'version: 0.0.1', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 0.2.0', + }; + final Future> result = + runCapturingPrint(runner, ['version-check']); + + await expectLater( + result, + throwsA(const TypeMatcher()), + ); + }); + + test('gracefully handles missing pubspec.yaml', () async { + final Directory pluginDir = createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); + gitDiffResponse = 'packages/plugin/pubspec.yaml'; + pluginDir.childFile('pubspec.yaml').deleteSync(); + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + + expect( + output, + orderedEquals([ + 'Determine diff with base sha: master', + 'Checking versions for packages/plugin/pubspec.yaml...', + ' Deleted; skipping.', + 'No version check errors found!', + ]), + ); + expect(gitDirCommands.length, equals(1)); + expect(gitDirCommands.first.join(' '), + equals('diff --name-only master HEAD')); + }); + + test('allows minor changes to platform interfaces', () async { + createFakePlugin('plugin_platform_interface', packagesDir, + includeChangeLog: true, includeVersion: true); + gitDiffResponse = 'packages/plugin_platform_interface/pubspec.yaml'; + gitShowResponses = { + 'master:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.0.0', + 'HEAD:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.1.0', + }; + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + expect( + output, + containsAllInOrder([ + 'No version check errors found!', + ]), + ); + expect(gitDirCommands.length, equals(3)); + expect( + gitDirCommands, + containsAll([ + equals(['diff', '--name-only', 'master', 'HEAD']), + equals([ + 'show', + 'master:packages/plugin_platform_interface/pubspec.yaml' + ]), + equals([ + 'show', + 'HEAD:packages/plugin_platform_interface/pubspec.yaml' + ]), + ])); + }); + + test('disallows breaking changes to platform interfaces', () async { + createFakePlugin('plugin_platform_interface', packagesDir, + includeChangeLog: true, includeVersion: true); + gitDiffResponse = 'packages/plugin_platform_interface/pubspec.yaml'; + gitShowResponses = { + 'master:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.0.0', + 'HEAD:packages/plugin_platform_interface/pubspec.yaml': + 'version: 2.0.0', + }; + final Future> output = runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + await expectLater( + output, + throwsA(const TypeMatcher()), + ); + expect(gitDirCommands.length, equals(3)); + expect( + gitDirCommands, + containsAll([ + equals(['diff', '--name-only', 'master', 'HEAD']), + equals([ + 'show', + 'master:packages/plugin_platform_interface/pubspec.yaml' + ]), + equals([ + 'show', + 'HEAD:packages/plugin_platform_interface/pubspec.yaml' + ]), + ])); + }); + + test('Allow empty lines in front of the first version in CHANGELOG', + () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); + + createFakePubspec(pluginDirectory, isFlutter: true, version: '1.0.1'); + const String changelog = ''' + + + +## 1.0.1 + +* Some changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + expect( + output, + containsAllInOrder([ + 'Checking the first version listed in CHANGELOG.md matches the version in pubspec.yaml for plugin.', + 'plugin passed version check', + 'No version check errors found!' + ]), + ); + }); + + test('Throws if versions in changelog and pubspec do not match', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); + + createFakePubspec(pluginDirectory, isFlutter: true, 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; + }); + expect(hasError, isTrue); + + expect( + output, + containsAllInOrder([ + _redColorString(''' +versions for plugin in CHANGELOG.md and pubspec.yaml do not match. +The version in pubspec.yaml is 1.0.1. +The first version listed in CHANGELOG.md is 1.0.2. +'''), + ]), + ); + }); + + test('Success if CHANGELOG and pubspec versions match', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); + + createFakePubspec(pluginDirectory, isFlutter: true, version: '1.0.1'); + const String changelog = ''' +## 1.0.1 + +* Some changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + expect( + output, + containsAllInOrder([ + 'Checking the first version listed in CHANGELOG.md matches the version in pubspec.yaml for plugin.', + 'plugin passed version check', + 'No version check errors found!' + ]), + ); + }); + + test( + 'Fail if pubspec version only matches an older version listed in CHANGELOG', + () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); + + createFakePubspec(pluginDirectory, isFlutter: true, version: '1.0.0'); + const String changelog = ''' +## 1.0.1 + +* Some changes. + +## 1.0.0 + +* Some other 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; + }); + expect(hasError, isTrue); + + expect( + output, + containsAllInOrder([ + _redColorString( + ''' +versions for plugin in CHANGELOG.md and pubspec.yaml do not match. +The version in pubspec.yaml is 1.0.0. +The first version listed in CHANGELOG.md is 1.0.1. +''', + ) + ]), + ); + }); + + test('Allow NEXT as a placeholder for gathering CHANGELOG entries', + () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); + + createFakePubspec(pluginDirectory, isFlutter: true, version: '1.0.0'); + const String changelog = ''' +## NEXT + +* Some changes that won't be published until the next time there's a release. + +## 1.0.0 + +* Some other changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + await expectLater( + output, + containsAllInOrder([ + 'Found NEXT; validating next version in the CHANGELOG.', + 'plugin passed version check', + 'No version check errors found!', + ]), + ); + }); + + test('Fail if NEXT is left in the CHANGELOG when adding a version bump', + () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); + + createFakePubspec(pluginDirectory, isFlutter: true, version: '1.0.1'); + const String changelog = ''' +## 1.0.1 + +* Some changes. + +## NEXT + +* Some changes that should have been folded in 1.0.1. + +## 1.0.0 + +* Some other 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; + }); + expect(hasError, isTrue); + + expect( + output, + containsAllInOrder([ + _redColorString( + ''' +When bumping the version for release, the NEXT section should be incorporated +into the new version's release notes. +''', + ) + ]), + ); + }); + + test('Fail if the version changes without replacing NEXT', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); + + createFakePubspec(pluginDirectory, isFlutter: true, version: '1.0.1'); + const String changelog = ''' +## NEXT + +* Some changes that should be listed as part of 1.0.1. + +## 1.0.0 + +* Some other changes. +'''; + 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; + }); + expect(hasError, isTrue); + + expect( + output, + containsAllInOrder([ + 'Found NEXT; validating next version in the CHANGELOG.', + _redColorString( + ''' +versions for plugin in CHANGELOG.md and pubspec.yaml do not match. +The version in pubspec.yaml is 1.0.1. +The first version listed in CHANGELOG.md is 1.0.0. +''', + ) + ]), + ); + }); + + test('allows valid against pub', () async { + const Map httpResponse = { + 'name': 'some_package', + 'versions': [ + '0.0.1', + '0.0.2', + '1.0.0', + ], + }; + final MockClient mockClient = MockClient((http.Request request) async { + return http.Response(json.encode(httpResponse), 200); + }); + final VersionCheckCommand command = VersionCheckCommand(packagesDir, + processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); + + runner = CommandRunner( + 'version_check_command', 'Test for $VersionCheckCommand'); + runner.addCommand(command); + + createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); + gitDiffResponse = 'packages/plugin/pubspec.yaml'; + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', + }; + final List output = await runCapturingPrint(runner, + ['version-check', '--base-sha=master', '--against-pub']); + + expect( + output, + containsAllInOrder([ + '${indentation}plugin: Current largest version on pub: 1.0.0', + 'No version check errors found!', + ]), + ); + }); + + test('denies invalid against pub', () async { + const Map httpResponse = { + 'name': 'some_package', + 'versions': [ + '0.0.1', + '0.0.2', + ], + }; + final MockClient mockClient = MockClient((http.Request request) async { + return http.Response(json.encode(httpResponse), 200); + }); + final VersionCheckCommand command = VersionCheckCommand(packagesDir, + processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); + + runner = CommandRunner( + 'version_check_command', 'Test for $VersionCheckCommand'); + runner.addCommand(command); + + createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); + gitDiffResponse = 'packages/plugin/pubspec.yaml'; + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', + }; + + bool hasError = false; + final List result = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + '--against-pub' + ], errorHandler: (Error e) { + expect(e, isA()); + hasError = true; + }); + expect(hasError, isTrue); + + expect( + result, + containsAllInOrder([ + _redColorString( + ''' +${indentation}Incorrectly updated version. +${indentation}HEAD: 2.0.0, pub: 0.0.2. +${indentation}Allowed versions: {1.0.0: NextVersionType.BREAKING_MAJOR, 0.1.0: NextVersionType.MINOR, 0.0.3: NextVersionType.PATCH}''', + ) + ]), + ); + }); + + test( + 'throw and print error message if http request failed when checking against pub', + () async { + final MockClient mockClient = MockClient((http.Request request) async { + return http.Response('xx', 400); + }); + final VersionCheckCommand command = VersionCheckCommand(packagesDir, + processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); + + runner = CommandRunner( + 'version_check_command', 'Test for $VersionCheckCommand'); + runner.addCommand(command); + + createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); + gitDiffResponse = 'packages/plugin/pubspec.yaml'; + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', + }; + bool hasError = false; + final List result = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + '--against-pub' + ], errorHandler: (Error e) { + expect(e, isA()); + hasError = true; + }); + expect(hasError, isTrue); + + expect( + result, + containsAllInOrder([ + _redColorString( + ''' +${indentation}Error fetching version on pub for plugin. +${indentation}HTTP Status 400 +${indentation}HTTP response: xx +''', + ) + ]), + ); + }); + + test('when checking against pub, allow any version if http status is 404.', + () async { + final MockClient mockClient = MockClient((http.Request request) async { + return http.Response('xx', 404); + }); + final VersionCheckCommand command = VersionCheckCommand(packagesDir, + processRunner: processRunner, gitDir: gitDir, httpClient: mockClient); + + runner = CommandRunner( + 'version_check_command', 'Test for $VersionCheckCommand'); + runner.addCommand(command); + + createFakePlugin('plugin', packagesDir, + includeChangeLog: true, includeVersion: true); + gitDiffResponse = 'packages/plugin/pubspec.yaml'; + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + 'HEAD:packages/plugin/pubspec.yaml': 'version: 2.0.0', + }; + final List result = await runCapturingPrint(runner, + ['version-check', '--base-sha=master', '--against-pub']); + + expect( + result, + containsAllInOrder([ + '${indentation}Unable to find package on pub server. Safe to ignore if the project is new.', + 'No version check errors found!', + ]), + ); + }); + }); + + group('Pre 1.0', () { + test('nextVersion allows patch version', () { + testAllowedVersion('0.12.0', '0.12.0+1', + nextVersionType: NextVersionType.PATCH); + testAllowedVersion('0.12.0+4', '0.12.0+5', + nextVersionType: NextVersionType.PATCH); + }); + + test('nextVersion does not allow jumping patch', () { + testAllowedVersion('0.12.0', '0.12.0+2', allowed: false); + testAllowedVersion('0.12.0+2', '0.12.0+4', allowed: false); + }); + + test('nextVersion does not allow going back', () { + testAllowedVersion('0.12.0', '0.11.0', allowed: false); + testAllowedVersion('0.12.0+2', '0.12.0+1', allowed: false); + testAllowedVersion('0.12.0+1', '0.12.0', allowed: false); + }); + + test('nextVersion allows minor version', () { + testAllowedVersion('0.12.0', '0.12.1', + nextVersionType: NextVersionType.MINOR); + testAllowedVersion('0.12.0+4', '0.12.1', + nextVersionType: NextVersionType.MINOR); + }); + + test('nextVersion does not allow jumping minor', () { + testAllowedVersion('0.12.0', '0.12.2', allowed: false); + testAllowedVersion('0.12.0+2', '0.12.3', allowed: false); + }); + }); + + group('Releasing 1.0', () { + test('nextVersion allows releasing 1.0', () { + testAllowedVersion('0.12.0', '1.0.0', + nextVersionType: NextVersionType.BREAKING_MAJOR); + testAllowedVersion('0.12.0+4', '1.0.0', + nextVersionType: NextVersionType.BREAKING_MAJOR); + }); + + test('nextVersion does not allow jumping major', () { + testAllowedVersion('0.12.0', '2.0.0', allowed: false); + testAllowedVersion('0.12.0+4', '2.0.0', allowed: false); + }); + + test('nextVersion does not allow un-releasing', () { + testAllowedVersion('1.0.0', '0.12.0+4', allowed: false); + testAllowedVersion('1.0.0', '0.12.0', allowed: false); + }); + }); + + group('Post 1.0', () { + test('nextVersion allows patch jumps', () { + testAllowedVersion('1.0.1', '1.0.2', + nextVersionType: NextVersionType.PATCH); + testAllowedVersion('1.0.0', '1.0.1', + nextVersionType: NextVersionType.PATCH); + }); + + test('nextVersion does not allow build jumps', () { + testAllowedVersion('1.0.1', '1.0.1+1', allowed: false); + testAllowedVersion('1.0.0+5', '1.0.0+6', allowed: false); + }); + + test('nextVersion does not allow skipping patches', () { + testAllowedVersion('1.0.1', '1.0.3', allowed: false); + testAllowedVersion('1.0.0', '1.0.6', allowed: false); + }); + + test('nextVersion allows minor version jumps', () { + testAllowedVersion('1.0.1', '1.1.0', + nextVersionType: NextVersionType.MINOR); + testAllowedVersion('1.0.0', '1.1.0', + nextVersionType: NextVersionType.MINOR); + }); + + test('nextVersion does not allow skipping minor versions', () { + testAllowedVersion('1.0.1', '1.2.0', allowed: false); + testAllowedVersion('1.1.0', '1.3.0', allowed: false); + }); + + test('nextVersion allows breaking changes', () { + testAllowedVersion('1.0.1', '2.0.0', + nextVersionType: NextVersionType.BREAKING_MAJOR); + testAllowedVersion('1.0.0', '2.0.0', + nextVersionType: NextVersionType.BREAKING_MAJOR); + }); + + test('nextVersion does not allow skipping major versions', () { + testAllowedVersion('1.0.1', '3.0.0', allowed: false); + testAllowedVersion('1.1.0', '2.3.0', allowed: false); + }); + }); +} diff --git a/script/tool/test/xctest_command_test.dart b/script/tool/test/xctest_command_test.dart new file mode 100644 index 000000000000..8ed8144562c9 --- /dev/null +++ b/script/tool/test/xctest_command_test.dart @@ -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 'dart:convert'; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common.dart'; +import 'package:flutter_plugin_tools/src/xctest_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +// Note: This uses `dynamic` deliberately, and should not be updated to Object, +// in order to ensure that the code correctly handles this return type from +// JSON decoding. +final Map _kDeviceListMap = { + 'runtimes': >[ + { + 'bundlePath': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime', + 'buildversion': '17A577', + 'runtimeRoot': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime/Contents/Resources/RuntimeRoot', + 'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-0', + 'version': '13.0', + 'isAvailable': true, + 'name': 'iOS 13.0' + }, + { + 'bundlePath': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime', + 'buildversion': '17L255', + 'runtimeRoot': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot', + 'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-4', + 'version': '13.4', + 'isAvailable': true, + 'name': 'iOS 13.4' + }, + { + 'bundlePath': + '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime', + 'buildversion': '17T531', + 'runtimeRoot': + '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot', + 'identifier': 'com.apple.CoreSimulator.SimRuntime.watchOS-6-2', + 'version': '6.2.1', + 'isAvailable': true, + 'name': 'watchOS 6.2' + } + ], + 'devices': { + 'com.apple.CoreSimulator.SimRuntime.iOS-13-4': >[ + { + 'dataPath': + '/Users/xxx/Library/Developer/CoreSimulator/Devices/2706BBEB-1E01-403E-A8E9-70E8E5A24774/data', + 'logPath': + '/Users/xxx/Library/Logs/CoreSimulator/2706BBEB-1E01-403E-A8E9-70E8E5A24774', + 'udid': '2706BBEB-1E01-403E-A8E9-70E8E5A24774', + 'isAvailable': true, + 'deviceTypeIdentifier': + 'com.apple.CoreSimulator.SimDeviceType.iPhone-8', + 'state': 'Shutdown', + 'name': 'iPhone 8' + }, + { + 'dataPath': + '/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data', + 'logPath': + '/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'udid': '1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'isAvailable': true, + 'deviceTypeIdentifier': + 'com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus', + 'state': 'Shutdown', + 'name': 'iPhone 8 Plus' + } + ] + } +}; + +void main() { + const String _kDestination = '--ios-destination'; + + group('test xctest_command', () { + late FileSystem fileSystem; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final XCTestCommand command = + XCTestCommand(packagesDir, processRunner: processRunner); + + runner = CommandRunner('xctest_command', 'Test for xctest_command'); + runner.addCommand(command); + }); + + test('Fails if no platforms are provided', () async { + expect( + () => runner.run(['xctest']), + throwsA(isA()), + ); + }); + + group('iOS', () { + test('skip if iOS is not supported', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: false, + isMacOsPlugin: true); + + createFakePubspec(pluginDirectory.childDirectory('example'), + isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + final List output = await runCapturingPrint(runner, + ['xctest', '--ios', _kDestination, 'foo_destination']); + expect( + output, contains('iOS is not implemented by this plugin package.')); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skip if iOS is implemented in a federated package', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + createFakePubspec(pluginDirectory, + iosSupport: PlatformSupport.federated); + + createFakePubspec(pluginDirectory.childDirectory('example'), + isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + final List output = await runCapturingPrint(runner, + ['xctest', '--ios', _kDestination, 'foo_destination']); + expect( + output, contains('iOS is not implemented by this plugin package.')); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('running with correct destination, exclude 1 plugin', () async { + final Directory pluginDirectory1 = + createFakePlugin('plugin1', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + final Directory pluginDirectory2 = + createFakePlugin('plugin2', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory1 = + pluginDirectory1.childDirectory('example'); + createFakePubspec(pluginExampleDirectory1, isFlutter: true); + final Directory pluginExampleDirectory2 = + pluginDirectory2.childDirectory('example'); + createFakePubspec(pluginExampleDirectory2, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + processRunner.resultStdout = + '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; + final List output = await runCapturingPrint(runner, [ + 'xctest', + '--ios', + _kDestination, + 'foo_destination', + '--exclude', + 'plugin1' + ]); + + expect(output, isNot(contains('Start running for plugin1...'))); + expect(output, contains('Start running for plugin2...')); + expect(output, + contains('Successfully ran iOS xctest for plugin2/example')); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + 'analyze', + '-workspace', + 'ios/Runner.xcworkspace', + '-configuration', + 'Debug', + '-scheme', + 'Runner', + '-destination', + 'foo_destination', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory2.path), + ])); + }); + + test('Not specifying --ios-destination assigns an available simulator', + () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + final Map schemeCommandResult = { + 'project': { + 'targets': ['bar_scheme', 'foo_scheme'] + } + }; + // For simplicity of the test, we combine all the mock results into a single mock result, each internal command + // will get this result and they should still be able to parse them correctly. + processRunner.resultStdout = + jsonEncode(schemeCommandResult..addAll(_kDeviceListMap)); + await runner.run(['xctest', '--ios']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + const ProcessCall( + 'xcrun', ['simctl', 'list', '--json'], null), + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + 'analyze', + '-workspace', + 'ios/Runner.xcworkspace', + '-configuration', + 'Debug', + '-scheme', + 'Runner', + '-destination', + 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + }); + + group('macOS', () { + test('skip if macOS is not supported', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isIosPlugin: true, + isMacOsPlugin: false); + + createFakePubspec(pluginDirectory.childDirectory('example'), + isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + final List output = await runCapturingPrint(runner, + ['xctest', '--macos', _kDestination, 'foo_destination']); + expect(output, + contains('macOS is not implemented by this plugin package.')); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skip if macOS is implemented in a federated package', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isMacOsPlugin: true); + createFakePubspec(pluginDirectory, + macosSupport: PlatformSupport.federated); + + createFakePubspec(pluginDirectory.childDirectory('example'), + isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + final List output = await runCapturingPrint(runner, + ['xctest', '--macos', _kDestination, 'foo_destination']); + expect(output, + contains('macOS is not implemented by this plugin package.')); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('runs for macOS plugin', () async { + final Directory pluginDirectory1 = + createFakePlugin('plugin', packagesDir, + withExtraFiles: >[ + ['example', 'test'], + ], + isMacOsPlugin: true); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + createFakePubspec(pluginExampleDirectory, isFlutter: true); + + final MockProcess mockProcess = MockProcess(); + mockProcess.exitCodeCompleter.complete(0); + processRunner.processToReturn = mockProcess; + processRunner.resultStdout = + '{"project":{"targets":["bar_scheme", "foo_scheme"]}}'; + final List output = await runCapturingPrint(runner, [ + 'xctest', + '--macos', + ]); + + expect(output, + contains('Successfully ran macOS xctest for plugin/example')); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'test', + 'analyze', + '-workspace', + 'macos/Runner.xcworkspace', + '-configuration', + 'Debug', + '-scheme', + 'Runner', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + }); + }); +} diff --git a/script/tool_runner.sh b/script/tool_runner.sh new file mode 100755 index 000000000000..d16e940d5a4d --- /dev/null +++ b/script/tool_runner.sh @@ -0,0 +1,78 @@ +#!/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 + +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +readonly REPO_DIR="$(dirname "$SCRIPT_DIR")" + +source "$SCRIPT_DIR/common.sh" + +# Plugins that are excluded from this task. +ALL_EXCLUDED=("") + +# Plugins that deliberately use their own analysis_options.yaml. +# +# This list should only be deleted from, never added to. This only exists +# because we adopted stricter analysis rules recently and needed to exclude +# already failing packages to start linting the repo as a whole. +# +# Finding all: `find packages -name analysis_options.yaml | sort | cut -d/ -f2` +# +# TODO(ecosystem): Remove everything from this list. https://github.com/flutter/flutter/issues/76229 +CUSTOM_ANALYSIS_PLUGINS=( + android_alarm_manager + android_intent + battery + camera + connectivity + cross_file + device_info + e2e + espresso + file_selector + flutter_plugin_android_lifecycle + google_maps_flutter + google_sign_in + image_picker + in_app_purchase + integration_test + ios_platform_images + local_auth + package_info + plugin_platform_interface + quick_actions + sensors + share + shared_preferences + url_launcher + video_player + webview_flutter + wifi_info_flutter +) + +# Comma-separated string of the list above +readonly CUSTOM_FLAG=$(IFS=, ; echo "${CUSTOM_ANALYSIS_PLUGINS[*]}") +# Set some default actions if run without arguments. +ACTIONS=("$@") +if [[ "${#ACTIONS[@]}" == 0 ]]; then + ACTIONS=("analyze" "--custom-analysis" "$CUSTOM_FLAG" "test" "java-test") +elif [[ "${ACTIONS[0]}" == "analyze" ]]; then + ACTIONS=("${ACTIONS[@]}" "--custom-analysis" "$CUSTOM_FLAG") +fi + +BRANCH_NAME="${BRANCH_NAME:-"$(git rev-parse --abbrev-ref HEAD)"}" + +# This has to be turned into a list and then split out to the command line, +# otherwise it gets treated as a single argument. +PLUGIN_SHARDING=($PLUGIN_SHARDING) + +if [[ "${BRANCH_NAME}" == "master" ]]; then + echo "Running for all packages" + (cd "$REPO_DIR" && plugin_tools "${ACTIONS[@]}" --exclude="$ALL_EXCLUDED" ${PLUGIN_SHARDING[@]}) +else + echo running "${ACTIONS[@]}" + (cd "$REPO_DIR" && plugin_tools "${ACTIONS[@]}" --run-on-changed-packages --exclude="$ALL_EXCLUDED" ${PLUGIN_SHARDING[@]}) +fi