diff --git a/.ci.yaml b/.ci.yaml new file mode 100644 index 000000000000..00aecdca1122 --- /dev/null +++ b/.ci.yaml @@ -0,0 +1,113 @@ +# 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/CI_YAML.md +enabled_branches: + - master + +platform_properties: + linux: + properties: + caches: >- + [ + ] + dependencies: > + [ + {"dependency": "curl"} + ] + device_type: none + os: Linux + windows: + properties: + caches: >- + [ + {"name": "vsbuild", "path": "vsbuild"}, + {"name": "pub_cache", "path": ".pub-cache"} + ] + dependencies: > + [ + {"dependency": "certs"} + ] + device_type: none + os: Windows + +targets: + - name: Windows win32-platform_tests master + recipe: plugins/plugins + timeout: 30 + properties: + add_recipes_cq: "true" + target_file: windows_build_and_platform_tests.yaml + dependencies: > + [ + {"dependency": "vs_build"} + ] + scheduler: luci + + - name: Windows win32-platform_tests stable + recipe: plugins/plugins + timeout: 30 + properties: + add_recipes_cq: "true" + target_file: windows_build_and_platform_tests.yaml + channel: stable + dependencies: > + [ + {"dependency": "vs_build"} + ] + scheduler: luci + + - name: Windows windows-build_all_plugins master + recipe: plugins/plugins + timeout: 30 + properties: + add_recipes_cq: "true" + target_file: build_all_plugins.yaml + dependencies: > + [ + {"dependency": "vs_build"} + ] + scheduler: luci + + - name: Windows windows-build_all_plugins stable + recipe: plugins/plugins + timeout: 30 + properties: + add_recipes_cq: "true" + target_file: build_all_plugins.yaml + channel: stable + dependencies: > + [ + {"dependency": "vs_build"} + ] + scheduler: luci + + - name: Windows uwp-platform_tests master + recipe: plugins/plugins + timeout: 30 + properties: + add_recipes_cq: "true" + target_file: uwp_build_and_platform_tests.yaml + dependencies: > + [ + {"dependency": "vs_build"} + ] + scheduler: luci + + - name: Windows plugin_tools_tests + recipe: plugins/plugins + timeout: 30 + properties: + add_recipes_cq: "true" + target_file: plugin_tools_tests.yaml + scheduler: luci + + - name: Linux ci_yaml plugins roller + recipe: infra/ci_yaml + timeout: 30 + scheduler: luci + runIf: + - .ci.yaml diff --git a/.ci/Dockerfile b/.ci/Dockerfile index 13ac087498d1..a3deb6948d90 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -1,17 +1,24 @@ -FROM cirrusci/flutter:stable +# The Flutter version is not important here, since the CI scripts update Flutter +# before running. What matters is that the base image is pinned to minimize +# unintended changes when modifying this file. +FROM cirrusci/flutter:2.2.2 -RUN sudo apt-get update -y +RUN apt-get update -y -RUN sudo apt-get install -y --no-install-recommends gnupg +# Required by Roboeletric and the Android SDK. +RUN apt-get install -y openjdk-8-jdk +ENV JAVA_HOME=/usr/lib/jvm/java-8-openjdk-amd64 + +RUN apt-get install -y --no-install-recommends gnupg # Add repo for gcloud sdk and install it RUN echo "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" | \ - 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 - -RUN sudo apt-get update && sudo apt-get install -y google-cloud-sdk && \ +RUN apt-get update && apt-get install -y google-cloud-sdk && \ gcloud config set core/disable_usage_reporting true && \ gcloud config set component_manager/disable_update_check true @@ -23,7 +30,21 @@ RUN yes | sdkmanager \ RUN yes | sdkmanager --licenses +# Install formatter. +RUN apt-get install -y clang-format + +# Install xvfb to allow running headless +RUN apt-get install -y xvfb libegl1-mesa +# Install Linux desktop build tool requirements. +RUN apt-get install -y clang cmake ninja-build file pkg-config +# Install necessary libraries. +RUN 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 +RUN apt-get update && apt-get install -y --no-install-recommends google-chrome-stable + +# Make Chrome the default so http: has a handler for url_launcher tests. +RUN 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 cd6072dfc7ba..000000000000 --- a/.ci/Dockerfile-LinuxDesktop +++ /dev/null @@ -1,21 +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 requirements. -RUN sudo apt-get install -y clang make cmake ninja-build rsync pkg-config diff --git a/.ci/scripts/build_all_plugins.sh b/.ci/scripts/build_all_plugins.sh new file mode 100644 index 000000000000..008dea7c5e13 --- /dev/null +++ b/.ci/scripts/build_all_plugins.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +cd all_plugins +flutter build windows --debug +flutter build windows --release diff --git a/.ci/scripts/build_examples_uwp.sh b/.ci/scripts/build_examples_uwp.sh new file mode 100644 index 000000000000..639cb054e4b7 --- /dev/null +++ b/.ci/scripts/build_examples_uwp.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +dart ./script/tool/bin/flutter_plugin_tools.dart build-examples --winuwp \ + --packages-for-branch diff --git a/.ci/scripts/build_examples_win32.sh b/.ci/scripts/build_examples_win32.sh new file mode 100644 index 000000000000..8c090f4b78d2 --- /dev/null +++ b/.ci/scripts/build_examples_win32.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +dart ./script/tool/bin/flutter_plugin_tools.dart build-examples --windows \ + --packages-for-branch diff --git a/.ci/scripts/create_all_plugins_app.sh b/.ci/scripts/create_all_plugins_app.sh new file mode 100644 index 000000000000..196fef9b06c9 --- /dev/null +++ b/.ci/scripts/create_all_plugins_app.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +dart ./script/tool/bin/flutter_plugin_tools.dart all-plugins-app \ + --output-dir=. --exclude script/configs/exclude_all_plugins_app.yaml diff --git a/.ci/scripts/drive_examples_win32.sh b/.ci/scripts/drive_examples_win32.sh new file mode 100644 index 000000000000..63abc06bec5a --- /dev/null +++ b/.ci/scripts/drive_examples_win32.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +dart ./script/tool/bin/flutter_plugin_tools.dart drive-examples --windows \ + --packages-for-branch diff --git a/.ci/scripts/native_test_win32.sh b/.ci/scripts/native_test_win32.sh new file mode 100644 index 000000000000..938515784412 --- /dev/null +++ b/.ci/scripts/native_test_win32.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +dart ./script/tool/bin/flutter_plugin_tools.dart native-test --windows \ + --no-integration --packages-for-branch diff --git a/.ci/scripts/plugin_tools_tests.sh b/.ci/scripts/plugin_tools_tests.sh new file mode 100644 index 000000000000..96eec4349f08 --- /dev/null +++ b/.ci/scripts/plugin_tools_tests.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# Copyright 2013 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +cd script/tool +dart pub run test diff --git a/.ci/scripts/prepare_tool.sh b/.ci/scripts/prepare_tool.sh new file mode 100644 index 000000000000..1095e2189a36 --- /dev/null +++ b/.ci/scripts/prepare_tool.sh @@ -0,0 +1,10 @@ +#!/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. + +# To set FETCH_HEAD for "git merge-base" to work +git fetch origin master + +cd script/tool +dart pub get diff --git a/.ci/targets/build_all_plugins.yaml b/.ci/targets/build_all_plugins.yaml new file mode 100644 index 000000000000..b51a5b18dfd9 --- /dev/null +++ b/.ci/targets/build_all_plugins.yaml @@ -0,0 +1,7 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + - name: create all_plugins app + script: .ci/scripts/create_all_plugins_app.sh + - name: build all_plugins + script: .ci/scripts/build_all_plugins.sh diff --git a/.ci/targets/plugin_tools_tests.yaml b/.ci/targets/plugin_tools_tests.yaml new file mode 100644 index 000000000000..265e74bdd06b --- /dev/null +++ b/.ci/targets/plugin_tools_tests.yaml @@ -0,0 +1,5 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + - name: tool unit tests + script: .ci/scripts/plugin_tools_tests.sh diff --git a/.ci/targets/uwp_build_and_platform_tests.yaml b/.ci/targets/uwp_build_and_platform_tests.yaml new file mode 100644 index 000000000000..a7f070776ff1 --- /dev/null +++ b/.ci/targets/uwp_build_and_platform_tests.yaml @@ -0,0 +1,5 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + - name: build examples (UWP) + script: .ci/scripts/build_examples_uwp.sh diff --git a/.ci/targets/windows_build_and_platform_tests.yaml b/.ci/targets/windows_build_and_platform_tests.yaml new file mode 100644 index 000000000000..cda3e57f75d2 --- /dev/null +++ b/.ci/targets/windows_build_and_platform_tests.yaml @@ -0,0 +1,9 @@ +tasks: + - name: prepare tool + script: .ci/scripts/prepare_tool.sh + - name: build examples (Win32) + script: .ci/scripts/build_examples_win32.sh + - name: native unit tests (Win32) + script: .ci/scripts/native_test_win32.sh + - name: drive examples (Win32) + script: .ci/scripts/drive_examples_win32.sh diff --git a/.cirrus.yml b/.cirrus.yml index 8120b79842d3..ef6b9c1b6d44 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -1,164 +1,266 @@ +gcp_credentials: ENCRYPTED[!48cff44dd32e9cc412d4d381c7fe68d373ca04cf2639f8192d21cb1a9ab5e21129651423a1cf88f3fd7fe2125c1cabd9!] + +# 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_TOOL: "./script/tool/bin/flutter_plugin_tools.dart" + +tool_setup_template: &TOOL_SETUP_TEMPLATE + tool_setup_script: + - git fetch origin master # To set FETCH_HEAD for "git merge-base" to work + - cd script/tool + - dart pub get + +flutter_upgrade_template: &FLUTTER_UPGRADE_TEMPLATE + upgrade_flutter_script: + # Ensure that the repository has all the branches. + - cd $FLUTTER_HOME + - git config remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" + - git fetch origin + # Switch to the requested branch. + - git checkout $CHANNEL + # Reset to upstream branch, rather than using pull, since the base image + # can sometimes be in a state where it has diverged from upstream (!). + - git reset --hard @{u} + # Run doctor to allow auditing of what version of Flutter the run is using. + - flutter doctor -v + << : *TOOL_SETUP_TEMPLATE + +build_all_plugins_app_template: &BUILD_ALL_PLUGINS_APP_TEMPLATE + create_all_plugins_app_script: + - dart $PLUGIN_TOOL all-plugins-app --output-dir=. --exclude script/configs/exclude_all_plugins_app.yaml + build_all_plugins_debug_script: + - cd all_plugins + - if [[ "$BUILD_ALL_ARGS" == "web" ]]; then + - echo "Skipping; web does not support debug builds" + - else + - flutter build $BUILD_ALL_ARGS --debug + - fi + build_all_plugins_release_script: + - cd all_plugins + - flutter build $BUILD_ALL_ARGS --release + +macos_template: &MACOS_TEMPLATE + # Only one macOS task can run in parallel without credits, so use them for + # PRs on macOS. + use_compute_credits: $CIRRUS_USER_COLLABORATOR == 'true' + osx_instance: + image: big-sur-xcode-12.5 + +# 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: - E2E_PATH: "./packages/e2e" - 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-linux # gce vm image + builder_image_project: flutter-cirrus + cluster_name: test-cluster + zone: us-central1-a + namespace: default matrix: - - name: publishable + ### Platform-agnostic tasks ### + - name: Linux plugin_tools_tests script: - - flutter channel master - - ./script/check_publish.sh + - cd script/tool + - dart pub run test + - name: publishable + env: + # TODO (mvanbeusekom): Temporary override to "stable" because of failure on "master". + # Remove override once https://github.com/dart-lang/pub/issues/3152 is resolved. + CHANNEL: stable + CHANGE_DESC: "$TMPDIR/change-description.txt" + version_check_script: + # For pre-submit, pass the PR description to the script to allow for + # platform version breaking version change justifications. + # For post-submit, ignore platform version breaking version changes. + # The PR description isn't reliably part of the commit message, so using + # the same flags as for presubmit would likely result in false-positive + # post-submit failures. + - if [[ $CIRRUS_PR == "" ]]; then + - ./script/tool_runner.sh version-check --ignore-platform-interface-breaks + - else + - echo "$CIRRUS_CHANGE_MESSAGE" > "$CHANGE_DESC" + - ./script/tool_runner.sh version-check --change-description-file="$CHANGE_DESC" + - fi + publish_check_script: ./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 - - name: test + format_script: ./script/tool_runner.sh format --fail-on-change + pubspec_script: ./script/tool_runner.sh pubspec-check + license_script: dart $PLUGIN_TOOL license-check + - name: federated_safety + # This check is only meaningful for PRs, as it validates changes + # rather than state. + only_if: $CIRRUS_PR != "" + script: ./script/tool_runner.sh federation-safety-check + - name: dart_unit_tests 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 - - name: build_all_plugins_apk + env: + matrix: + CHANNEL: "master" + CHANNEL: "stable" + tool_script: + - cd script/tool + - dart analyze --fatal-infos 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: e2e_web_smoke_test - # Tests e2e example test in web. - only_if: "changesInclude('.cirrus.yml', 'packages/e2e/**') || $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 $E2E_PATH/example/ - - flutter drive -v --target=test_driver/example_e2e.dart -d web-server --release --browser-name=chrome - - name: build-apks+java-test+firebase-test-lab + # DO NOT change the custom-analysis argument here without changing the Dart repo. + # See the comment in script/configs/custom_analysis.yaml for details. + - ./script/tool_runner.sh analyze --custom-analysis=script/configs/custom_analysis.yaml + ### Android tasks ### + - name: android-build_all_plugins env: + BUILD_ALL_ARGS: "apk" matrix: - PLUGIN_SHARDING: "--shardIndex 0 --shardCount 2" - PLUGIN_SHARDING: "--shardIndex 1 --shardCount 2" + CHANNEL: "master" + CHANNEL: "stable" + << : *BUILD_ALL_PLUGINS_APP_TEMPLATE + ### Web tasks ### + - name: web-build_all_plugins + env: + BUILD_ALL_ARGS: "web" + matrix: + CHANNEL: "master" + CHANNEL: "stable" + << : *BUILD_ALL_PLUGINS_APP_TEMPLATE + ### Linux desktop tasks ### + - name: linux-build_all_plugins + env: + BUILD_ALL_ARGS: "linux" + matrix: + CHANNEL: "master" + CHANNEL: "stable" + setup_script: + - flutter config --enable-linux-desktop + << : *BUILD_ALL_PLUGINS_APP_TEMPLATE + - name: linux-platform_tests + env: + matrix: + CHANNEL: "master" + CHANNEL: "stable" + build_script: + - flutter config --enable-linux-desktop + - ./script/tool_runner.sh build-examples --linux + native_test_script: + - ./script/tool_runner.sh native-test --linux --no-integration + 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-linux # 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: android-platform_tests + env: + matrix: + PLUGIN_SHARDING: "--shardIndex 0 --shardCount 4" + PLUGIN_SHARDING: "--shardIndex 1 --shardCount 4" + PLUGIN_SHARDING: "--shardIndex 2 --shardCount 4" + PLUGIN_SHARDING: "--shardIndex 3 --shardCount 4" matrix: CHANNEL: "master" CHANNEL: "stable" 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 + GCLOUD_FIREBASE_TESTLAB_KEY: ENCRYPTED[!c9446a7b11d5520c2ebce3c64ccc82fe6d146272cb06a4a4590e22c389f33153f951347a25422522df1a81fe2f085e9a!] + build_script: # 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 - # This is a temporary workaround until we figure how to properly configure - # a UTF8 locale on Cirrus (or until the Gradle bug is fixed). - # TODO(amirh): Set the locale to UTF8. - - echo "$CIRRUS_CHANGE_MESSAGE" > /tmp/cirrus_change_message.txt - - echo "$CIRRUS_COMMIT_MESSAGE" > /tmp/cirrus_commit_message.txt + # TODO(stuartmorgan): See https://github.com/flutter/flutter/issues/24935 - 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 + lint_script: + # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they + # might include non-ASCII characters which makes Gradle crash. + # TODO(stuartmorgan): See https://github.com/flutter/flutter/issues/24935 + - export CIRRUS_CHANGE_MESSAGE="" + - export CIRRUS_COMMIT_MESSAGE="" + - ./script/tool_runner.sh lint-android # must come after build-examples + native_unit_test_script: + # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they + # might include non-ASCII characters which makes Gradle crash. + # TODO(stuartmorgan): See https://github.com/flutter/flutter/issues/24935 + - export CIRRUS_CHANGE_MESSAGE="" + - export CIRRUS_COMMIT_MESSAGE="" + # Native integration tests are handled by firebase-test-lab below, so + # only run unit tests. + # Must come after build-examples. + - ./script/tool_runner.sh native-test --android --no-integration --exclude script/configs/exclude_native_unit_android.yaml + firebase_test_lab_script: + # Unsetting CIRRUS_CHANGE_MESSAGE and CIRRUS_COMMIT_MESSAGE as they + # might include non-ASCII characters which makes Gradle crash. + # TODO(stuartmorgan): See https://github.com/flutter/flutter/issues/24935 + - export CIRRUS_CHANGE_MESSAGE="" + - export CIRRUS_COMMIT_MESSAGE="" + - if [[ -n "$GCLOUD_FIREBASE_TESTLAB_KEY" ]]; then - echo $GCLOUD_FIREBASE_TESTLAB_KEY > ${HOME}/gcloud-service-key.json - - ./script/incremental_build.sh firebase-test-lab + - ./script/tool_runner.sh firebase-test-lab --device model=flame,version=29 --device model=starqlteue,version=26 --exclude=script/configs/exclude_integration_android.yaml + - else + - echo "This user does not have permission to run Firebase Test Lab tests." - fi - - 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: - E2E_PATH: "./packages/e2e" - 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 + # Upload the full lint results to Cirrus to display in the results UI. + always: + android-lint_artifacts: + path: "**/reports/lint-results-debug.xml" + type: text/xml + format: android-lint + ### Web tasks ### + - name: web-platform_tests + env: + 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 + chromedriver_background_script: + - cd web_installers/packages/web_drivers/ + - 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=script/configs/exclude_integration_web.yaml +# 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 + ### iOS+macOS tasks *** + - name: darwin-lint_podspecs script: - # TODO(jackson): Allow web plugins once supported on stable - # https://github.com/flutter/flutter/issues/42864 - - if [[ "$CHANNEL" -eq "stable" ]]; then find . | grep _web$ | xargs rm -rf; fi - - flutter channel $CHANNEL - - ./script/build_all_plugins_app.sh ios --no-codesign - - name: lint_darwin_plugins + - ./script/tool_runner.sh podspecs + ### iOS tasks ### + - name: ios-build_all_plugins env: + BUILD_ALL_ARGS: "ios --no-codesign" matrix: - PLUGIN_SHARDING: "--shardIndex 0 --shardCount 2" - PLUGIN_SHARDING: "--shardIndex 1 --shardCount 2" - script: - # TODO(jmagman): Lint macOS podspecs but skip any that fail library validation. - - find . -name "*.podspec" | xargs grep -l "osx" | xargs rm - # Skip the dummy podspecs used to placate the tool. - - find . -name "*_web*.podspec" -o -name "*_mac*.podspec" | xargs rm - - ./script/incremental_build.sh podspecs --no-analyze camera --ignore-warnings camera - - name: build-ipas+drive-examples + CHANNEL: "master" + CHANNEL: "stable" + << : *BUILD_ALL_PLUGINS_APP_TEMPLATE + - name: ios-platform_tests env: PATH: $PATH:/usr/local/bin matrix: @@ -170,35 +272,42 @@ 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: - - flutter channel master - - ./script/build_all_plugins_app.sh macos - - name: build-apps+drive-examples + - ./script/tool_runner.sh build-examples --ios + xcode_analyze_script: + - ./script/tool_runner.sh xcode-analyze --ios + native_test_script: + - ./script/tool_runner.sh native-test --ios --ios-destination "platform=iOS Simulator,name=iPhone 11,OS=latest" --exclude=script/configs/exclude_native_ios.yaml + drive_script: + # `drive-examples` contains integration tests, which changes the UI of the application. + # This UI change sometimes affects `xctest`. + # So we run `drive-examples` after `native-test`; changing the order will result ci failure. + - ./script/tool_runner.sh drive-examples --ios --exclude=script/configs/exclude_integration_ios.yaml + ### macOS desktop tasks ### + - name: macos-build_all_plugins + env: + BUILD_ALL_ARGS: "macos" + matrix: + CHANNEL: "master" + CHANNEL: "stable" + setup_script: + - flutter config --enable-macos-desktop + << : *BUILD_ALL_PLUGINS_APP_TEMPLATE + - name: macos-platform_tests env: + matrix: + CHANNEL: "master" + CHANNEL: "stable" PATH: $PATH:/usr/local/bin build_script: - - flutter 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 + xcode_analyze_script: + - ./script/tool_runner.sh xcode-analyze --macos + native_test_script: + - ./script/tool_runner.sh native-test --macos --exclude=script/configs/exclude_native_macos.yaml + drive_script: + - ./script/tool_runner.sh drive-examples --macos diff --git a/.clang-format b/.clang-format new file mode 100644 index 000000000000..f6cb8ad931f5 --- /dev/null +++ b/.clang-format @@ -0,0 +1 @@ +BasedOnStyle: Google diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 083e125b32d2..a3a279ab2151 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 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 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]. -- [ ] 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? +- [ ] The title of the PR starts with the name of the plugin surrounded by square brackets, e.g. `[shared_preferences]` +- [ ] I listed at least one issue that this PR fixes in the description above. +- [ ] I [updated pubspec.yaml](https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages#version-and-changelog-updates) with an appropriate new version according to the [pub versioning philosophy]. +- [ ] I updated CHANGELOG.md to add a description of the change. +- [ ] I updated/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..825a3afd8508 --- /dev/null +++ b/.github/workflows/pull_request_label.yml @@ -0,0 +1,24 @@ +# 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: + permissions: + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@9794b1493b6f1fa7b006c5f8635a19c76c98be95 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" + sync-labels: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000000..a64acf7692f9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,54 @@ +name: release +on: + push: + branches: + - master + +jobs: + release: + if: github.repository_owner == 'flutter' + name: release + permissions: + # Release needs to push a tag back to the repo. + contents: write + runs-on: ubuntu-latest + steps: + - name: "Install Flutter" + # Github Actions don't support templates so it is hard to share this snippet with another action + # If we eventually need to use this in more workflow, we could create a shell script that contains this + # snippet. + run: | + cd $HOME + git clone https://github.com/flutter/flutter.git --depth 1 -b stable _flutter + echo "$HOME/_flutter/bin" >> $GITHUB_PATH + cd $GITHUB_WORKSPACE + # Checks out a copy of the repo. + - name: Check out code + uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f + with: + fetch-depth: 0 # Fetch all history so the tool can get all the tags to determine version. + - name: Set up tools + run: dart pub get + working-directory: ${{ github.workspace }}/script/tool + + # This workflow should be the last to run. So wait for all the other tests to succeed. + - name: Wait on all tests + uses: lewagon/wait-on-check-action@5e937358caba2c7876a2ee06e4a48d0664fe4967 + with: + ref: ${{ github.sha }} + running-workflow-name: 'release' + repo-token: ${{ secrets.GITHUB_TOKEN }} + wait-interval: 180 # seconds + allowed-conclusions: success,neutral + # verbose:true will produce too many logs that hang github actions web UI. + verbose: false + + - name: run release + run: | + git config --global user.name ${{ secrets.USER_NAME }} + git config --global user.email ${{ secrets.USER_EMAIL }} + dart ./script/tool/lib/src/main.dart publish-plugin --all-changed --base-sha=HEAD~ --skip-confirmation --remote=origin + env: {PUB_CREDENTIALS: "${{ secrets.PUB_CREDENTIALS }}"} + + env: + DEFAULT_BRANCH: master 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 b27c156188f8..0ca697b6a756 100644 --- a/AUTHORS +++ b/AUTHORS @@ -4,6 +4,7 @@ # Name/Organization Google Inc. +The Chromium Authors German Saprykin Benjamin Sauer larsenthomasj@gmail.com @@ -56,4 +57,11 @@ Giancarlo Rocha Ryo Miyake Théo Champion Kazuki Yamaguchi -Eitan Schwartz \ No newline at end of file +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 51a9407127c9..1d52dcefcbef 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -4,22 +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/e2e/* @collinjackson @digiter -packages/espresso/* @collinjackson @adazh -packages/google_maps_flutter/* @cyanglaz -packages/google_sign_in/* @cyanglaz @mehmetf -packages/image_picker/* @cyanglaz -packages/in_app_purchase/* @mklim @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 + +packages/camera/** @bparrishMines +packages/file_selector/** @ditman +packages/google_maps_flutter/** @cyanglaz +packages/image_picker/** @cyanglaz +packages/in_app_purchase/** @cyanglaz @LHLL +packages/ios_platform_images/** @gaaclarke diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f58dfea6a065..ac66886c1ff9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,220 +1,80 @@ # 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. +- [Contributing to Plugins and Packages](https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages), + for more information about how to make PRs for this repository, especially when + changing federated plugins. + +## Important note + +As of January 2021, we are no longer accepting non-critical PRs for the +[deprecated plugins](./README.md#deprecated), as all new development should +happen in the Flutter Community Plus replacements. If you have a PR for +something other than a critical issue (crashes, build failures, security issues) +in one of those pluigns, please [submit it to the Flutter Community Plus +replacement](https://github.com/fluttercommunity/plus_plugins/pulls) instead. + +## Other notes + +### Style + +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 7b995420294b..c6823b81eb84 100644 --- a/LICENSE +++ b/LICENSE @@ -1,27 +1,25 @@ -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: +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. + * 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. +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/README.md b/README.md index aeb03ffa23ac..b5b688ffbaef 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Flutter plugins [![Build Status](https://api.cirrus-ci.com/github/flutter/plugins.svg)](https://cirrus-ci.com/github/flutter/plugins/master) +[![Release Status](https://github.com/flutter/plugins/actions/workflows/release.yml/badge.svg)](https://github.com/flutter/plugins/actions/workflows/release.yml) This repo is a companion repo to the main [flutter repo](https://github.com/flutter/flutter). It contains the source code for @@ -19,6 +20,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 @@ -36,25 +40,39 @@ and send a [pull request](https://github.com/flutter/plugins/pulls). ## Plugins These are the available plugins in this repository. -| Plugin | 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_intent](./packages/android_intent/) | [![pub package](https://img.shields.io/pub/v/android_intent.svg)](https://pub.dev/packages/android_intent) | -| [battery](./packages/battery/) | [![pub package](https://img.shields.io/pub/v/battery.svg)](https://pub.dev/packages/battery) | -| [camera](./packages/camera/) | [![pub package](https://img.shields.io/pub/v/camera.svg)](https://pub.dev/packages/camera) | -| [connectivity](./packages/connectivity/) | [![pub package](https://img.shields.io/pub/v/connectivity.svg)](https://pub.dev/packages/connectivity) | -| [device_info](./packages/device_info/) | [![pub package](https://img.shields.io/pub/v/device_info.svg)](https://pub.dev/packages/device_info) | -| [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) | -| [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) | -| [image_picker](./packages/image_picker/) | [![pub package](https://img.shields.io/pub/v/image_picker.svg)](https://pub.dev/packages/image_picker) | -| [in_app_purchase](./packages/in_app_purchase/) | [![pub package](https://img.shields.io/pub/v/in_app_purchase.svg)](https://pub.dev/packages/in_app_purchase) | -| [local_auth](./packages/local_auth/) | [![pub package](https://img.shields.io/pub/v/local_auth.svg)](https://pub.dev/packages/local_auth) | -| [package_info](./packages/package_info/) | [![pub package](https://img.shields.io/pub/v/package_info.svg)](https://pub.dev/packages/package_info) | -| [path_provider](./packages/path_provider/) | [![pub package](https://img.shields.io/pub/v/path_provider.svg)](https://pub.dev/packages/path_provider) | -| [quick_actions](./packages/quick_actions/) | [![pub package](https://img.shields.io/pub/v/quick_actions.svg)](https://pub.dev/packages/quick_actions) | -| [sensors](./packages/sensors/) | [![pub package](https://img.shields.io/pub/v/sensors.svg)](https://pub.dev/packages/sensors) | -| [share](./packages/share/) | [![pub package](https://img.shields.io/pub/v/share.svg)](https://pub.dev/packages/share) | -| [shared_preferences](./packages/shared_preferences/) | [![pub package](https://img.shields.io/pub/v/shared_preferences.svg)](https://pub.dev/packages/shared_preferences) | -| [url_launcher](./packages/url_launcher/) | [![pub package](https://img.shields.io/pub/v/url_launcher.svg)](https://pub.dev/packages/url_launcher) | -| [video_player](./packages/video_player/) | [![pub package](https://img.shields.io/pub/v/video_player.svg)](https://pub.dev/packages/video_player) | -| [webview_flutter](./packages/webview_flutter/) | [![pub package](https://img.shields.io/pub/v/webview_flutter.svg)](https://pub.dev/packages/webview_flutter) | +| Plugin | Pub | Points | Popularity | Likes | +|--------|-----|--------|------------|-------| +| [camera](./packages/camera/) | [![pub package](https://img.shields.io/pub/v/camera.svg)](https://pub.dev/packages/camera) | [![pub points](https://badges.bar/camera/pub%20points)](https://pub.dev/packages/camera/score) | [![popularity](https://badges.bar/camera/popularity)](https://pub.dev/packages/camera/score) | [![likes](https://badges.bar/camera/likes)](https://pub.dev/packages/camera/score) | +| [espresso](./packages/espresso/) | [![pub package](https://img.shields.io/pub/v/espresso.svg)](https://pub.dev/packages/espresso) | [![pub points](https://badges.bar/espresso/pub%20points)](https://pub.dev/packages/espresso/score) | [![popularity](https://badges.bar/espresso/popularity)](https://pub.dev/packages/espresso/score) | [![likes](https://badges.bar/espresso/likes)](https://pub.dev/packages/espresso/score) | +| [file_selector](./packages/file_selector/) | [![pub package](https://img.shields.io/pub/v/file_selector.svg)](https://pub.dev/packages/file_selector) | [![pub points](https://badges.bar/file_selector/pub%20points)](https://pub.dev/packages/file_selector/score) | [![popularity](https://badges.bar/file_selector/popularity)](https://pub.dev/packages/file_selector/score) | [![likes](https://badges.bar/file_selector/likes)](https://pub.dev/packages/file_selector/score) | +| [flutter_plugin_android_lifecycle](./packages/flutter_plugin_android_lifecycle/) | [![pub package](https://img.shields.io/pub/v/flutter_plugin_android_lifecycle.svg)](https://pub.dev/packages/flutter_plugin_android_lifecycle) | [![pub points](https://badges.bar/flutter_plugin_android_lifecycle/pub%20points)](https://pub.dev/packages/flutter_plugin_android_lifecycle/score) | [![popularity](https://badges.bar/flutter_plugin_android_lifecycle/popularity)](https://pub.dev/packages/flutter_plugin_android_lifecycle/score) | [![likes](https://badges.bar/flutter_plugin_android_lifecycle/likes)](https://pub.dev/packages/flutter_plugin_android_lifecycle/score) | +| [google_maps_flutter](./packages/google_maps_flutter) | [![pub package](https://img.shields.io/pub/v/google_maps_flutter.svg)](https://pub.dev/packages/google_maps_flutter) | [![pub points](https://badges.bar/google_maps_flutter/pub%20points)](https://pub.dev/packages/google_maps_flutter/score) | [![popularity](https://badges.bar/google_maps_flutter/popularity)](https://pub.dev/packages/google_maps_flutter/score) | [![likes](https://badges.bar/google_maps_flutter/likes)](https://pub.dev/packages/google_maps_flutter/score) | +| [google_sign_in](./packages/google_sign_in/) | [![pub package](https://img.shields.io/pub/v/google_sign_in.svg)](https://pub.dev/packages/google_sign_in) | [![pub points](https://badges.bar/google_sign_in/pub%20points)](https://pub.dev/packages/google_sign_in/score) | [![popularity](https://badges.bar/google_sign_in/popularity)](https://pub.dev/packages/google_sign_in/score) | [![likes](https://badges.bar/google_sign_in/likes)](https://pub.dev/packages/google_sign_in/score) | +| [image_picker](./packages/image_picker/) | [![pub package](https://img.shields.io/pub/v/image_picker.svg)](https://pub.dev/packages/image_picker) | [![pub points](https://badges.bar/image_picker/pub%20points)](https://pub.dev/packages/image_picker/score) | [![popularity](https://badges.bar/image_picker/popularity)](https://pub.dev/packages/image_picker/score) | [![likes](https://badges.bar/image_picker/likes)](https://pub.dev/packages/image_picker/score) | +| [in_app_purchase](./packages/in_app_purchase/) | [![pub package](https://img.shields.io/pub/v/in_app_purchase.svg)](https://pub.dev/packages/in_app_purchase) | [![pub points](https://badges.bar/in_app_purchase/pub%20points)](https://pub.dev/packages/in_app_purchase/score) | [![popularity](https://badges.bar/in_app_purchase/popularity)](https://pub.dev/packages/in_app_purchase/score) | [![likes](https://badges.bar/in_app_purchase/likes)](https://pub.dev/packages/in_app_purchase/score) | +| [ios_platform_images](./packages/ios_platform_images/) | [![pub package](https://img.shields.io/pub/v/ios_platform_images.svg)](https://pub.dev/packages/ios_platform_images) | [![pub points](https://badges.bar/ios_platform_images/pub%20points)](https://pub.dev/packages/ios_platform_images/score) | [![popularity](https://badges.bar/ios_platform_images/popularity)](https://pub.dev/packages/ios_platform_images/score) | [![likes](https://badges.bar/ios_platform_images/likes)](https://pub.dev/packages/ios_platform_images/score) | +| [local_auth](./packages/local_auth/) | [![pub package](https://img.shields.io/pub/v/local_auth.svg)](https://pub.dev/packages/local_auth) | [![pub points](https://badges.bar/local_auth/pub%20points)](https://pub.dev/packages/local_auth/score) | [![popularity](https://badges.bar/local_auth/popularity)](https://pub.dev/packages/local_auth/score) | [![likes](https://badges.bar/local_auth/likes)](https://pub.dev/packages/local_auth/score) | +| [path_provider](./packages/path_provider/) | [![pub package](https://img.shields.io/pub/v/path_provider.svg)](https://pub.dev/packages/path_provider) | [![pub points](https://badges.bar/path_provider/pub%20points)](https://pub.dev/packages/path_provider/score) | [![popularity](https://badges.bar/path_provider/popularity)](https://pub.dev/packages/path_provider/score) | [![likes](https://badges.bar/path_provider/likes)](https://pub.dev/packages/path_provider/score) | +| [plugin_platform_interface](./packages/plugin_platform_interface/) | [![pub package](https://img.shields.io/pub/v/plugin_platform_interface.svg)](https://pub.dev/packages/plugin_platform_interface) | [![pub points](https://badges.bar/plugin_platform_interface/pub%20points)](https://pub.dev/packages/plugin_platform_interface/score) | [![popularity](https://badges.bar/plugin_platform_interface/popularity)](https://pub.dev/packages/plugin_platform_interface/score) | [![likes](https://badges.bar/plugin_platform_interface/likes)](https://pub.dev/packages/plugin_platform_interface/score) | +| [quick_actions](./packages/quick_actions/) | [![pub package](https://img.shields.io/pub/v/quick_actions.svg)](https://pub.dev/packages/quick_actions) | [![pub points](https://badges.bar/quick_actions/pub%20points)](https://pub.dev/packages/quick_actions/score) | [![popularity](https://badges.bar/quick_actions/popularity)](https://pub.dev/packages/quick_actions/score) | [![likes](https://badges.bar/quick_actions/likes)](https://pub.dev/packages/quick_actions/score) | +| [shared_preferences](./packages/shared_preferences/) | [![pub package](https://img.shields.io/pub/v/shared_preferences.svg)](https://pub.dev/packages/shared_preferences) | [![pub points](https://badges.bar/shared_preferences/pub%20points)](https://pub.dev/packages/shared_preferences/score) | [![popularity](https://badges.bar/shared_preferences/popularity)](https://pub.dev/packages/shared_preferences/score) | [![likes](https://badges.bar/shared_preferences/likes)](https://pub.dev/packages/shared_preferences/score) | +| [url_launcher](./packages/url_launcher/) | [![pub package](https://img.shields.io/pub/v/url_launcher.svg)](https://pub.dev/packages/url_launcher) | [![pub points](https://badges.bar/url_launcher/pub%20points)](https://pub.dev/packages/url_launcher/score) | [![popularity](https://badges.bar/url_launcher/popularity)](https://pub.dev/packages/url_launcher/score) | [![likes](https://badges.bar/url_launcher/likes)](https://pub.dev/packages/url_launcher/score) | +| [video_player](./packages/video_player/) | [![pub package](https://img.shields.io/pub/v/video_player.svg)](https://pub.dev/packages/video_player) | [![pub points](https://badges.bar/video_player/pub%20points)](https://pub.dev/packages/video_player/score) | [![popularity](https://badges.bar/video_player/popularity)](https://pub.dev/packages/video_player/score) | [![likes](https://badges.bar/video_player/likes)](https://pub.dev/packages/video_player/score) | +| [webview_flutter](./packages/webview_flutter/) | [![pub package](https://img.shields.io/pub/v/webview_flutter.svg)](https://pub.dev/packages/webview_flutter) | [![pub points](https://badges.bar/webview_flutter/pub%20points)](https://pub.dev/packages/webview_flutter/score) | [![popularity](https://badges.bar/webview_flutter/popularity)](https://pub.dev/packages/webview_flutter/score) | [![likes](https://badges.bar/webview_flutter/likes)](https://pub.dev/packages/webview_flutter/score) | + +### 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..793640e22d27 --- /dev/null +++ b/analysis_options_legacy.yaml @@ -0,0 +1,16 @@ +include: package:pedantic/analysis_options.1.8.0.yaml +analyzer: + exclude: + # Ignore generated files + - '**/*.g.dart' + - 'lib/src/generated/*.dart' + - '**/*.mocks.dart' # Mockito @GenerateMocks + errors: + always_require_non_null_named_parameters: false # not needed with nnbd + # TODO(https://github.com/flutter/flutter/issues/74381): + # Clean up existing unnecessary imports, and remove line to ignore. + unnecessary_import: ignore + unnecessary_null_comparison: false # Turned as long as nnbd mix-mode is supported. +linter: + rules: + - public_member_api_docs diff --git a/packages/android_alarm_manager/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 689429dc08f2..d53b932e3f0f 100644 --- a/packages/android_alarm_manager/CHANGELOG.md +++ b/packages/android_alarm_manager/CHANGELOG.md @@ -1,3 +1,57 @@ +## NEXT + +* Remove support for the V1 Android embedding. +* Updated Android lint settings. + +## 2.0.2 + +* Update README to point to Plus Plugins version. + +## 2.0.1 + +* Migrate maven repository from jcenter to mavenCentral. + +## 2.0.0 + +* Migrate to null safety. + +## 0.4.5+20 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 0.4.5+19 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 0.4.5+18 + +* Update Flutter SDK constraint. + +## 0.4.5+17 + +* Update Dart SDK constraint in example. + +## 0.4.5+16 + +* Remove unnecessary workaround from test. + +## 0.4.5+15 + +* Update android compileSdkVersion to 29. + +## 0.4.5+14 + +* Keep handling deprecated Android v1 classes for backward compatibility. + +## 0.4.5+13 + +* Android Code Inspection and Clean up. + +## 0.4.5+12 + +* Update package:e2e reference to use the local version in the flutter/plugins + repository. + ## 0.4.5+11 * Update lower bound of dart dependency to 2.1.0. diff --git a/packages/android_alarm_manager/LICENSE b/packages/android_alarm_manager/LICENSE index c89293372cf3..c6823b81eb84 100644 --- a/packages/android_alarm_manager/LICENSE +++ b/packages/android_alarm_manager/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/android_alarm_manager/README.md b/packages/android_alarm_manager/README.md index e7c7f6ee2713..beefa985ef10 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"> - + @@ -66,61 +74,7 @@ will not run in the same isolate as the main application. Unlike threads, isolat memory and communication between isolates must be done via message passing (see more documentation on isolates [here](https://api.dart.dev/stable/2.0.0/dart-isolate/dart-isolate-library.html)). - -## Using other plugins in alarm callbacks - -If alarm callbacks will need access to other Flutter plugins, including the -alarm manager plugin itself, it may be necessary to inform the background service how -to initialize plugins depending on which Flutter Android embedding the application is -using. - -### Flutter Android Embedding V1 - -For the Flutter Android Embedding V1, the background service must be provided a -callback to register plugins with the background isolate. This is done by giving -the `AlarmService` a callback to call the application's `onCreate` method. See the example's -[Application overrides](https://github.com/flutter/plugins/blob/master/packages/android_alarm_manager/example/android/app/src/main/java/io/flutter/plugins/androidalarmmanagerexample/Application.java). - -In particular, its `Application` class is as follows: - -```java -public class Application extends FlutterApplication implements PluginRegistrantCallback { - @Override - public void onCreate() { - super.onCreate(); - AlarmService.setPluginRegistrant(this); - } - - @Override - public void registerWith(PluginRegistry registry) { - GeneratedPluginRegistrant.registerWith(registry); - } -} -``` - -Which must be reflected in the application's `AndroidManifest.xml`. E.g.: - -```xml - = 1.12) - -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 04d09f62d936..7712ed56fe6f 100644 --- a/packages/android_alarm_manager/android/build.gradle +++ b/packages/android_alarm_manager/android/build.gradle @@ -1,10 +1,11 @@ group 'io.flutter.plugins.androidalarmmanager' version '1.0-SNAPSHOT' +def args = ["-Xlint:deprecation","-Xlint:unchecked","-Werror"] buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -15,24 +16,43 @@ buildscript { rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } +project.getTasks().withType(JavaCompile){ + options.compilerArgs.addAll(args) +} + apply plugin: 'com.android.library' android { - compileSdkVersion 28 - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } + compileSdkVersion 29 defaultConfig { minSdkVersion 16 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' + baseline file("lint-baseline.xml") + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } } diff --git a/packages/android_alarm_manager/android/gradle.properties b/packages/android_alarm_manager/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/android_alarm_manager/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/packages/android_alarm_manager/android/lint-baseline.xml b/packages/android_alarm_manager/android/lint-baseline.xml new file mode 100644 index 000000000000..de588614fdb2 --- /dev/null +++ b/packages/android_alarm_manager/android/lint-baseline.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + 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 fb6e7f85b317..aa59b578b157 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. @@ -13,11 +13,9 @@ import android.util.Log; import androidx.core.app.AlarmManagerCompat; import androidx.core.app.JobIntentService; -import io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; @@ -51,8 +49,6 @@ public static void enqueueAlarmProcessing(Context context, Intent alarmContext) *
    *
  • The given {@code callbackHandle} must correspond to a registered Dart callback. If the * handle does not resolve to a Dart callback then this method does nothing. - *
  • A static {@link #pluginRegistrantCallback} must exist, otherwise a {@link - * PluginRegistrantException} will be thrown. *
*/ public static void startBackgroundIsolate(Context context, long callbackHandle) { @@ -76,9 +72,8 @@ public static void startBackgroundIsolate(Context context, long callbackHandle) synchronized (alarmQueue) { // Handle all the alarm events received before the Dart isolate was // initialized, then clear the queue. - Iterator i = alarmQueue.iterator(); - while (i.hasNext()) { - flutterBackgroundExecutor.executeDartCallbackInBackgroundIsolate(i.next(), null); + for (Intent intent : alarmQueue) { + flutterBackgroundExecutor.executeDartCallbackInBackgroundIsolate(intent, null); } alarmQueue.clear(); } @@ -92,20 +87,6 @@ public static void setCallbackDispatcher(Context context, long callbackHandle) { FlutterBackgroundExecutor.setCallbackDispatcher(context, callbackHandle); } - /** - * Sets the {@link PluginRegistrantCallback} used to register the plugins used by an application - * with the newly spawned background isolate. - * - *

This should be invoked in {@link Application.onCreate} with {@link - * GeneratedPluginRegistrant} in applications using the V1 embedding API in order to use other - * plugins in the background isolate. For applications using the V2 embedding API, it is not - * necessary to set a {@link PluginRegistrantCallback} as plugins are registered automatically. - */ - public static void setPluginRegistrant(PluginRegistrantCallback callback) { - // Indirectly set in FlutterBackgroundExecutor for backwards compatibility. - FlutterBackgroundExecutor.setPluginRegistrant(callback); - } - private static void scheduleAlarm( Context context, int requestCode, @@ -231,7 +212,7 @@ public static void cancel(Context context, int requestCode) { } private static String getPersistentAlarmKey(int requestCode) { - return "android_alarm_manager/persistent_alarm_" + Integer.toString(requestCode); + return "android_alarm_manager/persistent_alarm_" + requestCode; } private static void addPersistentAlarm( @@ -276,13 +257,14 @@ private static void addPersistentAlarm( } private static void clearPersistentAlarm(Context context, int requestCode) { + String request = String.valueOf(requestCode); SharedPreferences p = context.getSharedPreferences(SHARED_PREFERENCES_KEY, 0); synchronized (persistentAlarmsLock) { Set persistentAlarms = p.getStringSet(PERSISTENT_ALARMS_SET_KEY, null); - if ((persistentAlarms == null) || !persistentAlarms.contains(requestCode)) { + if ((persistentAlarms == null) || !persistentAlarms.contains(request)) { return; } - persistentAlarms.remove(requestCode); + persistentAlarms.remove(request); String key = getPersistentAlarmKey(requestCode); p.edit().remove(key).putStringSet(PERSISTENT_ALARMS_SET_KEY, persistentAlarms).apply(); @@ -301,14 +283,12 @@ public static void reschedulePersistentAlarms(Context context) { return; } - Iterator it = persistentAlarms.iterator(); - while (it.hasNext()) { - int requestCode = Integer.parseInt(it.next()); + for (String persistentAlarm : persistentAlarms) { + int requestCode = Integer.parseInt(persistentAlarm); String key = getPersistentAlarmKey(requestCode); String json = p.getString(key, null); if (json == null) { - Log.e( - TAG, "Data for alarm request code " + Integer.toString(requestCode) + " is invalid."); + Log.e(TAG, "Data for alarm request code " + requestCode + " is invalid."); continue; } try { 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 2f3f5f9f2925..45f047b5ae68 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. @@ -6,6 +6,7 @@ import android.content.Context; import android.util.Log; +import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.JSONMethodCodec; @@ -13,8 +14,6 @@ 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.Registrar; -import io.flutter.view.FlutterNativeView; import org.json.JSONArray; import org.json.JSONException; @@ -28,8 +27,8 @@ *

  • The Dart side of this plugin sends the Android side a "AlarmService.start" message, along * with a Dart callback handle for a Dart callback that should be immediately invoked by a * background Dart isolate. - *
  • The Android side of this plugin spins up a background {@link FlutterNativeView}, which - * includes a background Dart isolate. + *
  • The Android side of this plugin spins up a background {@link FlutterEngine}, which includes + * a background Dart isolate. *
  • The Android side of this plugin instructs the new background Dart isolate to execute the * callback that was received in the "AlarmService.start" message. *
  • The Dart side of this plugin, running within the new background isolate, executes the @@ -49,12 +48,13 @@ public class AndroidAlarmManagerPlugin implements FlutterPlugin, MethodCallHandl /** * Registers this plugin with an associated Flutter execution context, represented by the given - * {@link Registrar}. + * {@link io.flutter.plugin.common.PluginRegistry.Registrar}. * *

    Once this method is executed, an instance of {@code AndroidAlarmManagerPlugin} will be * connected to, and running against, the associated Flutter execution context. */ - public static void registerWith(Registrar registrar) { + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { if (instance == null) { instance = new AndroidAlarmManagerPlugin(); } @@ -107,42 +107,46 @@ public void onMethodCall(MethodCall call, Result result) { String method = call.method; Object arguments = call.arguments; try { - if (method.equals("AlarmService.start")) { - // This message is sent when the Dart side of this plugin is told to initialize. - long callbackHandle = ((JSONArray) arguments).getLong(0); - // In response, this (native) side of the plugin needs to spin up a background - // Dart isolate by using the given callbackHandle, and then setup a background - // method channel to communicate with the new background isolate. Once completed, - // this onMethodCall() method will receive messages from both the primary and background - // method channels. - AlarmService.setCallbackDispatcher(context, callbackHandle); - AlarmService.startBackgroundIsolate(context, callbackHandle); - result.success(true); - } else if (method.equals("Alarm.periodic")) { - // This message indicates that the Flutter app would like to schedule a periodic - // task. - PeriodicRequest periodicRequest = PeriodicRequest.fromJson((JSONArray) arguments); - AlarmService.setPeriodic(context, periodicRequest); - result.success(true); - } else if (method.equals("Alarm.oneShotAt")) { - // This message indicates that the Flutter app would like to schedule a one-time - // task. - OneShotRequest oneShotRequest = OneShotRequest.fromJson((JSONArray) arguments); - AlarmService.setOneShot(context, oneShotRequest); - result.success(true); - } else if (method.equals("Alarm.cancel")) { - // This message indicates that the Flutter app would like to cancel a previously - // scheduled task. - int requestCode = ((JSONArray) arguments).getInt(0); - AlarmService.cancel(context, requestCode); - result.success(true); - } else { - result.notImplemented(); + switch (method) { + case "AlarmService.start": + // This message is sent when the Dart side of this plugin is told to initialize. + long callbackHandle = ((JSONArray) arguments).getLong(0); + // In response, this (native) side of the plugin needs to spin up a background + // Dart isolate by using the given callbackHandle, and then setup a background + // method channel to communicate with the new background isolate. Once completed, + // this onMethodCall() method will receive messages from both the primary and background + // method channels. + AlarmService.setCallbackDispatcher(context, callbackHandle); + AlarmService.startBackgroundIsolate(context, callbackHandle); + result.success(true); + break; + case "Alarm.periodic": + // This message indicates that the Flutter app would like to schedule a periodic + // task. + PeriodicRequest periodicRequest = PeriodicRequest.fromJson((JSONArray) arguments); + AlarmService.setPeriodic(context, periodicRequest); + result.success(true); + break; + case "Alarm.oneShotAt": + // This message indicates that the Flutter app would like to schedule a one-time + // task. + OneShotRequest oneShotRequest = OneShotRequest.fromJson((JSONArray) arguments); + AlarmService.setOneShot(context, oneShotRequest); + result.success(true); + break; + case "Alarm.cancel": + // This message indicates that the Flutter app would like to cancel a previously + // scheduled task. + int requestCode = ((JSONArray) arguments).getInt(0); + AlarmService.cancel(context, requestCode); + result.success(true); + break; + default: + result.notImplemented(); + break; } } catch (JSONException e) { result.error("error", "JSON error: " + e.getMessage(), null); - } catch (PluginRegistrantException e) { - result.error("error", "AlarmManager error: " + e.getMessage(), null); } } 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 4e755c315528..0aa08ed216e0 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. @@ -19,9 +19,7 @@ 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.PluginRegistrantCallback; import io.flutter.view.FlutterCallbackInformation; -import io.flutter.view.FlutterMain; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; @@ -32,7 +30,10 @@ public class FlutterBackgroundExecutor implements MethodCallHandler { private static final String TAG = "FlutterBackgroundExecutor"; private static final String CALLBACK_HANDLE_KEY = "callback_handle"; - private static PluginRegistrantCallback pluginRegistrantCallback; + + @SuppressWarnings("deprecation") + private static io.flutter.plugin.common.PluginRegistry.PluginRegistrantCallback + pluginRegistrantCallback; /** * The {@link MethodChannel} that connects the Android side of this plugin with the background @@ -44,18 +45,6 @@ public class FlutterBackgroundExecutor implements MethodCallHandler { private AtomicBoolean isCallbackDispatcherReady = new AtomicBoolean(false); - /** - * Sets the {@code PluginRegistrantCallback} used to register plugins with the newly spawned - * isolate. - * - *

    Note: this is only necessary for applications using the V1 engine embedding API as plugins - * are automatically registered via reflection in the V2 engine embedding API. If not set, alarm - * callbacks will not be able to utilize functionality from other plugins. - */ - public static void setPluginRegistrant(PluginRegistrantCallback callback) { - pluginRegistrantCallback = callback; - } - /** * Sets the Dart callback handle for the Dart method that is responsible for initializing the * background Dart isolate, preparing it to receive Dart callback tasks requests. @@ -78,20 +67,15 @@ private void onInitialized() { @Override public void onMethodCall(MethodCall call, Result result) { String method = call.method; - Object arguments = call.arguments; - try { - if (method.equals("AlarmService.initialized")) { - // This message is sent by the background method channel as soon as the background isolate - // is running. From this point forward, the Android side of this plugin can send - // callback handles through the background method channel, and the Dart side will execute - // the Dart methods corresponding to those callback handles. - onInitialized(); - result.success(true); - } else { - result.notImplemented(); - } - } catch (PluginRegistrantException e) { - result.error("error", "AlarmManager error: " + e.getMessage(), null); + if (method.equals("AlarmService.initialized")) { + // This message is sent by the background method channel as soon as the background isolate + // is running. From this point forward, the Android side of this plugin can send + // callback handles through the background method channel, and the Dart side will execute + // the Dart methods corresponding to those callback handles. + onInitialized(); + result.success(true); + } else { + result.notImplemented(); } } @@ -102,7 +86,7 @@ public void onMethodCall(MethodCall call, Result result) { *

    The isolate is configured as follows: * *

      - *
    • Bundle Path: {@code FlutterMain.findAppBundlePath(context)}. + *
    • Bundle Path: {@code io.flutter.view.FlutterMain.findAppBundlePath(context)}. *
    • Entrypoint: The Dart method used the last time this plugin was initialized in the * foreground. *
    • Run args: none. @@ -113,8 +97,6 @@ public void onMethodCall(MethodCall call, Result result) { *
        *
      • The given callback must correspond to a registered Dart callback. If the handle does not * resolve to a Dart callback then this method does nothing. - *
      • A static {@link #pluginRegistrantCallback} must exist, otherwise a {@link - * PluginRegistrantException} will be thrown. *
      */ public void startBackgroundIsolate(Context context) { @@ -131,7 +113,7 @@ public void startBackgroundIsolate(Context context) { *

      The isolate is configured as follows: * *

        - *
      • Bundle Path: {@code FlutterMain.findAppBundlePath(context)}. + *
      • Bundle Path: {@code io.flutter.view.FlutterMain.findAppBundlePath(context)}. *
      • Entrypoint: The Dart method represented by {@code callbackHandle}. *
      • Run args: none. *
      @@ -141,8 +123,6 @@ public void startBackgroundIsolate(Context context) { *
        *
      • The given {@code callbackHandle} must correspond to a registered Dart callback. If the * handle does not resolve to a Dart callback then this method does nothing. - *
      • A static {@link #pluginRegistrantCallback} must exist, otherwise a {@link - * PluginRegistrantException} will be thrown. *
      */ public void startBackgroundIsolate(Context context, long callbackHandle) { @@ -152,9 +132,10 @@ public void startBackgroundIsolate(Context context, long callbackHandle) { } Log.i(TAG, "Starting AlarmService..."); - String appBundlePath = FlutterMain.findAppBundlePath(context); + @SuppressWarnings("deprecation") + String appBundlePath = io.flutter.view.FlutterMain.findAppBundlePath(); AssetManager assets = context.getAssets(); - if (appBundlePath != null && !isRunning()) { + if (!isRunning()) { backgroundFlutterEngine = new FlutterEngine(context); // We need to create an instance of `FlutterEngine` before looking up the @@ -162,10 +143,6 @@ public void startBackgroundIsolate(Context context, long callbackHandle) { // lookup will fail. FlutterCallbackInformation flutterCallback = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle); - if (flutterCallback == null) { - Log.e(TAG, "Fatal: failed to find callback"); - return; - } DartExecutor executor = backgroundFlutterEngine.getDartExecutor(); initializeMethodChannel(executor); 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 deleted file mode 100644 index debcd7ee7529..000000000000 --- a/packages/android_alarm_manager/android/src/main/java/io/flutter/plugins/androidalarmmanager/PluginRegistrantException.java +++ /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. - -package io.flutter.plugins.androidalarmmanager; - -class PluginRegistrantException extends RuntimeException { - public PluginRegistrantException() { - super( - "PluginRegistrantCallback is not set. Did you forget to call " - + "AlarmService.setPluginRegistrant? See the README for instructions."); - } -} 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/DartIntegrationTest.java b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/android_alarm_manager/example/android/app/src/androidTest/java/io/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/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..a841a239d3af 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. @@ -17,6 +17,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.rule.ActivityTestRule; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,6 +40,7 @@ public void setUp() throws Exception { ActivityScenario.launch(DriverExtensionActivity.class); } + @Ignore("Disabled due to flake: https://github.com/flutter/flutter/issues/88837") @Test public void startBackgroundIsolate() throws Exception { 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 04aef18888a9..a5bb72415f14 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,15 +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. package io.flutter.plugins.androidalarmmanagerexample; import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterTestRunner; +import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; +@DartIntegrationTest @RunWith(FlutterTestRunner.class) public class MainActivityTest { @Rule 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..2fef38483800 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 @@ -6,18 +6,8 @@ - - - + get _localPath async { + final Directory directory = await getTemporaryDirectory(); + return directory.path; +} + +Future get _localFile async { + final String path = await _localPath; + return File('$path/counter.txt'); +} + +Future writeCounter(int counter) async { + final File file = await _localFile; + + // Write the file. + return file.writeAsString('$counter'); +} + +Future readCounter() async { + try { + final File file = await _localFile; + + // Read the file. + final String contents = await file.readAsString(); + + return int.parse(contents); + // ignore: unused_catch_clause + } on FileSystemException catch (e) { + // If encountering an error, return 0. + return 0; + } +} + +Future incrementCounter() async { + final int value = await readCounter(); + await writeCounter(value + 1); +} + +void appMain() { + enableFlutterDriverExtension(); + app.main(); +} + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + setUp(() async { + await AndroidAlarmManager.initialize(); + }); + + group('oneshot', () { + testWidgets('cancelled before it fires', (WidgetTester tester) async { + final int alarmId = 0; + final int startingValue = await readCounter(); + await AndroidAlarmManager.oneShot( + const Duration(seconds: 1), alarmId, incrementCounter); + expect(await AndroidAlarmManager.cancel(alarmId), isTrue); + await Future.delayed(const Duration(seconds: 4)); + expect(await readCounter(), startingValue); + }); + + testWidgets('cancelled after it fires', (WidgetTester tester) async { + final int alarmId = 1; + final int startingValue = await readCounter(); + await AndroidAlarmManager.oneShot( + const Duration(seconds: 1), alarmId, incrementCounter, + exact: true, wakeup: true); + await Future.delayed(const Duration(seconds: 2)); + // poll until file is updated + while (await readCounter() == startingValue) { + await Future.delayed(const Duration(seconds: 1)); + } + expect(await readCounter(), startingValue + 1); + expect(await AndroidAlarmManager.cancel(alarmId), isTrue); + }); + }); + + testWidgets('periodic', (WidgetTester tester) async { + final int alarmId = 2; + final int startingValue = await readCounter(); + await AndroidAlarmManager.periodic( + const Duration(seconds: 1), alarmId, incrementCounter, + wakeup: true, exact: true); + // poll until file is updated + while (await readCounter() < startingValue + 2) { + await Future.delayed(const Duration(seconds: 1)); + } + expect(await readCounter(), startingValue + 2); + expect(await AndroidAlarmManager.cancel(alarmId), isTrue); + await Future.delayed(const Duration(seconds: 3)); + expect(await readCounter(), startingValue + 2); + }); +} diff --git a/packages/android_alarm_manager/example/ios/Runner.xcodeproj/project.pbxproj b/packages/android_alarm_manager/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 10f77b41d130..000000000000 --- a/packages/android_alarm_manager/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,490 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; }; - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 3B80C3931E831B6300D905FE /* App.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; }; - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9740EEBA1CF902C7004384FC /* Flutter.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - C952AD53387AE85A4AAC19D3 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 365DE79D3A08F3F6322AB7B4 /* libPods-Runner.a */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 192BD17BD81C291EF9467E75 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 365DE79D3A08F3F6322AB7B4 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 842A7CA20B55950D87F2A01A /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - C952AD53387AE85A4AAC19D3 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 1B44A04DB1D7DBDE7E239095 /* Pods */ = { - isa = PBXGroup; - children = ( - 192BD17BD81C291EF9467E75 /* Pods-Runner.debug.xcconfig */, - 842A7CA20B55950D87F2A01A /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B80C3931E831B6300D905FE /* App.framework */, - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 1B44A04DB1D7DBDE7E239095 /* Pods */, - B10ADDD1244B5A67F70F5F08 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - B10ADDD1244B5A67F70F5F08 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 365DE79D3A08F3F6322AB7B4 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 9AC722C5D70651C49D7ECF80 /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - E95CF7E4BD7CAFC3E0F4E1E2 /* [CP] Embed Pods Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = Runner; - productName = Runner; - productReference = 97C146EE1CF9000F007C117D /* Runner.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 97C146E61CF9000F007C117D /* Project object */ = { - isa = PBXProject; - attributes = { - LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; - TargetAttributes = { - 97C146ED1CF9000F007C117D = { - CreatedOnToolsVersion = 7.3.1; - }; - }; - }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 97C146E51CF9000F007C117D; - productRefGroup = 97C146EF1CF9000F007C117D /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 97C146ED1CF9000F007C117D /* Runner */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 97C146EC1CF9000F007C117D /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Thin Binary"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; - 9AC722C5D70651C49D7ECF80 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - E95CF7E4BD7CAFC3E0F4E1E2 /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "[CP] Embed Pods Frameworks"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_COMMA = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_STRICT_PROTOTYPES = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CLANG_WARN_UNREACHABLE_CODE = YES; - CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - COPY_PHASE_STRIP = NO; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.androidAlarmManagerExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.androidAlarmManagerExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/android_alarm_manager/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/android_alarm_manager/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a16ed0f..000000000000 --- a/packages/android_alarm_manager/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/packages/android_alarm_manager/example/ios/Runner/AppDelegate.h b/packages/android_alarm_manager/example/ios/Runner/AppDelegate.h deleted file mode 100644 index 36e21bbf9cf4..000000000000 --- a/packages/android_alarm_manager/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,6 +0,0 @@ -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/android_alarm_manager/example/ios/Runner/AppDelegate.m b/packages/android_alarm_manager/example/ios/Runner/AppDelegate.m deleted file mode 100644 index 59a72e90be12..000000000000 --- a/packages/android_alarm_manager/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,13 +0,0 @@ -#include "AppDelegate.h" -#include "GeneratedPluginRegistrant.h" - -@implementation AppDelegate - -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - [GeneratedPluginRegistrant registerWithRegistry:self]; - // Override point for customization after application launch. - return [super application:application didFinishLaunchingWithOptions:launchOptions]; -} - -@end diff --git a/packages/android_alarm_manager/example/ios/Runner/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 2fc191881c10..821440c49659 100644 --- a/packages/android_alarm_manager/example/pubspec.yaml +++ b/packages/android_alarm_manager/example/pubspec.yaml @@ -1,14 +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 - e2e: 0.3.0 - path_provider: ^1.3.1 + shared_preferences: ^2.0.0 + integration_test: + sdk: flutter + path_provider: ^2.0.0 dev_dependencies: espresso: ^0.0.1+3 @@ -16,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/android_alarm_manager_e2e.dart b/packages/android_alarm_manager/example/test_driver/android_alarm_manager_e2e.dart deleted file mode 100644 index a5bc1ac0ba48..000000000000 --- a/packages/android_alarm_manager/example/test_driver/android_alarm_manager_e2e.dart +++ /dev/null @@ -1,106 +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:io'; -import 'package:android_alarm_manager_example/main.dart' as app; -import 'package:android_alarm_manager/android_alarm_manager.dart'; -import 'package:e2e/e2e.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_driver/driver_extension.dart'; -import 'package:path_provider/path_provider.dart'; - -// From https://flutter.dev/docs/cookbook/persistence/reading-writing-files -Future get _localPath async { - final Directory directory = await getTemporaryDirectory(); - return directory.path; -} - -Future get _localFile async { - final String path = await _localPath; - return File('$path/counter.txt'); -} - -Future writeCounter(int counter) async { - final File file = await _localFile; - - // Write the file. - return file.writeAsString('$counter'); -} - -Future readCounter() async { - try { - final File file = await _localFile; - - // Read the file. - final String contents = await file.readAsString(); - - return int.parse(contents); - // ignore: unused_catch_clause - } on FileSystemException catch (e) { - // If encountering an error, return 0. - return 0; - } -} - -Future incrementCounter() async { - final int value = await readCounter(); - await writeCounter(value + 1); -} - -void appMain() { - enableFlutterDriverExtension(); - app.main(); -} - -void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); - - setUp(() async { - await AndroidAlarmManager.initialize(); - }); - - group('oneshot', () { - testWidgets('cancelled before it fires', (WidgetTester tester) async { - final int alarmId = 0; - final int startingValue = await readCounter(); - await AndroidAlarmManager.oneShot( - const Duration(seconds: 1), alarmId, incrementCounter); - expect(await AndroidAlarmManager.cancel(alarmId), isTrue); - await Future.delayed(const Duration(seconds: 4)); - expect(await readCounter(), startingValue); - }); - - testWidgets('cancelled after it fires', (WidgetTester tester) async { - final int alarmId = 1; - final int startingValue = await readCounter(); - await AndroidAlarmManager.oneShot( - const Duration(seconds: 1), alarmId, incrementCounter, - exact: true, wakeup: true); - await Future.delayed(const Duration(seconds: 2)); - // poll until file is updated - while (await readCounter() == startingValue) { - await Future.delayed(const Duration(seconds: 1)); - } - expect(await readCounter(), startingValue + 1); - expect(await AndroidAlarmManager.cancel(alarmId), isTrue); - }); - }); - - testWidgets('periodic', (WidgetTester tester) async { - final int alarmId = 2; - final int startingValue = await readCounter(); - await AndroidAlarmManager.periodic( - const Duration(seconds: 1), alarmId, incrementCounter, - wakeup: true, exact: true); - // poll until file is updated - while (await readCounter() < startingValue + 2) { - await Future.delayed(const Duration(seconds: 1)); - } - expect(await readCounter(), startingValue + 2); - expect(await AndroidAlarmManager.cancel(alarmId), isTrue); - await Future.delayed(const Duration(seconds: 3)); - expect(await readCounter(), startingValue + 2); - }); -} diff --git a/packages/android_alarm_manager/example/test_driver/android_alarm_manager_e2e_test.dart b/packages/android_alarm_manager/example/test_driver/android_alarm_manager_e2e_test.dart deleted file mode 100644 index eea5e8abc15f..000000000000 --- a/packages/android_alarm_manager/example/test_driver/android_alarm_manager_e2e_test.dart +++ /dev/null @@ -1,41 +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:io'; - -import 'package:flutter_driver/flutter_driver.dart'; -import 'package:vm_service_client/vm_service_client.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 result = - await driver.requestData(null, timeout: const Duration(minutes: 5)); - await driver.close(); - await subscription.cancel(); - exit(result == 'pass' ? 0 : 1); -} diff --git a/packages/android_alarm_manager/example/test_driver/integration_test.dart b/packages/android_alarm_manager/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..6a0e6fa82dbe --- /dev/null +++ b/packages/android_alarm_manager/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/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 8e46b8c202eb..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+11 -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 4d63b0cbceab..79eafe70e821 100644 --- a/packages/android_intent/CHANGELOG.md +++ b/packages/android_intent/CHANGELOG.md @@ -1,3 +1,47 @@ +## NEXT + +* Remove references to the V1 Android embedding. +* Updated Android lint settings. +* Specify Java 8 for Android build. + +## 2.0.2 + +* Update README to point to Plus Plugins version. + +## 2.0.1 + +* Migrate maven repository from jcenter to mavenCentral. + +## 2.0.0 + +* Migrate to null safety. +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 0.3.7+8 + +* Update Flutter SDK constraint. + +## 0.3.7+7 + +* Update Dart SDK constraint in example. + +## 0.3.7+6 + +* Update android compileSdkVersion to 29. + +## 0.3.7+5 + +* Android Code Inspection and Clean up. + +## 0.3.7+4 + +* Keep handling deprecated Android v1 classes for backward compatibility. + +## 0.3.7+3 + +* Update the `platform` package dependency to resolve the conflict with the latest flutter. + ## 0.3.7+2 * Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). @@ -8,8 +52,8 @@ ## 0.3.7 -* Add a `Future canResolveActivity` method to the AndroidIntent class. It - can be used to determine whether a device supports a particular intent or has +* Add a `Future canResolveActivity` method to the AndroidIntent class. It + can be used to determine whether a device supports a particular intent or has an app installed that can resolve it. It is based on PackageManager [resolveActivity](https://developer.android.com/reference/android/content/pm/PackageManager#resolveActivity(android.content.Intent,%20int)). @@ -32,7 +76,7 @@ ## 0.3.5 -* Add support for [setType](https://developer.android.com/reference/android/content/Intent.html#setType(java.lang.String)) and [setDataAndType](https://developer.android.com/reference/android/content/Intent.html#setDataAndType(android.net.Uri,%20java.lang.String)) parameters. +* Add support for [setType](https://developer.android.com/reference/android/content/Intent.html#setType(java.lang.String)) and [setDataAndType](https://developer.android.com/reference/android/content/Intent.html#setDataAndType(android.net.Uri,%20java.lang.String)) parameters. ## 0.3.4+8 diff --git a/packages/android_intent/LICENSE b/packages/android_intent/LICENSE index c89293372cf3..c6823b81eb84 100644 --- a/packages/android_intent/LICENSE +++ b/packages/android_intent/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/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 d261dac7df1c..f0af1602dbb1 100644 --- a/packages/android_intent/android/build.gradle +++ b/packages/android_intent/android/build.gradle @@ -1,10 +1,11 @@ group 'io.flutter.plugins.androidintent' version '1.0-SNAPSHOT' +def args = ["-Xlint:deprecation","-Xlint:unchecked","-Werror"] buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { @@ -15,14 +16,18 @@ buildscript { 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 @@ -30,9 +35,22 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } testOptions { unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } } } diff --git a/packages/android_intent/android/gradle.properties b/packages/android_intent/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/android_intent/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M 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 2f35dfcf0372..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,10 +1,13 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.androidintent; import androidx.annotation.NonNull; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import io.flutter.plugin.common.PluginRegistry.Registrar; /** * Plugin implementation that uses the new {@code io.flutter.embedding} package. @@ -32,7 +35,8 @@ public AndroidIntentPlugin() { *

      Calling this automatically initializes the plugin. However plugins initialized this way * won't react to changes in activity or context, unlike {@link AndroidIntentPlugin}. */ - public static void registerWith(Registrar registrar) { + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { IntentSender sender = new IntentSender(registrar.activity(), registrar.context()); MethodCallHandlerImpl impl = new MethodCallHandlerImpl(sender); impl.startListening(registrar.messenger()); 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 a47f7a162b78..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; @@ -15,6 +19,7 @@ import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; import java.util.ArrayList; +import java.util.HashMap; import java.util.Map; /** Forwards incoming {@link MethodCall}s to {@link IntentSender#send}. */ @@ -75,14 +80,19 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { String action = convertAction((String) call.argument("action")); Integer flags = call.argument("flags"); String category = call.argument("category"); - Uri data = call.argument("data") != null ? Uri.parse((String) call.argument("data")) : null; - Bundle arguments = convertArguments((Map) call.argument("arguments")); + String stringData = call.argument("data"); + Uri data = call.argument("data") != null ? Uri.parse(stringData) : null; + Map stringMap = call.argument("arguments"); + Bundle arguments = convertArguments(stringMap); String packageName = call.argument("package"); - ComponentName componentName = - (!TextUtils.isEmpty(packageName) - && !TextUtils.isEmpty((String) call.argument("componentName"))) - ? new ComponentName(packageName, (String) call.argument("componentName")) - : null; + String component = call.argument("componentName"); + ComponentName componentName = null; + if (packageName != null + && component != null + && !TextUtils.isEmpty(packageName) + && !TextUtils.isEmpty(component)) { + componentName = new ComponentName(packageName, component); + } String type = call.argument("type"); Intent intent = @@ -128,6 +138,9 @@ private static Bundle convertArguments(Map arguments) { } for (String key : arguments.keySet()) { Object value = arguments.get(key); + ArrayList stringArrayList = isStringArrayList(value); + ArrayList integerArrayList = isIntegerArrayList(value); + Map stringMap = isStringKeyedMap(value); if (value instanceof Integer) { bundle.putInt(key, (Integer) value); } else if (value instanceof String) { @@ -146,12 +159,12 @@ private static Bundle convertArguments(Map arguments) { bundle.putLongArray(key, (long[]) value); } else if (value instanceof double[]) { bundle.putDoubleArray(key, (double[]) value); - } else if (isTypedArrayList(value, Integer.class)) { - bundle.putIntegerArrayList(key, (ArrayList) value); - } else if (isTypedArrayList(value, String.class)) { - bundle.putStringArrayList(key, (ArrayList) value); - } else if (isStringKeyedMap(value)) { - bundle.putBundle(key, convertArguments((Map) value)); + } else if (integerArrayList != null) { + bundle.putIntegerArrayList(key, integerArrayList); + } else if (stringArrayList != null) { + bundle.putStringArrayList(key, stringArrayList); + } else if (stringMap != null) { + bundle.putBundle(key, convertArguments(stringMap)); } else { throw new UnsupportedOperationException("Unsupported type " + value); } @@ -159,29 +172,54 @@ private static Bundle convertArguments(Map arguments) { return bundle; } - private static boolean isTypedArrayList(Object value, Class type) { + private static ArrayList isIntegerArrayList(Object value) { + ArrayList integerArrayList = new ArrayList<>(); + if (!(value instanceof ArrayList)) { + return null; + } + ArrayList intList = (ArrayList) value; + for (Object o : intList) { + if (!(o instanceof Integer)) { + return null; + } else { + integerArrayList.add((Integer) o); + } + } + return integerArrayList; + } + + private static ArrayList isStringArrayList(Object value) { + ArrayList stringArrayList = new ArrayList<>(); if (!(value instanceof ArrayList)) { - return false; + return null; } - ArrayList list = (ArrayList) value; - for (Object o : list) { - if (!(o == null || type.isInstance(o))) { - return false; + ArrayList stringList = (ArrayList) value; + for (Object o : stringList) { + if (!(o instanceof String)) { + return null; + } else { + stringArrayList.add((String) o); } } - return true; + return stringArrayList; } - private static boolean isStringKeyedMap(Object value) { + private static Map isStringKeyedMap(Object value) { + Map stringMap = new HashMap<>(); if (!(value instanceof Map)) { - return false; + return null; } - Map map = (Map) value; - for (Object key : map.keySet()) { - if (!(key == null || key instanceof String)) { - return false; + Map mapValue = (Map) value; + for (Object key : mapValue.keySet()) { + if (!(key instanceof String)) { + return null; + } else { + Object o = mapValue.get(key); + if (o != null) { + stringMap.put((String) key, o); + } } } - return true; + return stringMap; } } diff --git a/packages/android_intent/android/src/test/java/io/flutter/plugins/androidintent/MethodCallHandlerImplTest.java b/packages/android_intent/android/src/test/java/io/flutter/plugins/androidintent/MethodCallHandlerImplTest.java 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/DartIntegrationTest.java b/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/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 deleted file mode 100644 index db000f0f995c..000000000000 --- a/packages/android_intent/example/android/app/src/androidTestDebug/java/io/flutter/plugins/androidintentexample/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.flutter.plugins.androidintentexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} 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 d390dcd74997..358fc78bfcfd 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,11 +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.androidintentexample; import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; -@RunWith(FlutterRunner.class) +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) public class MainActivityTest { @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); } diff --git a/packages/android_intent/example/android/app/src/main/AndroidManifest.xml b/packages/android_intent/example/android/app/src/main/AndroidManifest.xml index 761c35fd64d8..e0aa7f84d7b9 100644 --- a/packages/android_intent/example/android/app/src/main/AndroidManifest.xml +++ b/packages/android_intent/example/android/app/src/main/AndroidManifest.xml @@ -1,23 +1,8 @@ - - - - - + android:label="android_intent_example"> + widget is Text && widget.data.startsWith('Tap here'), + ), + findsNWidgets(2), + ); + } else { + expect( + find.byWidgetPredicate( + (Widget widget) => + widget is Text && + widget.data.startsWith('This plugin only works with Android'), + ), + findsOneWidget, + ); + } + }); + + testWidgets('#launch throws when no Activity is found', + (WidgetTester tester) async { + // We can't test that any of this is really working, this is mostly just + // checking that the plugin API is registered. Only works on Android. + const AndroidIntent intent = + AndroidIntent(action: 'LAUNCH', package: 'foobar'); + await expectLater(() async => await intent.launch(), throwsA((Exception e) { + return e is PlatformException && + e.message.contains('No Activity found to handle Intent'); + })); + }, skip: !Platform.isAndroid); + + testWidgets('#canResolveActivity returns true when example Activity is found', + (WidgetTester tester) async { + AndroidIntent intent = AndroidIntent( + action: 'action_view', + package: 'io.flutter.plugins.androidintentexample', + componentName: 'io.flutter.embedding.android.FlutterActivity', + ); + await expectLater(() async => await intent.canResolveActivity(), isFalse); + }, skip: !Platform.isAndroid); + + testWidgets('#canResolveActivity returns false when no Activity is found', + (WidgetTester tester) async { + const AndroidIntent intent = + AndroidIntent(action: 'LAUNCH', package: 'foobar'); + await expectLater(() async => await intent.canResolveActivity(), isFalse); + }, skip: !Platform.isAndroid); +} diff --git a/packages/android_intent/example/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/project.xcworkspace/contents.xcworkspacedata b/packages/android_intent/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a16ed0f..000000000000 --- a/packages/android_intent/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +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/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 22ff833f8198..42e59930ce64 100644 --- a/packages/android_intent/example/pubspec.yaml +++ b/packages/android_intent/example/pubspec.yaml @@ -1,17 +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: - e2e: "^0.2.1" + 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/android_intent_e2e.dart b/packages/android_intent/example/test_driver/android_intent_e2e.dart deleted file mode 100644 index 880e7efee76c..000000000000 --- a/packages/android_intent/example/test_driver/android_intent_e2e.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'dart:io'; - -import 'package:android_intent/android_intent.dart'; -import 'package:android_intent_example/main.dart'; -import 'package:e2e/e2e.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_test/flutter_test.dart'; - -/// This is a smoke test that verifies that the example app builds and loads. -/// Because this plugin works by launching Android platform UIs it's not -/// possible to meaningfully test it through its Dart interface currently. There -/// are more useful unit tests for the platform logic under android/src/test/. -void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); - testWidgets('Embedding example app loads', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(MyApp()); - - // Verify that the new embedding example app builds - if (Platform.isAndroid) { - expect( - find.byWidgetPredicate( - (Widget widget) => - widget is Text && widget.data.startsWith('Tap here'), - ), - findsNWidgets(2), - ); - } else { - expect( - find.byWidgetPredicate( - (Widget widget) => - widget is Text && - widget.data.startsWith('This plugin only works with Android'), - ), - findsOneWidget, - ); - } - }); - - testWidgets('#launch throws when no Activity is found', - (WidgetTester tester) async { - // We can't test that any of this is really working, this is mostly just - // checking that the plugin API is registered. Only works on Android. - const AndroidIntent intent = - AndroidIntent(action: 'LAUNCH', package: 'foobar'); - await expectLater(() async => await intent.launch(), throwsA((Exception e) { - return e is PlatformException && - e.message.contains('No Activity found to handle Intent'); - })); - }, skip: !Platform.isAndroid); - - testWidgets('#canResolveActivity returns true when example Activity is found', - (WidgetTester tester) async { - AndroidIntent intent = AndroidIntent( - action: 'action_view', - package: 'io.flutter.plugins.androidintentexample', - componentName: 'io.flutter.embedding.android.FlutterActivity', - ); - await expectLater(() async => await intent.canResolveActivity(), isFalse); - }, skip: !Platform.isAndroid); - - testWidgets('#canResolveActivity returns false when no Activity is found', - (WidgetTester tester) async { - const AndroidIntent intent = - AndroidIntent(action: 'LAUNCH', package: 'foobar'); - await expectLater(() async => await intent.canResolveActivity(), isFalse); - }, skip: !Platform.isAndroid); -} diff --git a/packages/android_intent/example/test_driver/android_intent_e2e_test.dart b/packages/android_intent/example/test_driver/android_intent_e2e_test.dart deleted file mode 100644 index 6147d44df2ec..000000000000 --- a/packages/android_intent/example/test_driver/android_intent_e2e_test.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:flutter_driver/flutter_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); -} diff --git a/packages/android_intent/example/test_driver/integration_test.dart b/packages/android_intent/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..6a0e6fa82dbe --- /dev/null +++ b/packages/android_intent/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/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 2e786277d869..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+2 +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 - 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/CHANGELOG.md b/packages/battery/CHANGELOG.md deleted file mode 100644 index 2229fb13f291..000000000000 --- a/packages/battery/CHANGELOG.md +++ /dev/null @@ -1,132 +0,0 @@ -## 1.0.1 - -* Update lower bound of dart dependency to 2.1.0. - -## 1.0.0 - -* Bump the package version to 1.0.0 following ecosystem pre-migration (https://github.com/amirh/bump_to_1.0/projects/1). - -## 0.3.1+10 - -* Update minimum Flutter version to 1.12.13+hotfix.5 -* Fix CocoaPods podspec lint warnings. - -## 0.3.1+9 - -* Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). - -## 0.3.1+8 - -* Make the pedantic dev_dependency explicit. - -## 0.3.1+7 - -* Clean up various Android workarounds no longer needed after framework v1.12. - -## 0.3.1+6 - -* Remove the deprecated `author:` field from pubspec.yaml -* Migrate the plugin to the pubspec platforms manifest. -* Require Flutter SDK 1.10.0 or greater. - -## 0.3.1+5 - -* Fix pedantic linter errors. - -## 0.3.1+4 - -* Update and migrate iOS example project. - -## 0.3.1+3 - -* Remove AndroidX warning. - -## 0.3.1+2 - -* Include lifecycle dependency as a compileOnly one on Android to resolve - potential version conflicts with other transitive libraries. - -## 0.3.1+1 - -* Android: Use android.arch.lifecycle instead of androidx.lifecycle:lifecycle in `build.gradle` to support apps that has not been migrated to AndroidX. - -## 0.3.1 - -* Support the v2 Android embedder. - -## 0.3.0+6 - -* Define clang module for iOS. - -## 0.3.0+5 - -* Fix Gradle version. - -## 0.3.0+4 - -* Update Dart code to conform to current Dart formatter. - -## 0.3.0+3 - -* Fix `batteryLevel` usage example in README - -## 0.3.0+2 - -* Bump the minimum Flutter version to 1.2.0. -* Add template type parameter to `invokeMethod` calls. - -## 0.3.0+1 - -* Log a more detailed warning at build time about the previous AndroidX - migration. - -## 0.3.0 - -* **Breaking change**. Migrate from the deprecated original Android Support - Library to AndroidX. This shouldn't result in any functional changes, but it - requires any Android apps using this plugin to [also - migrate](https://developer.android.com/jetpack/androidx/migrate) if they're - using the original support library. - -## 0.2.3 - -* Updated mockito dependency to 3.0.0 to get Dart 2 support. -* Update test package dependency to 1.3.0, and fixed tests to match. - -## 0.2.2 - -* Updated Gradle tooling to match Android Studio 3.1.2. - -## 0.2.1 - -* Fixed Dart 2 type error. -* Removed use of deprecated parameter in example. - -## 0.2.0 - -* **Breaking change**. Set SDK constraints to match the Flutter beta release. - -## 0.1.1 - -* Fixed warnings from the Dart 2.0 analyzer. -* Simplified and upgraded Android project template to Android SDK 27. -* Updated package description. - -## 0.1.0 - -* **Breaking change**. Upgraded to Gradle 4.1 and Android Studio Gradle plugin - 3.0.1. Older Flutter projects need to upgrade their Gradle setup as well in - order to use this version of the plugin. Instructions can be found - [here](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1). - -## 0.0.2 - -* Add FLT prefix to iOS types. - -## 0.0.1+1 - -* Updated README - -## 0.0.1 - -* Initial release diff --git a/packages/battery/LICENSE b/packages/battery/LICENSE deleted file mode 100644 index c89293372cf3..000000000000 --- a/packages/battery/LICENSE +++ /dev/null @@ -1,27 +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/battery/README.md b/packages/battery/README.md deleted file mode 100644 index 93f8330db0db..000000000000 --- a/packages/battery/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# Battery - -[![pub package](https://img.shields.io/pub/v/battery.svg)](https://pub.dartlang.org/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/). - -### Example - -``` dart -// Import package -import 'package:battery/battery.dart'; - -// Instantiate it -var battery = Battery(); - -// Access current battery level -print(await battery.batteryLevel); - -// Be informed when the state (full, charging, discharging) changes -_battery.onBatteryStateChanged.listen((BatteryState state) { - // Do something with new state -}); -``` diff --git a/packages/battery/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/android/build.gradle b/packages/battery/android/build.gradle deleted file mode 100644 index ff485fbc6b29..000000000000 --- a/packages/battery/android/build.gradle +++ /dev/null @@ -1,34 +0,0 @@ -group 'io.flutter.plugins.battery' -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/battery/android/gradle.properties b/packages/battery/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/battery/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M 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 new file mode 100644 index 000000000000..ddc912d2ba2a --- /dev/null +++ b/packages/battery/battery/CHANGELOG.md @@ -0,0 +1,199 @@ +## NEXT + +* Remove references to the Android v1 embedding. +* Updated Android lint settings. + +## 2.0.3 + +* Update README to point to Plus Plugins version. + +## 2.0.2 + +* Migrate maven repository from jcenter to mavenCentral. + +## 2.0.1 + +* Update platform_plugin_interface version requirement. + +## 2.0.0 + +* Migrate to null safety. + +## 1.0.11 + +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. + +## 1.0.10 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 1.0.9 + +* Update Flutter SDK constraint. + +## 1.0.8 + +* Update Dart SDK constraint in example. + +## 1.0.7 + +* Update android compileSdkVersion to 29. + +## 1.0.6 + +* Keep handling deprecated Android v1 classes for backward compatibility. + +## 1.0.5 + +* Ported to use platform interface. + +## 1.0.4+1 + +* Moved everything from battery to battery/battery + +## 1.0.4 + +* Updated README.md. + +## 1.0.3 + +* Update package:e2e to use package:integration_test + + +## 1.0.2 + +* Update package:e2e reference to use the local version in the flutter/plugins + repository. + +## 1.0.1 + +* Update lower bound of dart dependency to 2.1.0. + +## 1.0.0 + +* Bump the package version to 1.0.0 following ecosystem pre-migration (https://github.com/amirh/bump_to_1.0/projects/1). + +## 0.3.1+10 + +* Update minimum Flutter version to 1.12.13+hotfix.5 +* Fix CocoaPods podspec lint warnings. + +## 0.3.1+9 + +* Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). + +## 0.3.1+8 + +* Make the pedantic dev_dependency explicit. + +## 0.3.1+7 + +* Clean up various Android workarounds no longer needed after framework v1.12. + +## 0.3.1+6 + +* Remove the deprecated `author:` field from pubspec.yaml +* Migrate the plugin to the pubspec platforms manifest. +* Require Flutter SDK 1.10.0 or greater. + +## 0.3.1+5 + +* Fix pedantic linter errors. + +## 0.3.1+4 + +* Update and migrate iOS example project. + +## 0.3.1+3 + +* Remove AndroidX warning. + +## 0.3.1+2 + +* Include lifecycle dependency as a compileOnly one on Android to resolve + potential version conflicts with other transitive libraries. + +## 0.3.1+1 + +* Android: Use android.arch.lifecycle instead of androidx.lifecycle:lifecycle in `build.gradle` to support apps that has not been migrated to AndroidX. + +## 0.3.1 + +* Support the v2 Android embedder. + +## 0.3.0+6 + +* Define clang module for iOS. + +## 0.3.0+5 + +* Fix Gradle version. + +## 0.3.0+4 + +* Update Dart code to conform to current Dart formatter. + +## 0.3.0+3 + +* Fix `batteryLevel` usage example in README + +## 0.3.0+2 + +* Bump the minimum Flutter version to 1.2.0. +* Add template type parameter to `invokeMethod` calls. + +## 0.3.0+1 + +* Log a more detailed warning at build time about the previous AndroidX + migration. + +## 0.3.0 + +* **Breaking change**. Migrate from the deprecated original Android Support + Library to AndroidX. This shouldn't result in any functional changes, but it + requires any Android apps using this plugin to [also + migrate](https://developer.android.com/jetpack/androidx/migrate) if they're + using the original support library. + +## 0.2.3 + +* Updated mockito dependency to 3.0.0 to get Dart 2 support. +* Update test package dependency to 1.3.0, and fixed tests to match. + +## 0.2.2 + +* Updated Gradle tooling to match Android Studio 3.1.2. + +## 0.2.1 + +* Fixed Dart 2 type error. +* Removed use of deprecated parameter in example. + +## 0.2.0 + +* **Breaking change**. Set SDK constraints to match the Flutter beta release. + +## 0.1.1 + +* Fixed warnings from the Dart 2.0 analyzer. +* Simplified and upgraded Android project template to Android SDK 27. +* Updated package description. + +## 0.1.0 + +* **Breaking change**. Upgraded to Gradle 4.1 and Android Studio Gradle plugin + 3.0.1. Older Flutter projects need to upgrade their Gradle setup as well in + order to use this version of the plugin. Instructions can be found + [here](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1). + +## 0.0.2 + +* Add FLT prefix to iOS types. + +## 0.0.1+1 + +* Updated README + +## 0.0.1 + +* Initial release diff --git a/packages/battery/battery/LICENSE b/packages/battery/battery/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/battery/battery/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/battery/battery/README.md b/packages/battery/battery/README.md new file mode 100644 index 000000000000..5337978b4d79 --- /dev/null +++ b/packages/battery/battery/README.md @@ -0,0 +1,41 @@ +# Battery + +--- + +## Deprecation Notice + +This plugin has been replaced by the [Flutter Community Plus +Plugins](https://plus.fluttercommunity.dev/) version, +[`battery_plus`](https://pub.dev/packages/battery_plus). +No further updates are planned to this plugin, and we encourage all users to +migrate to the Plus version. + +Critical fixes (e.g., for any security incidents) will be provided through the +end of 2021, at which point this package will be marked as discontinued. + +--- + +[![pub package](https://img.shields.io/pub/v/battery.svg)](https://pub.dev/packages/battery) + +A Flutter plugin to access various information about the battery of the device the app is running on. + +## Usage +To use this plugin, add `battery` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). + +### Example + +``` dart +// Import package +import 'package:battery/battery.dart'; + +// Instantiate it +var _battery = Battery(); + +// Access current battery level +print(await _battery.batteryLevel); + +// Be informed when the state (full, charging, discharging) changes +_battery.onBatteryStateChanged.listen((BatteryState state) { + // Do something with new state +}); +``` diff --git a/packages/battery/battery/android/build.gradle b/packages/battery/battery/android/build.gradle new file mode 100644 index 000000000000..14f503813f7e --- /dev/null +++ b/packages/battery/battery/android/build.gradle @@ -0,0 +1,48 @@ +group 'io.flutter.plugins.battery' +version '1.0-SNAPSHOT' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.3.0' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 29 + + defaultConfig { + minSdkVersion 16 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + lintOptions { + disable 'InvalidPackage' + disable 'GradleDependency' + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/packages/battery/android/gradle/wrapper/gradle-wrapper.properties b/packages/battery/battery/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/battery/android/gradle/wrapper/gradle-wrapper.properties rename to packages/battery/battery/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/battery/android/settings.gradle b/packages/battery/battery/android/settings.gradle similarity index 100% rename from packages/battery/android/settings.gradle rename to packages/battery/battery/android/settings.gradle diff --git a/packages/battery/android/src/main/AndroidManifest.xml b/packages/battery/battery/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/battery/android/src/main/AndroidManifest.xml rename to packages/battery/battery/android/src/main/AndroidManifest.xml diff --git a/packages/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 similarity index 95% rename from packages/battery/android/src/main/java/io/flutter/plugins/battery/BatteryPlugin.java rename to packages/battery/battery/android/src/main/java/io/flutter/plugins/battery/BatteryPlugin.java index c17cfc133176..7f2e1efbeede 100644 --- a/packages/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. @@ -21,7 +21,6 @@ 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; /** BatteryPlugin */ public class BatteryPlugin implements MethodCallHandler, StreamHandler, FlutterPlugin { @@ -32,7 +31,8 @@ public class BatteryPlugin implements MethodCallHandler, StreamHandler, FlutterP private EventChannel eventChannel; /** Plugin registration. */ - public static void registerWith(PluginRegistry.Registrar registrar) { + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { final BatteryPlugin instance = new BatteryPlugin(); instance.onAttachedToEngine(registrar.context(), registrar.messenger()); } diff --git a/packages/battery/battery/example/README.md b/packages/battery/battery/example/README.md new file mode 100644 index 000000000000..ac3fc4d8a470 --- /dev/null +++ b/packages/battery/battery/example/README.md @@ -0,0 +1,8 @@ +# battery_example + +Demonstrates how to use the battery plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.dev/). diff --git a/packages/battery/battery/example/android/app/build.gradle b/packages/battery/battery/example/android/app/build.gradle new file mode 100644 index 000000000000..4fcd6ba0049e --- /dev/null +++ b/packages/battery/battery/example/android/app/build.gradle @@ -0,0 +1,60 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 29 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + applicationId "io.flutter.plugins.batteryexample" + minSdkVersion 16 + targetSdkVersion 28 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' +} diff --git a/packages/battery/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/battery/battery/example/android/app/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/battery/example/android/app/gradle/wrapper/gradle-wrapper.properties rename to packages/battery/battery/example/android/app/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/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 new file mode 100644 index 000000000000..5068d043bdfc --- /dev/null +++ b/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/FlutterActivityTest.java @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.batteryexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class FlutterActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/battery/battery/example/android/app/src/main/AndroidManifest.xml b/packages/battery/battery/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..11feb41de96a --- /dev/null +++ b/packages/battery/battery/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/packages/battery/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/battery/battery/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from packages/battery/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/battery/battery/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/battery/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/battery/battery/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from packages/battery/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/battery/battery/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/battery/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/battery/battery/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from packages/battery/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/battery/battery/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/battery/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/battery/battery/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from packages/battery/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/battery/battery/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/battery/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/battery/battery/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from packages/battery/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/battery/battery/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/packages/battery/battery/example/android/build.gradle b/packages/battery/battery/example/android/build.gradle new file mode 100644 index 000000000000..e101ac08df55 --- /dev/null +++ b/packages/battery/battery/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/battery/example/android/gradle.properties b/packages/battery/battery/example/android/gradle.properties similarity index 100% rename from packages/battery/example/android/gradle.properties rename to packages/battery/battery/example/android/gradle.properties diff --git a/packages/battery/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/battery/battery/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/battery/example/android/gradle/wrapper/gradle-wrapper.properties rename to packages/battery/battery/example/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/battery/example/android/settings.gradle b/packages/battery/battery/example/android/settings.gradle similarity index 100% rename from packages/battery/example/android/settings.gradle rename to packages/battery/battery/example/android/settings.gradle 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/android_alarm_manager/example/ios/Flutter/AppFrameworkInfo.plist b/packages/battery/battery/example/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from packages/android_alarm_manager/example/ios/Flutter/AppFrameworkInfo.plist rename to packages/battery/battery/example/ios/Flutter/AppFrameworkInfo.plist diff --git a/packages/android_alarm_manager/example/ios/Flutter/Debug.xcconfig b/packages/battery/battery/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/android_alarm_manager/example/ios/Flutter/Debug.xcconfig rename to packages/battery/battery/example/ios/Flutter/Debug.xcconfig diff --git a/packages/android_alarm_manager/example/ios/Flutter/Release.xcconfig b/packages/battery/battery/example/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/android_alarm_manager/example/ios/Flutter/Release.xcconfig rename to packages/battery/battery/example/ios/Flutter/Release.xcconfig 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 new file mode 100644 index 000000000000..f994b369afe9 --- /dev/null +++ b/packages/battery/battery/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,460 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + CC33A11108F15DB5F0C6C7AD /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E2CD29898079A0E658445A5 /* libPods-Runner.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1E2CD29898079A0E658445A5 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 5F92487ECF695372E82D90C5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + BF850F5DC44F7AE2B245B994 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CC33A11108F15DB5F0C6C7AD /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1C99224A167BC35DA0CD0913 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 1E2CD29898079A0E658445A5 /* libPods-Runner.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 571753FC2D526E56A295E627 /* Pods */ = { + isa = PBXGroup; + children = ( + 5F92487ECF695372E82D90C5 /* Pods-Runner.debug.xcconfig */, + BF850F5DC44F7AE2B245B994 /* Pods-Runner.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 571753FC2D526E56A295E627 /* Pods */, + 1C99224A167BC35DA0CD0913 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + C2BE46BB51B1181F0FD17925 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1100; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + C2BE46BB51B1181F0FD17925 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.batteryExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.batteryExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/battery/battery/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/battery/battery/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/battery/battery/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/battery/battery/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/battery/battery/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/packages/android_alarm_manager/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/battery/battery/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/android_alarm_manager/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/battery/battery/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/battery/battery/example/ios/Runner/AppDelegate.h b/packages/battery/battery/example/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..0681d288bb70 --- /dev/null +++ b/packages/battery/battery/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/battery/battery/example/ios/Runner/AppDelegate.m b/packages/battery/battery/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..b790a0a52635 --- /dev/null +++ b/packages/battery/battery/example/ios/Runner/AppDelegate.m @@ -0,0 +1,16 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "AppDelegate.h" +#include "GeneratedPluginRegistrant.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + [GeneratedPluginRegistrant registerWithRegistry:self]; + return [super application:application didFinishLaunchingWithOptions:launchOptions]; +} + +@end diff --git a/packages/android_alarm_manager/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/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/battery/battery/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/android_alarm_manager/example/ios/Runner/Base.lproj/Main.storyboard b/packages/battery/battery/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/battery/battery/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/packages/battery/example/ios/Runner/Info.plist b/packages/battery/battery/example/ios/Runner/Info.plist similarity index 100% rename from packages/battery/example/ios/Runner/Info.plist rename to packages/battery/battery/example/ios/Runner/Info.plist diff --git a/packages/battery/battery/example/ios/Runner/main.m b/packages/battery/battery/example/ios/Runner/main.m new file mode 100644 index 000000000000..f97b9ef5c8a1 --- /dev/null +++ b/packages/battery/battery/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/battery/battery/example/lib/main.dart b/packages/battery/battery/example/lib/main.dart new file mode 100644 index 000000000000..b139d0d8e4be --- /dev/null +++ b/packages/battery/battery/example/lib/main.dart @@ -0,0 +1,95 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:battery/battery.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: MyHomePage(title: 'Flutter Demo Home Page'), + ); + } +} + +class MyHomePage extends StatefulWidget { + MyHomePage({Key? key, required this.title}) : super(key: key); + + final String title; + + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + final Battery _battery = Battery(); + + BatteryState? _batteryState; + late StreamSubscription _batteryStateSubscription; + + @override + void initState() { + super.initState(); + _batteryStateSubscription = + _battery.onBatteryStateChanged.listen((BatteryState state) { + setState(() { + _batteryState = state; + }); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: Center( + child: Text('$_batteryState'), + ), + floatingActionButton: FloatingActionButton( + child: const Icon(Icons.battery_unknown), + onPressed: () async { + final int batteryLevel = await _battery.batteryLevel; + // ignore: unawaited_futures + showDialog( + context: context, + builder: (_) => AlertDialog( + content: Text('Battery: $batteryLevel%'), + actions: [ + TextButton( + child: const Text('OK'), + onPressed: () { + Navigator.pop(context); + }, + ) + ], + ), + ); + }, + ), + ); + } + + @override + void dispose() { + super.dispose(); + if (_batteryStateSubscription != null) { + _batteryStateSubscription.cancel(); + } + } +} diff --git a/packages/battery/battery/example/pubspec.yaml b/packages/battery/battery/example/pubspec.yaml new file mode 100644 index 000000000000..e33dda9b86a3 --- /dev/null +++ b/packages/battery/battery/example/pubspec.yaml @@ -0,0 +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: + 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/android_alarm_manager/ios/Assets/.gitkeep b/packages/battery/battery/ios/Assets/.gitkeep similarity index 100% rename from packages/android_alarm_manager/ios/Assets/.gitkeep rename to packages/battery/battery/ios/Assets/.gitkeep diff --git a/packages/battery/ios/Classes/FLTBatteryPlugin.h b/packages/battery/battery/ios/Classes/FLTBatteryPlugin.h similarity index 76% rename from packages/battery/ios/Classes/FLTBatteryPlugin.h rename to packages/battery/battery/ios/Classes/FLTBatteryPlugin.h index 9743ca501208..fd6a3e964d83 100644 --- a/packages/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/ios/Classes/FLTBatteryPlugin.m b/packages/battery/battery/ios/Classes/FLTBatteryPlugin.m similarity index 98% rename from packages/battery/ios/Classes/FLTBatteryPlugin.m rename to packages/battery/battery/ios/Classes/FLTBatteryPlugin.m index f1e82a64eb1b..558d395bb9c0 100644 --- a/packages/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/ios/battery.podspec b/packages/battery/battery/ios/battery.podspec similarity index 100% rename from packages/battery/ios/battery.podspec rename to packages/battery/battery/ios/battery.podspec diff --git a/packages/battery/battery/lib/battery.dart b/packages/battery/battery/lib/battery.dart new file mode 100644 index 000000000000..92e54be095fe --- /dev/null +++ b/packages/battery/battery/lib/battery.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 'dart:async'; + +import 'package:battery_platform_interface/battery_platform_interface.dart'; + +export 'package:battery_platform_interface/battery_platform_interface.dart' + show BatteryState; + +/// API for accessing information about the battery of the device the Flutter +/// app is currently running on. +class Battery { + /// Returns the current battery level in percent. + Future get batteryLevel async => + await BatteryPlatform.instance.batteryLevel(); + + /// Fires whenever the battery state changes. + Stream get onBatteryStateChanged => + BatteryPlatform.instance.onBatteryStateChanged(); +} diff --git a/packages/battery/battery/pubspec.yaml b/packages/battery/battery/pubspec.yaml new file mode 100644 index 000000000000..05226e3f8029 --- /dev/null +++ b/packages/battery/battery/pubspec.yaml @@ -0,0 +1,34 @@ +name: battery +description: Flutter plugin for accessing information about the battery state + (full, charging, discharging) on Android and iOS. +repository: https://github.com/flutter/plugins/tree/master/packages/battery/battery +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+battery%22 +version: 2.0.3 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.12.13+hotfix.5" + +flutter: + plugin: + platforms: + android: + package: io.flutter.plugins.battery + pluginClass: BatteryPlugin + ios: + pluginClass: FLTBatteryPlugin + +dependencies: + flutter: + sdk: flutter + meta: ^1.3.0 + battery_platform_interface: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + plugin_platform_interface: ^2.0.0 + integration_test: + sdk: flutter + pedantic: ^1.10.0 + test: ^1.16.3 diff --git a/packages/battery/battery/test/battery_test.dart b/packages/battery/battery/test/battery_test.dart new file mode 100644 index 000000000000..8870acb775de --- /dev/null +++ b/packages/battery/battery/test/battery_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 'dart:async'; + +import 'package:battery/battery.dart'; +import 'package:battery_platform_interface/battery_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:test/fake.dart'; + +void main() { + group('battery', () { + late Battery battery; + MockBatteryPlatform fakePlatform; + setUp(() async { + fakePlatform = MockBatteryPlatform(); + BatteryPlatform.instance = fakePlatform; + battery = Battery(); + }); + test('batteryLevel', () async { + int result = await battery.batteryLevel; + expect(result, 42); + }); + test('onBatteryStateChanged', () async { + BatteryState result = await battery.onBatteryStateChanged.first; + expect(result, BatteryState.full); + }); + }); +} + +class MockBatteryPlatform extends Fake + with MockPlatformInterfaceMixin + implements BatteryPlatform { + Future batteryLevel() async { + return 42; + } + + Stream onBatteryStateChanged() { + StreamController result = StreamController(); + result.add(BatteryState.full); + return result.stream; + } +} diff --git a/packages/battery/battery_platform_interface/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 new file mode 100644 index 000000000000..a9106dd78ce9 --- /dev/null +++ b/packages/battery/battery_platform_interface/CHANGELOG.md @@ -0,0 +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 new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/battery/battery_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/battery/battery_platform_interface/README.md b/packages/battery/battery_platform_interface/README.md new file mode 100644 index 000000000000..e1a42571c6b3 --- /dev/null +++ b/packages/battery/battery_platform_interface/README.md @@ -0,0 +1,26 @@ +# battery_platform_interface + +A common platform interface for the [`battery`][1] plugin. + +This interface allows platform-specific implementations of the `battery` +plugin, as well as the plugin itself, to ensure they are supporting the +same interface. + +# Usage + +To implement a new platform-specific implementation of `battery`, extend +[`BatteryPlatform`][2] with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`BatteryPlatform` by calling +`BatteryPlatform.instance = MyPlatformBattery()`. + +# Note on breaking changes + +Strongly prefer non-breaking changes (such as adding a method to the interface) +over breaking changes for this package. + +See https://flutter.dev/go/platform-interface-breaking-changes for a discussion +on why a less-clean interface is preferable to a breaking change. + +[1]: ../battery +[2]: lib/battery_platform_interface.dart diff --git a/packages/battery/battery_platform_interface/lib/battery_platform_interface.dart b/packages/battery/battery_platform_interface/lib/battery_platform_interface.dart new file mode 100644 index 000000000000..548edf8257f5 --- /dev/null +++ b/packages/battery/battery_platform_interface/lib/battery_platform_interface.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:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'method_channel/method_channel_battery.dart'; +import 'enums/battery_state.dart'; + +export 'enums/battery_state.dart'; + +/// The interface that implementations of battery must implement. +/// +/// Platform implementations should extend this class rather than implement it as `battery` +/// does not consider newly added methods to be breaking changes. Extending this class +/// (using `extends`) ensures that the subclass will get the default implementation, while +/// platform implementations that `implements` this interface will be broken by newly added +/// [BatteryPlatform] methods. +abstract class BatteryPlatform extends PlatformInterface { + /// Constructs a BatteryPlatform. + BatteryPlatform() : super(token: _token); + + static final Object _token = Object(); + + static BatteryPlatform _instance = MethodChannelBattery(); + + /// The default instance of [BatteryPlatform] to use. + /// + /// Defaults to [MethodChannelBattery]. + static BatteryPlatform get instance => _instance; + + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [BatteryPlatform] when they register themselves. + static set instance(BatteryPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + /// Gets the battery level from device. + Future batteryLevel() { + throw UnimplementedError('batteryLevel() has not been implemented.'); + } + + /// gets battery state from device. + Stream onBatteryStateChanged() { + throw UnimplementedError( + 'onBatteryStateChanged() has not been implemented.'); + } +} diff --git a/packages/battery/battery_platform_interface/lib/enums/battery_state.dart b/packages/battery/battery_platform_interface/lib/enums/battery_state.dart new file mode 100644 index 000000000000..a525e78ccdf5 --- /dev/null +++ b/packages/battery/battery_platform_interface/lib/enums/battery_state.dart @@ -0,0 +1,15 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Indicates the current battery state. +enum BatteryState { + /// The battery is completely full of energy. + full, + + /// The battery is currently storing energy. + charging, + + /// The battery is currently losing energy. + discharging +} diff --git a/packages/battery/battery_platform_interface/lib/method_channel/method_channel_battery.dart b/packages/battery/battery_platform_interface/lib/method_channel/method_channel_battery.dart new file mode 100644 index 000000000000..1d0a8329c257 --- /dev/null +++ b/packages/battery/battery_platform_interface/lib/method_channel/method_channel_battery.dart @@ -0,0 +1,56 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart'; + +import 'package:battery_platform_interface/battery_platform_interface.dart'; + +import '../battery_platform_interface.dart'; + +/// An implementation of [BatteryPlatform] that uses method channels. +class MethodChannelBattery extends BatteryPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final MethodChannel channel = MethodChannel('plugins.flutter.io/battery'); + + /// The event channel used to interact with the native platform. + @visibleForTesting + final EventChannel eventChannel = EventChannel('plugins.flutter.io/charging'); + + /// Method channel for getting battery level. + Future batteryLevel() async { + return (await channel.invokeMethod('getBatteryLevel')).toInt(); + } + + /// Stream variable for storing battery state. + Stream? _onBatteryStateChanged; + + /// Event channel for getting battery change state. + Stream onBatteryStateChanged() { + if (_onBatteryStateChanged == null) { + _onBatteryStateChanged = eventChannel + .receiveBroadcastStream() + .map((dynamic event) => _parseBatteryState(event)); + } + + return _onBatteryStateChanged!; + } +} + +/// Method for parsing battery state. +BatteryState _parseBatteryState(String state) { + switch (state) { + case 'full': + return BatteryState.full; + case 'charging': + return BatteryState.charging; + case 'discharging': + return BatteryState.discharging; + default: + throw ArgumentError('$state is not a valid BatteryState.'); + } +} diff --git a/packages/battery/battery_platform_interface/pubspec.yaml b/packages/battery/battery_platform_interface/pubspec.yaml new file mode 100644 index 000000000000..461cd0bd88ea --- /dev/null +++ b/packages/battery/battery_platform_interface/pubspec.yaml @@ -0,0 +1,23 @@ +name: battery_platform_interface +description: A common platform interface for the battery plugin. +repository: https://github.com/flutter/plugins/tree/master/packages/battery/battery_platform_interface +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+battery%22 +# NOTE: We strongly prefer non-breaking changes, even at the expense of a +# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes +version: 2.0.1 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" + +dependencies: + flutter: + sdk: flutter + meta: ^1.3.0 + plugin_platform_interface: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + mockito: ^5.0.0 + pedantic: ^1.10.0 diff --git a/packages/battery/battery_platform_interface/test/method_channel_battery_test.dart b/packages/battery/battery_platform_interface/test/method_channel_battery_test.dart new file mode 100644 index 000000000000..697582843a95 --- /dev/null +++ b/packages/battery/battery_platform_interface/test/method_channel_battery_test.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:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:battery_platform_interface/battery_platform_interface.dart'; + +import 'package:battery_platform_interface/method_channel/method_channel_battery.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$MethodChannelBattery', () { + late MethodChannelBattery methodChannelBattery; + + setUp(() async { + methodChannelBattery = MethodChannelBattery(); + + methodChannelBattery.channel + .setMockMethodCallHandler((MethodCall methodCall) async { + switch (methodCall.method) { + case 'getBatteryLevel': + return 90; + default: + return null; + } + }); + + MethodChannel(methodChannelBattery.eventChannel.name) + .setMockMethodCallHandler((MethodCall methodCall) async { + switch (methodCall.method) { + case 'listen': + await _ambiguate(ServicesBinding.instance)! + .defaultBinaryMessenger + .handlePlatformMessage( + methodChannelBattery.eventChannel.name, + methodChannelBattery.eventChannel.codec + .encodeSuccessEnvelope('full'), + (_) {}, + ); + break; + case 'cancel': + default: + return null; + } + }); + }); + + /// Test for batetry level call. + test('getBatteryLevel', () async { + final int result = await methodChannelBattery.batteryLevel(); + expect(result, 90); + }); + + /// Test for battery changed state call. + test('onBatteryChanged', () async { + final BatteryState result = + await methodChannelBattery.onBatteryStateChanged().first; + expect(result, BatteryState.full); + }); + }); +} + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +// TODO(ianh): Remove this once we roll stable in late 2021. +T? _ambiguate(T? value) => value; diff --git a/packages/battery/example/README.md b/packages/battery/example/README.md deleted file mode 100644 index dcb94ed1b616..000000000000 --- a/packages/battery/example/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# battery_example - -Demonstrates how to use the battery plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](http://flutter.io/). diff --git a/packages/battery/example/android/app/build.gradle b/packages/battery/example/android/app/build.gradle deleted file mode 100644 index e84c2c45889d..000000000000 --- a/packages/battery/example/android/app/build.gradle +++ /dev/null @@ -1,60 +0,0 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -apply plugin: 'com.android.application' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -android { - compileSdkVersion 28 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - applicationId "io.flutter.plugins.batteryexample" - minSdkVersion 16 - targetSdkVersion 28 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} - -dependencies { - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' -} diff --git a/packages/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/EmbedderV1ActivityTest.java b/packages/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/EmbedderV1ActivityTest.java deleted file mode 100644 index ef6e5d9fe246..000000000000 --- a/packages/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/EmbedderV1ActivityTest.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.batteryexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class EmbedderV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbedderV1Activity.class); -} diff --git a/packages/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/FlutterActivityTest.java b/packages/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/FlutterActivityTest.java deleted file mode 100644 index 1986d0a55c32..000000000000 --- a/packages/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/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.batteryexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import io.flutter.embedding.android.FlutterActivity; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class FlutterActivityTest { - @Rule - public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); -} diff --git a/packages/battery/example/android/app/src/main/AndroidManifest.xml b/packages/battery/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index d44a8ac5757a..000000000000 --- a/packages/battery/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/packages/battery/example/android/app/src/main/java/io/flutter/plugins/batteryexample/EmbedderV1Activity.java b/packages/battery/example/android/app/src/main/java/io/flutter/plugins/batteryexample/EmbedderV1Activity.java deleted file mode 100644 index 7ccc9c1e2fd3..000000000000 --- a/packages/battery/example/android/app/src/main/java/io/flutter/plugins/batteryexample/EmbedderV1Activity.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.batteryexample; - -import android.os.Bundle; -import dev.flutter.plugins.e2e.E2EPlugin; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.battery.BatteryPlugin; - -public class EmbedderV1Activity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - BatteryPlugin.registerWith(registrarFor("io.flutter.plugins.battery.BatteryPlugin")); - E2EPlugin.registerWith(registrarFor("dev.flutter.plugins.e2e.E2EPlugin")); - } -} diff --git a/packages/battery/example/android/build.gradle b/packages/battery/example/android/build.gradle deleted file mode 100644 index 541636cc492a..000000000000 --- a/packages/battery/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/battery/example/battery_example.iml b/packages/battery/example/battery_example.iml deleted file mode 100644 index 9d5dae19540c..000000000000 --- a/packages/battery/example/battery_example.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/battery/example/ios/Flutter/AppFrameworkInfo.plist b/packages/battery/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6c2de8086bcd..000000000000 --- a/packages/battery/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - UIRequiredDeviceCapabilities - - arm64 - - MinimumOSVersion - 8.0 - - diff --git a/packages/battery/example/ios/Runner.xcodeproj/project.pbxproj b/packages/battery/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index aa42a8509346..000000000000 --- a/packages/battery/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 */; }; - CC33A11108F15DB5F0C6C7AD /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1E2CD29898079A0E658445A5 /* libPods-Runner.a */; }; -/* End PBXBuildFile section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 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 = ""; }; - 1E2CD29898079A0E658445A5 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; - 5F92487ECF695372E82D90C5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 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 = ""; }; - BF850F5DC44F7AE2B245B994 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - CC33A11108F15DB5F0C6C7AD /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 1C99224A167BC35DA0CD0913 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 1E2CD29898079A0E658445A5 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; - 571753FC2D526E56A295E627 /* Pods */ = { - isa = PBXGroup; - children = ( - 5F92487ECF695372E82D90C5 /* Pods-Runner.debug.xcconfig */, - BF850F5DC44F7AE2B245B994 /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 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 */, - 571753FC2D526E56A295E627 /* Pods */, - 1C99224A167BC35DA0CD0913 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - C2BE46BB51B1181F0FD17925 /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 4096151B6BA12D6D4D7DD96A /* [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"; - }; - 4096151B6BA12D6D4D7DD96A /* [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"; - }; - C2BE46BB51B1181F0FD17925 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - 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.batteryExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.batteryExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/battery/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/battery/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a16ed0f..000000000000 --- a/packages/battery/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/packages/battery/example/ios/Runner/AppDelegate.h b/packages/battery/example/ios/Runner/AppDelegate.h deleted file mode 100644 index d9e18e990f2e..000000000000 --- a/packages/battery/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/battery/example/ios/Runner/AppDelegate.m b/packages/battery/example/ios/Runner/AppDelegate.m deleted file mode 100644 index a4b51c88eb60..000000000000 --- a/packages/battery/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/battery/example/ios/Runner/main.m b/packages/battery/example/ios/Runner/main.m deleted file mode 100644 index bec320c0bee0..000000000000 --- a/packages/battery/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/battery/example/lib/main.dart b/packages/battery/example/lib/main.dart deleted file mode 100644 index 1c1dfcf252b6..000000000000 --- a/packages/battery/example/lib/main.dart +++ /dev/null @@ -1,95 +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 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:battery/battery.dart'; - -void main() { - runApp(MyApp()); -} - -class MyApp extends StatelessWidget { - @override - Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: MyHomePage(title: 'Flutter Demo Home Page'), - ); - } -} - -class MyHomePage extends StatefulWidget { - MyHomePage({Key key, this.title}) : super(key: key); - - final String title; - - @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - Battery _battery = Battery(); - - BatteryState _batteryState; - StreamSubscription _batteryStateSubscription; - - @override - void initState() { - super.initState(); - _batteryStateSubscription = - _battery.onBatteryStateChanged.listen((BatteryState state) { - setState(() { - _batteryState = state; - }); - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Plugin example app'), - ), - body: Center( - child: Text('$_batteryState'), - ), - floatingActionButton: FloatingActionButton( - child: const Icon(Icons.battery_unknown), - onPressed: () async { - final int batteryLevel = await _battery.batteryLevel; - // ignore: unawaited_futures - showDialog( - context: context, - builder: (_) => AlertDialog( - content: Text('Battery: $batteryLevel%'), - actions: [ - FlatButton( - child: const Text('OK'), - onPressed: () { - Navigator.pop(context); - }, - ) - ], - ), - ); - }, - ), - ); - } - - @override - void dispose() { - super.dispose(); - if (_batteryStateSubscription != null) { - _batteryStateSubscription.cancel(); - } - } -} diff --git a/packages/battery/example/pubspec.yaml b/packages/battery/example/pubspec.yaml deleted file mode 100644 index 0788d02a9431..000000000000 --- a/packages/battery/example/pubspec.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: battery_example -description: Demonstrates how to use the battery plugin. - -dependencies: - flutter: - sdk: flutter - battery: - path: ../ - -dev_dependencies: - flutter_driver: - sdk: flutter - e2e: ^0.2.1 - pedantic: ^1.8.0 - -flutter: - uses-material-design: true diff --git a/packages/battery/lib/battery.dart b/packages/battery/lib/battery.dart deleted file mode 100644 index 091b001f749f..000000000000 --- a/packages/battery/lib/battery.dart +++ /dev/null @@ -1,75 +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' show visibleForTesting; - -/// Indicates the current battery state. -enum BatteryState { - /// The battery is completely full of energy. - full, - - /// The battery is currently storing energy. - charging, - - /// The battery is currently losing energy. - discharging -} - -/// API for accessing information about the battery of the device the Flutter -/// app is currently running on. -class Battery { - /// Initializes the plugin and starts listening for potential platform events. - factory Battery() { - if (_instance == null) { - final MethodChannel methodChannel = - const MethodChannel('plugins.flutter.io/battery'); - final EventChannel eventChannel = - const EventChannel('plugins.flutter.io/charging'); - _instance = Battery.private(methodChannel, eventChannel); - } - return _instance; - } - - /// This constructor is only used for testing and shouldn't be accessed by - /// users of the plugin. It may break or change at any time. - @visibleForTesting - Battery.private(this._methodChannel, this._eventChannel); - - static Battery _instance; - - final MethodChannel _methodChannel; - final EventChannel _eventChannel; - Stream _onBatteryStateChanged; - - /// Returns the current battery level in percent. - Future get batteryLevel => _methodChannel - .invokeMethod('getBatteryLevel') - .then((dynamic result) => result); - - /// Fires whenever the battery state changes. - Stream get onBatteryStateChanged { - if (_onBatteryStateChanged == null) { - _onBatteryStateChanged = _eventChannel - .receiveBroadcastStream() - .map((dynamic event) => _parseBatteryState(event)); - } - return _onBatteryStateChanged; - } -} - -BatteryState _parseBatteryState(String state) { - switch (state) { - case 'full': - return BatteryState.full; - case 'charging': - return BatteryState.charging; - case 'discharging': - return BatteryState.discharging; - default: - throw ArgumentError('$state is not a valid BatteryState.'); - } -} diff --git a/packages/battery/pubspec.yaml b/packages/battery/pubspec.yaml deleted file mode 100644 index 6ad8fcfad97a..000000000000 --- a/packages/battery/pubspec.yaml +++ /dev/null @@ -1,32 +0,0 @@ -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 -version: 1.0.1 - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.battery - pluginClass: BatteryPlugin - ios: - pluginClass: FLTBatteryPlugin - -dependencies: - flutter: - sdk: flutter - meta: ^1.0.5 - -dev_dependencies: - async: ^2.0.8 - test: ^1.3.0 - mockito: 3.0.0 - flutter_test: - sdk: flutter - e2e: ^0.2.1 - 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/battery/test/battery_e2e.dart b/packages/battery/test/battery_e2e.dart deleted file mode 100644 index 6ffc7e6541fb..000000000000 --- a/packages/battery/test/battery_e2e.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:e2e/e2e.dart'; - -void main() { - E2EWidgetsFlutterBinding.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/test/battery_test.dart b/packages/battery/test/battery_test.dart deleted file mode 100644 index 93d69604c83a..000000000000 --- a/packages/battery/test/battery_test.dart +++ /dev/null @@ -1,71 +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:async/async.dart'; -import 'package:flutter/services.dart'; -import 'package:test/test.dart'; -import 'package:battery/battery.dart'; -import 'package:mockito/mockito.dart'; - -void main() { - MockMethodChannel methodChannel; - MockEventChannel eventChannel; - Battery battery; - - setUp(() { - methodChannel = MockMethodChannel(); - eventChannel = MockEventChannel(); - battery = Battery.private(methodChannel, eventChannel); - }); - - test('batteryLevel', () async { - when(methodChannel.invokeMethod('getBatteryLevel')) - .thenAnswer((Invocation invoke) => Future.value(42)); - expect(await battery.batteryLevel, 42); - }); - - group('battery state', () { - StreamController controller; - - setUp(() { - controller = StreamController(); - when(eventChannel.receiveBroadcastStream()) - .thenAnswer((Invocation invoke) => controller.stream); - }); - - tearDown(() { - controller.close(); - }); - - test('calls receiveBroadcastStream once', () { - battery.onBatteryStateChanged; - battery.onBatteryStateChanged; - battery.onBatteryStateChanged; - verify(eventChannel.receiveBroadcastStream()).called(1); - }); - - test('receive values', () async { - final StreamQueue queue = - StreamQueue(battery.onBatteryStateChanged); - - controller.add("full"); - expect(await queue.next, BatteryState.full); - - controller.add("discharging"); - expect(await queue.next, BatteryState.discharging); - - controller.add("charging"); - expect(await queue.next, BatteryState.charging); - - controller.add("illegal"); - expect(queue.next, throwsArgumentError); - }); - }); -} - -class MockMethodChannel extends Mock implements MethodChannel {} - -class MockEventChannel extends Mock implements EventChannel {} diff --git a/packages/camera/CHANGELOG.md b/packages/camera/CHANGELOG.md deleted file mode 100644 index 379e03dcb030..000000000000 --- a/packages/camera/CHANGELOG.md +++ /dev/null @@ -1,272 +0,0 @@ -## 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 c89293372cf3..000000000000 --- a/packages/camera/LICENSE +++ /dev/null @@ -1,27 +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 4c23236cd0aa..000000000000 --- a/packages/camera/README.md +++ /dev/null @@ -1,103 +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 { - 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 95f5a22cfb6d..000000000000 --- a/packages/camera/android/build.gradle +++ /dev/null @@ -1,49 +0,0 @@ -group 'io.flutter.plugins.camera' -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 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/gradle.properties b/packages/camera/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/camera/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M 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 cb58d19a9a02..000000000000 --- a/packages/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java +++ /dev/null @@ -1,174 +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); - } - - 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..c9dfc63eb46c --- /dev/null +++ b/packages/camera/camera/CHANGELOG.md @@ -0,0 +1,544 @@ +## NEXT + +* Updated package description. + +## 0.9.4+1 + +* Fixed Android implementation throwing IllegalStateException when switching to a different activity. + +## 0.9.4 + +* Add web support by endorsing `package:camera_web`. + +## 0.9.3+1 + +* Remove iOS 9 availability check around ultra high capture sessions. + +## 0.9.3 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. + +## 0.9.2+2 + +* Ensure that setting the exposure offset returns the new offset value on Android. + +## 0.9.2+1 + +* Fixed camera controller throwing an exception when being replaced in the preview widget. + +## 0.9.2 + +* Added functions to pause and resume the camera preview. + +## 0.9.1+1 + +* Replace `device_info` reference with `device_info_plus` in the [README.md](README.md) + +## 0.9.1 + +* Added `lensAperture`, `sensorExposureTime` and `sensorSensitivity` properties to the `CameraImage` dto. + +## 0.9.0 + +* Complete rewrite of Android plugin to fix many capture, focus, flash, orientation and exposure issues. +* Fixed crash when opening front-facing cameras on some legacy android devices like Sony XZ. +* Android Flash mode works with full precapture sequence. +* Updated Android lint settings. + +## 0.8.1+7 + +* Fix device orientation sometimes not affecting the camera preview orientation. + +## 0.8.1+6 + +* Remove references to the Android V1 embedding. + +## 0.8.1+5 + +* Make sure the `setFocusPoint` and `setExposurePoint` coordinates work correctly in all orientations on iOS (instead of only in portrait mode). + +## 0.8.1+4 + +* Silenced warnings that may occur during build when using a very + recent version of Flutter relating to null safety. + +## 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..24566e76bbfc --- /dev/null +++ b/packages/camera/camera/README.md @@ -0,0 +1,137 @@ +# Camera Plugin + +[![pub package](https://img.shields.io/pub/v/camera.svg)](https://pub.dev/packages/camera) + +A Flutter plugin for iOS, Android and Web allowing access to the device cameras. + +*Note*: This plugin is still under development, and some APIs might not be available yet. We are working on a refactor which can be followed here: [issue](https://github.com/flutter/flutter/issues/31225) + +## 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 + +The camera plugin functionality works on iOS 10.0 or higher. If compiling for any version lower than 10.0, +make sure to programmatically check the version of iOS running on the device before using any camera plugin features. +The [device_info_plus](https://pub.dev/packages/device_info_plus) plugin, for example, can be used to check the iOS version. + +Add two rows to the `ios/Runner/Info.plist`: + +* 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. + +### Web integration + +For web integration details, see the +[`camera_web` package](https://pub.dev/packages/camera_web). + +### 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..633efd0b284a --- /dev/null +++ b/packages/camera/camera/android/build.gradle @@ -0,0 +1,66 @@ +group 'io.flutter.plugins.camera' +version '1.0-SNAPSHOT' +def args = ["-Xlint:deprecation","-Xlint:unchecked","-Werror"] + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.5.0' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +project.getTasks().withType(JavaCompile){ + options.compilerArgs.addAll(args) +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 29 + + defaultConfig { + minSdkVersion 21 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + lintOptions { + disable 'InvalidPackage' + disable 'GradleDependency' + baseline file("lint-baseline.xml") + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} + +dependencies { + compileOnly 'androidx.annotation:annotation:1.1.0' + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-inline:3.12.4' + testImplementation 'androidx.test:core:1.3.0' + testImplementation 'org.robolectric:robolectric:4.3' +} diff --git a/packages/camera/camera/android/lint-baseline.xml b/packages/camera/camera/android/lint-baseline.xml new file mode 100644 index 000000000000..4ddaafa87988 --- /dev/null +++ b/packages/camera/camera/android/lint-baseline.xml @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..75ced531b08a --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/Camera.java @@ -0,0 +1,1156 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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.annotation.SuppressLint; +import android.annotation.TargetApi; +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.CameraDevice; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.TotalCaptureResult; +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.HandlerThread; +import android.os.Looper; +import android.util.Log; +import android.util.Size; +import android.view.Display; +import android.view.Surface; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugins.camera.features.CameraFeature; +import io.flutter.plugins.camera.features.CameraFeatureFactory; +import io.flutter.plugins.camera.features.CameraFeatures; +import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; +import io.flutter.plugins.camera.features.autofocus.FocusMode; +import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature; +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; +import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature; +import io.flutter.plugins.camera.features.exposurepoint.ExposurePointFeature; +import io.flutter.plugins.camera.features.flash.FlashFeature; +import io.flutter.plugins.camera.features.flash.FlashMode; +import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionPreset; +import io.flutter.plugins.camera.features.sensororientation.DeviceOrientationManager; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; +import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; +import io.flutter.plugins.camera.media.MediaRecorderBuilder; +import io.flutter.plugins.camera.types.CameraCaptureProperties; +import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper; +import io.flutter.view.TextureRegistry.SurfaceTextureEntry; +import java.io.File; +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); +} + +class Camera + implements CameraCaptureCallback.CameraCaptureStateListener, + ImageReader.OnImageAvailableListener { + private static final String TAG = "Camera"; + + private static final HashMap supportedImageFormats; + + // Current supported outputs. + static { + supportedImageFormats = new HashMap<>(); + supportedImageFormats.put("yuv420", ImageFormat.YUV_420_888); + supportedImageFormats.put("jpeg", ImageFormat.JPEG); + } + + /** + * Holds all of the camera features/settings and will be used to update the request builder when + * one changes. + */ + private final CameraFeatures cameraFeatures; + + private final SurfaceTextureEntry flutterTexture; + private final boolean enableAudio; + private final Context applicationContext; + private final DartMessenger dartMessenger; + private final CameraProperties cameraProperties; + private final CameraFeatureFactory cameraFeatureFactory; + private final Activity activity; + /** A {@link CameraCaptureSession.CaptureCallback} that handles events related to JPEG capture. */ + private final CameraCaptureCallback cameraCaptureCallback; + /** A {@link Handler} for running tasks in the background. */ + private Handler backgroundHandler; + + /** An additional thread for running tasks that shouldn't block the UI. */ + private HandlerThread backgroundHandlerThread; + + private CameraDevice cameraDevice; + private CameraCaptureSession captureSession; + private ImageReader pictureImageReader; + private ImageReader imageStreamReader; + /** {@link CaptureRequest.Builder} for the camera preview */ + private CaptureRequest.Builder previewRequestBuilder; + + private MediaRecorder mediaRecorder; + /** True when recording video. */ + private boolean recordingVideo; + /** True when the preview is paused. */ + private boolean pausedPreview; + + private File captureFile; + + /** Holds the current capture timeouts */ + private CaptureTimeoutsWrapper captureTimeouts; + /** Holds the last known capture properties */ + private CameraCaptureProperties captureProps; + + private MethodChannel.Result flutterResult; + + public Camera( + final Activity activity, + final SurfaceTextureEntry flutterTexture, + final CameraFeatureFactory cameraFeatureFactory, + final DartMessenger dartMessenger, + final CameraProperties cameraProperties, + final ResolutionPreset resolutionPreset, + final boolean enableAudio) { + + if (activity == null) { + throw new IllegalStateException("No activity available!"); + } + this.activity = activity; + this.enableAudio = enableAudio; + this.flutterTexture = flutterTexture; + this.dartMessenger = dartMessenger; + this.applicationContext = activity.getApplicationContext(); + this.cameraProperties = cameraProperties; + this.cameraFeatureFactory = cameraFeatureFactory; + this.cameraFeatures = + CameraFeatures.init( + cameraFeatureFactory, cameraProperties, activity, dartMessenger, resolutionPreset); + + // Create capture callback. + captureTimeouts = new CaptureTimeoutsWrapper(3000, 3000); + captureProps = new CameraCaptureProperties(); + cameraCaptureCallback = CameraCaptureCallback.create(this, captureTimeouts, captureProps); + + startBackgroundThread(); + } + + @Override + public void onConverged() { + takePictureAfterPrecapture(); + } + + @Override + public void onPrecapture() { + runPrecaptureSequence(); + } + + /** + * Updates the builder settings with all of the available features. + * + * @param requestBuilder request builder to update. + */ + private void updateBuilderSettings(CaptureRequest.Builder requestBuilder) { + for (CameraFeature feature : cameraFeatures.getAllFeatures()) { + Log.d(TAG, "Updating builder with feature: " + feature.getDebugName()); + feature.updateBuilder(requestBuilder); + } + } + + private void prepareMediaRecorder(String outputFilePath) throws IOException { + Log.i(TAG, "prepareMediaRecorder"); + + if (mediaRecorder != null) { + mediaRecorder.release(); + } + + final PlatformChannel.DeviceOrientation lockedOrientation = + ((SensorOrientationFeature) cameraFeatures.getSensorOrientation()) + .getLockedCaptureOrientation(); + + mediaRecorder = + new MediaRecorderBuilder(getRecordingProfile(), outputFilePath) + .setEnableAudio(enableAudio) + .setMediaOrientation( + lockedOrientation == null + ? getDeviceOrientationManager().getVideoOrientation() + : getDeviceOrientationManager().getVideoOrientation(lockedOrientation)) + .build(); + } + + @SuppressLint("MissingPermission") + public void open(String imageFormatGroup) throws CameraAccessException { + final ResolutionFeature resolutionFeature = cameraFeatures.getResolution(); + + if (!resolutionFeature.checkIsSupported()) { + // Tell the user that the camera they are trying to open is not supported, + // as its {@link android.media.CamcorderProfile} cannot be fetched due to the name + // not being a valid parsable integer. + dartMessenger.sendCameraErrorEvent( + "Camera with name \"" + + cameraProperties.getCameraName() + + "\" is not supported by this plugin."); + return; + } + + // Always capture using JPEG format. + pictureImageReader = + ImageReader.newInstance( + resolutionFeature.getCaptureSize().getWidth(), + resolutionFeature.getCaptureSize().getHeight(), + ImageFormat.JPEG, + 1); + + // For image streaming, use the provided image format or fall back to YUV420. + 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; + } + imageStreamReader = + ImageReader.newInstance( + resolutionFeature.getPreviewSize().getWidth(), + resolutionFeature.getPreviewSize().getHeight(), + imageFormat, + 1); + + // Open the camera. + CameraManager cameraManager = CameraUtils.getCameraManager(activity); + cameraManager.openCamera( + cameraProperties.getCameraName(), + new CameraDevice.StateCallback() { + @Override + public void onOpened(@NonNull CameraDevice device) { + cameraDevice = device; + try { + startPreview(); + dartMessenger.sendCameraInitializedEvent( + resolutionFeature.getPreviewSize().getWidth(), + resolutionFeature.getPreviewSize().getHeight(), + cameraFeatures.getExposureLock().getValue(), + cameraFeatures.getAutoFocus().getValue(), + cameraFeatures.getExposurePoint().checkIsSupported(), + cameraFeatures.getFocusPoint().checkIsSupported()); + } catch (CameraAccessException e) { + dartMessenger.sendCameraErrorEvent(e.getMessage()); + close(); + } + } + + @Override + public void onClosed(@NonNull CameraDevice camera) { + Log.i(TAG, "open | onClosed"); + + dartMessenger.sendCameraClosingEvent(); + super.onClosed(camera); + } + + @Override + public void onDisconnected(@NonNull CameraDevice cameraDevice) { + Log.i(TAG, "open | onDisconnected"); + + close(); + dartMessenger.sendCameraErrorEvent("The camera was disconnected."); + } + + @Override + public void onError(@NonNull CameraDevice cameraDevice, int errorCode) { + Log.i(TAG, "open | onError"); + + 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); + } + }, + backgroundHandler); + } + + 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. + previewRequestBuilder = cameraDevice.createCaptureRequest(templateType); + + // Build Flutter surface to render to. + ResolutionFeature resolutionFeature = cameraFeatures.getResolution(); + SurfaceTexture surfaceTexture = flutterTexture.surfaceTexture(); + surfaceTexture.setDefaultBufferSize( + resolutionFeature.getPreviewSize().getWidth(), + resolutionFeature.getPreviewSize().getHeight()); + Surface flutterSurface = new Surface(surfaceTexture); + previewRequestBuilder.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) { + previewRequestBuilder.addTarget(surface); + } + } + + // Update camera regions. + Size cameraBoundaries = + CameraRegionUtils.getCameraBoundaries(cameraProperties, previewRequestBuilder); + cameraFeatures.getExposurePoint().setCameraBoundaries(cameraBoundaries); + cameraFeatures.getFocusPoint().setCameraBoundaries(cameraBoundaries); + + // Prepare the callback. + CameraCaptureSession.StateCallback callback = + new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(@NonNull CameraCaptureSession session) { + // Camera was already closed. + if (cameraDevice == null) { + dartMessenger.sendCameraErrorEvent("The camera was closed during configuration."); + return; + } + captureSession = session; + + Log.i(TAG, "Updating builder settings"); + updateBuilderSettings(previewRequestBuilder); + + 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 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 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, backgroundHandler); + } + + // Send a repeating request to refresh capture session. + private void refreshPreviewCaptureSession( + @Nullable Runnable onSuccessCallback, @NonNull ErrorCallback onErrorCallback) { + if (captureSession == null) { + Log.i( + TAG, + "[refreshPreviewCaptureSession] captureSession not yet initialized, " + + "skipping preview capture session refresh."); + return; + } + + try { + if (!pausedPreview) { + captureSession.setRepeatingRequest( + previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); + } + + if (onSuccessCallback != null) { + onSuccessCallback.run(); + } + + } catch (CameraAccessException e) { + onErrorCallback.onError("cameraAccess", e.getMessage()); + } + } + + public void takePicture(@NonNull final Result result) { + // Only take one picture at a time. + if (cameraCaptureCallback.getCameraState() != CameraState.STATE_PREVIEW) { + result.error("captureAlreadyActive", "Picture is currently already being captured", null); + return; + } + + flutterResult = result; + + // Create temporary file. + final File outputDir = applicationContext.getCacheDir(); + try { + captureFile = File.createTempFile("CAP", ".jpg", outputDir); + captureTimeouts.reset(); + } catch (IOException | SecurityException e) { + dartMessenger.error(flutterResult, "cannotCreateFile", e.getMessage(), null); + return; + } + + // Listen for picture being taken. + pictureImageReader.setOnImageAvailableListener(this, backgroundHandler); + + final AutoFocusFeature autoFocusFeature = cameraFeatures.getAutoFocus(); + final boolean isAutoFocusSupported = autoFocusFeature.checkIsSupported(); + if (isAutoFocusSupported && autoFocusFeature.getValue() == FocusMode.auto) { + runPictureAutoFocus(); + } else { + runPrecaptureSequence(); + } + } + + /** + * Run the precapture sequence for capturing a still image. This method should be called when a + * response is received in {@link #cameraCaptureCallback} from lockFocus(). + */ + private void runPrecaptureSequence() { + Log.i(TAG, "runPrecaptureSequence"); + try { + // First set precapture state to idle or else it can hang in STATE_WAITING_PRECAPTURE_START. + previewRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE); + captureSession.capture( + previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); + + // Repeating request to refresh preview session. + refreshPreviewCaptureSession( + null, + (code, message) -> dartMessenger.error(flutterResult, "cameraAccess", message, null)); + + // Start precapture. + cameraCaptureCallback.setCameraState(CameraState.STATE_WAITING_PRECAPTURE_START); + + previewRequestBuilder.set( + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER, + CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START); + + // Trigger one capture to start AE sequence. + captureSession.capture( + previewRequestBuilder.build(), cameraCaptureCallback, backgroundHandler); + + } catch (CameraAccessException e) { + e.printStackTrace(); + } + } + + /** + * Capture a still picture. This method should be called when a response is received {@link + * #cameraCaptureCallback} from both lockFocus(). + */ + private void takePictureAfterPrecapture() { + Log.i(TAG, "captureStillPicture"); + cameraCaptureCallback.setCameraState(CameraState.STATE_CAPTURING); + + if (cameraDevice == null) { + return; + } + // This is the CaptureRequest.Builder that is used to take a picture. + CaptureRequest.Builder stillBuilder; + try { + stillBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE); + } catch (CameraAccessException e) { + dartMessenger.error(flutterResult, "cameraAccess", e.getMessage(), null); + return; + } + stillBuilder.addTarget(pictureImageReader.getSurface()); + + // Zoom. + stillBuilder.set( + CaptureRequest.SCALER_CROP_REGION, + previewRequestBuilder.get(CaptureRequest.SCALER_CROP_REGION)); + + // Have all features update the builder. + updateBuilderSettings(stillBuilder); + + // Orientation. + final PlatformChannel.DeviceOrientation lockedOrientation = + ((SensorOrientationFeature) cameraFeatures.getSensorOrientation()) + .getLockedCaptureOrientation(); + stillBuilder.set( + CaptureRequest.JPEG_ORIENTATION, + lockedOrientation == null + ? getDeviceOrientationManager().getPhotoOrientation() + : getDeviceOrientationManager().getPhotoOrientation(lockedOrientation)); + + CameraCaptureSession.CaptureCallback captureCallback = + new CameraCaptureSession.CaptureCallback() { + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + unlockAutoFocus(); + } + }; + + try { + captureSession.stopRepeating(); + captureSession.abortCaptures(); + Log.i(TAG, "sending capture request"); + captureSession.capture(stillBuilder.build(), captureCallback, backgroundHandler); + } catch (CameraAccessException e) { + dartMessenger.error(flutterResult, "cameraAccess", e.getMessage(), null); + } + } + + @SuppressWarnings("deprecation") + private Display getDefaultDisplay() { + return activity.getWindowManager().getDefaultDisplay(); + } + + /** Starts a background thread and its {@link Handler}. */ + public void startBackgroundThread() { + if (backgroundHandlerThread != null) { + return; + } + + backgroundHandlerThread = HandlerThreadFactory.create("CameraBackground"); + try { + backgroundHandlerThread.start(); + } catch (IllegalThreadStateException e) { + // Ignore exception in case the thread has already started. + } + backgroundHandler = HandlerFactory.create(backgroundHandlerThread.getLooper()); + } + + /** Stops the background thread and its {@link Handler}. */ + public void stopBackgroundThread() { + if (backgroundHandlerThread != null) { + backgroundHandlerThread.quitSafely(); + try { + backgroundHandlerThread.join(); + } catch (InterruptedException e) { + dartMessenger.error(flutterResult, "cameraAccess", e.getMessage(), null); + } + } + backgroundHandlerThread = null; + backgroundHandler = null; + } + + /** Start capturing a picture, doing autofocus first. */ + private void runPictureAutoFocus() { + Log.i(TAG, "runPictureAutoFocus"); + + cameraCaptureCallback.setCameraState(CameraState.STATE_WAITING_FOCUS); + lockAutoFocus(); + } + + private void lockAutoFocus() { + Log.i(TAG, "lockAutoFocus"); + if (captureSession == null) { + Log.i(TAG, "[unlockAutoFocus] captureSession null, returning"); + return; + } + + // Trigger AF to start. + previewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + + try { + captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler); + } catch (CameraAccessException e) { + dartMessenger.sendCameraErrorEvent(e.getMessage()); + } + } + + /** Cancel and reset auto focus state and refresh the preview session. */ + private void unlockAutoFocus() { + Log.i(TAG, "unlockAutoFocus"); + if (captureSession == null) { + Log.i(TAG, "[unlockAutoFocus] captureSession null, returning"); + return; + } + try { + // Cancel existing AF state. + previewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler); + + // Set AF state to idle again. + previewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + + captureSession.capture(previewRequestBuilder.build(), null, backgroundHandler); + } catch (CameraAccessException e) { + dartMessenger.sendCameraErrorEvent(e.getMessage()); + return; + } + + refreshPreviewCaptureSession( + null, + (errorCode, errorMessage) -> + dartMessenger.error(flutterResult, errorCode, errorMessage, null)); + } + + public void startVideoRecording(@NonNull Result result) { + final File outputDir = applicationContext.getCacheDir(); + try { + captureFile = File.createTempFile("REC", ".mp4", outputDir); + } catch (IOException | SecurityException e) { + result.error("cannotCreateFile", e.getMessage(), null); + return; + } + try { + prepareMediaRecorder(captureFile.getAbsolutePath()); + } catch (IOException e) { + recordingVideo = false; + captureFile = null; + result.error("videoRecordingFailed", e.getMessage(), null); + return; + } + // Re-create autofocus feature so it's using video focus mode now. + cameraFeatures.setAutoFocus( + cameraFeatureFactory.createAutoFocusFeature(cameraProperties, true)); + recordingVideo = true; + try { + createCaptureSession( + CameraDevice.TEMPLATE_RECORD, () -> mediaRecorder.start(), mediaRecorder.getSurface()); + result.success(null); + } catch (CameraAccessException e) { + recordingVideo = false; + captureFile = null; + result.error("videoRecordingFailed", e.getMessage(), null); + } + } + + public void stopVideoRecording(@NonNull final Result result) { + if (!recordingVideo) { + result.success(null); + return; + } + // Re-create autofocus feature so it's using continuous capture focus mode now. + cameraFeatures.setAutoFocus( + cameraFeatureFactory.createAutoFocusFeature(cameraProperties, false)); + recordingVideo = false; + try { + captureSession.abortCaptures(); + mediaRecorder.stop(); + } catch (CameraAccessException | IllegalStateException e) { + // Ignore exceptions and try to continue (changes are camera session already aborted capture). + } + mediaRecorder.reset(); + try { + startPreview(); + } catch (CameraAccessException | IllegalStateException e) { + result.error("videoRecordingFailed", e.getMessage(), null); + return; + } + result.success(captureFile.getAbsolutePath()); + captureFile = 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); + } + + /** + * Method handler for setting new flash modes. + * + * @param result Flutter result. + * @param newMode new mode. + */ + public void setFlashMode(@NonNull final Result result, @NonNull FlashMode newMode) { + // Save the new flash mode setting. + final FlashFeature flashFeature = cameraFeatures.getFlash(); + flashFeature.setValue(newMode); + flashFeature.updateBuilder(previewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setFlashModeFailed", "Could not set flash mode.", null)); + } + + /** + * Method handler for setting new exposure modes. + * + * @param result Flutter result. + * @param newMode new mode. + */ + public void setExposureMode(@NonNull final Result result, @NonNull ExposureMode newMode) { + final ExposureLockFeature exposureLockFeature = cameraFeatures.getExposureLock(); + exposureLockFeature.setValue(newMode); + exposureLockFeature.updateBuilder(previewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> + result.error("setExposureModeFailed", "Could not set exposure mode.", null)); + } + + /** + * Sets new exposure point from dart. + * + * @param result Flutter result. + * @param point The exposure point. + */ + public void setExposurePoint(@NonNull final Result result, @Nullable Point point) { + final ExposurePointFeature exposurePointFeature = cameraFeatures.getExposurePoint(); + exposurePointFeature.setValue(point); + exposurePointFeature.updateBuilder(previewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> + result.error("setExposurePointFailed", "Could not set exposure point.", null)); + } + + /** Return the max exposure offset value supported by the camera to dart. */ + public double getMaxExposureOffset() { + return cameraFeatures.getExposureOffset().getMaxExposureOffset(); + } + + /** Return the min exposure offset value supported by the camera to dart. */ + public double getMinExposureOffset() { + return cameraFeatures.getExposureOffset().getMinExposureOffset(); + } + + /** Return the exposure offset step size to dart. */ + public double getExposureOffsetStepSize() { + return cameraFeatures.getExposureOffset().getExposureOffsetStepSize(); + } + + /** + * Sets new focus mode from dart. + * + * @param result Flutter result. + * @param newMode New mode. + */ + public void setFocusMode(final Result result, @NonNull FocusMode newMode) { + final AutoFocusFeature autoFocusFeature = cameraFeatures.getAutoFocus(); + autoFocusFeature.setValue(newMode); + autoFocusFeature.updateBuilder(previewRequestBuilder); + + /* + * For focus mode an extra step of actually locking/unlocking the + * focus has to be done, in order to ensure it goes into the correct state. + */ + if (!pausedPreview) { + switch (newMode) { + case locked: + // Perform a single focus trigger. + if (captureSession == null) { + Log.i(TAG, "[unlockAutoFocus] captureSession null, returning"); + return; + } + lockAutoFocus(); + + // Set AF state to idle again. + previewRequestBuilder.set( + CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + + try { + captureSession.setRepeatingRequest( + previewRequestBuilder.build(), null, backgroundHandler); + } catch (CameraAccessException e) { + if (result != null) { + result.error( + "setFocusModeFailed", "Error setting focus mode: " + e.getMessage(), null); + } + return; + } + break; + case auto: + // Cancel current AF trigger and set AF to idle again. + unlockAutoFocus(); + break; + } + } + + if (result != null) { + result.success(null); + } + } + + /** + * Sets new focus point from dart. + * + * @param result Flutter result. + * @param point the new coordinates. + */ + public void setFocusPoint(@NonNull final Result result, @Nullable Point point) { + final FocusPointFeature focusPointFeature = cameraFeatures.getFocusPoint(); + focusPointFeature.setValue(point); + focusPointFeature.updateBuilder(previewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setFocusPointFailed", "Could not set focus point.", null)); + + this.setFocusMode(null, cameraFeatures.getAutoFocus().getValue()); + } + + /** + * Sets a new exposure offset from dart. From dart the offset comes as a double, like +1.3 or + * -1.3. + * + * @param result flutter result. + * @param offset new value. + */ + public void setExposureOffset(@NonNull final Result result, double offset) { + final ExposureOffsetFeature exposureOffsetFeature = cameraFeatures.getExposureOffset(); + exposureOffsetFeature.setValue(offset); + exposureOffsetFeature.updateBuilder(previewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(exposureOffsetFeature.getValue()), + (code, message) -> + result.error("setExposureOffsetFailed", "Could not set exposure offset.", null)); + } + + public float getMaxZoomLevel() { + return cameraFeatures.getZoomLevel().getMaximumZoomLevel(); + } + + public float getMinZoomLevel() { + return cameraFeatures.getZoomLevel().getMinimumZoomLevel(); + } + + /** Shortcut to get current recording profile. */ + CamcorderProfile getRecordingProfile() { + return cameraFeatures.getResolution().getRecordingProfile(); + } + + /** Shortut to get deviceOrientationListener. */ + DeviceOrientationManager getDeviceOrientationManager() { + return cameraFeatures.getSensorOrientation().getDeviceOrientationManager(); + } + + /** + * Sets zoom level from dart. + * + * @param result Flutter result. + * @param zoom new value. + */ + public void setZoomLevel(@NonNull final Result result, float zoom) throws CameraAccessException { + final ZoomLevelFeature zoomLevel = cameraFeatures.getZoomLevel(); + float maxZoom = zoomLevel.getMaximumZoomLevel(); + float minZoom = zoomLevel.getMinimumZoomLevel(); + + 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; + } + + zoomLevel.setValue(zoom); + zoomLevel.updateBuilder(previewRequestBuilder); + + refreshPreviewCaptureSession( + () -> result.success(null), + (code, message) -> result.error("setZoomLevelFailed", "Could not set zoom level.", null)); + } + + /** + * Lock capture orientation from dart. + * + * @param orientation new orientation. + */ + public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { + cameraFeatures.getSensorOrientation().lockCaptureOrientation(orientation); + } + + /** Unlock capture orientation from dart. */ + public void unlockCaptureOrientation() { + cameraFeatures.getSensorOrientation().unlockCaptureOrientation(); + } + + /** Pause the preview from dart. */ + public void pausePreview() throws CameraAccessException { + this.pausedPreview = true; + this.captureSession.stopRepeating(); + } + + /** Resume the preview from dart. */ + public void resumePreview() { + this.pausedPreview = false; + this.refreshPreviewCaptureSession( + null, (code, message) -> dartMessenger.sendCameraErrorEvent(message)); + } + + public void startPreview() throws CameraAccessException { + if (pictureImageReader == null || pictureImageReader.getSurface() == null) return; + Log.i(TAG, "startPreview"); + + createCaptureSession(CameraDevice.TEMPLATE_PREVIEW, pictureImageReader.getSurface()); + } + + public void startPreviewWithImageStream(EventChannel imageStreamChannel) + throws CameraAccessException { + createCaptureSession(CameraDevice.TEMPLATE_RECORD, imageStreamReader.getSurface()); + Log.i(TAG, "startPreviewWithImageStream"); + + 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, backgroundHandler); + } + }); + } + + /** + * This a callback object for the {@link ImageReader}. "onImageAvailable" will be called when a + * still image is ready to be saved. + */ + @Override + public void onImageAvailable(ImageReader reader) { + Log.i(TAG, "onImageAvailable"); + + backgroundHandler.post( + new ImageSaver( + // Use acquireNextImage since image reader is only for one image. + reader.acquireNextImage(), + captureFile, + new ImageSaver.Callback() { + @Override + public void onComplete(String absolutePath) { + dartMessenger.finish(flutterResult, absolutePath); + } + + @Override + public void onError(String errorCode, String errorMessage) { + dartMessenger.error(flutterResult, errorCode, errorMessage, null); + } + })); + cameraCaptureCallback.setCameraState(CameraState.STATE_PREVIEW); + } + + private void setImageStreamImageAvailableListener(final EventChannel.EventSink imageStreamSink) { + imageStreamReader.setOnImageAvailableListener( + reader -> { + Image img = reader.acquireNextImage(); + // Use acquireNextImage since image reader is only for one image. + 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); + imageBuffer.put("lensAperture", this.captureProps.getLastLensAperture()); + imageBuffer.put("sensorExposureTime", this.captureProps.getLastSensorExposureTime()); + Integer sensorSensitivity = this.captureProps.getLastSensorSensitivity(); + imageBuffer.put( + "sensorSensitivity", sensorSensitivity == null ? null : (double) sensorSensitivity); + + final Handler handler = new Handler(Looper.getMainLooper()); + handler.post(() -> imageStreamSink.success(imageBuffer)); + img.close(); + }, + backgroundHandler); + } + + private void closeCaptureSession() { + if (captureSession != null) { + Log.i(TAG, "closeCaptureSession"); + + captureSession.close(); + captureSession = null; + } + } + + public void close() { + Log.i(TAG, "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; + } + + stopBackgroundThread(); + } + + public void dispose() { + Log.i(TAG, "dispose"); + + close(); + flutterTexture.release(); + getDeviceOrientationManager().stop(); + } + + /** Factory class that assists in creating a {@link HandlerThread} instance. */ + static class HandlerThreadFactory { + /** + * Creates a new instance of the {@link HandlerThread} class. + * + *

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

      This method is visible for testing purposes only and should never be used outside this * + * class. + * + * @param looper to give to the Handler. + * @return new instance of the {@link Handler} class. + */ + @VisibleForTesting + public static Handler create(Looper looper) { + return new Handler(looper); + } + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java new file mode 100644 index 000000000000..805f18298958 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraCaptureCallback.java @@ -0,0 +1,183 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCaptureSession.CaptureCallback; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.TotalCaptureResult; +import android.util.Log; +import androidx.annotation.NonNull; +import io.flutter.plugins.camera.types.CameraCaptureProperties; +import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper; + +/** + * A callback object for tracking the progress of a {@link android.hardware.camera2.CaptureRequest} + * submitted to the camera device. + */ +class CameraCaptureCallback extends CaptureCallback { + private static final String TAG = "CameraCaptureCallback"; + private final CameraCaptureStateListener cameraStateListener; + private CameraState cameraState; + private final CaptureTimeoutsWrapper captureTimeouts; + private final CameraCaptureProperties captureProps; + + private CameraCaptureCallback( + @NonNull CameraCaptureStateListener cameraStateListener, + @NonNull CaptureTimeoutsWrapper captureTimeouts, + @NonNull CameraCaptureProperties captureProps) { + cameraState = CameraState.STATE_PREVIEW; + this.cameraStateListener = cameraStateListener; + this.captureTimeouts = captureTimeouts; + this.captureProps = captureProps; + } + + /** + * Creates a new instance of the {@link CameraCaptureCallback} class. + * + * @param cameraStateListener instance which will be called when the camera state changes. + * @param captureTimeouts specifying the different timeout counters that should be taken into + * account. + * @return a configured instance of the {@link CameraCaptureCallback} class. + */ + public static CameraCaptureCallback create( + @NonNull CameraCaptureStateListener cameraStateListener, + @NonNull CaptureTimeoutsWrapper captureTimeouts, + @NonNull CameraCaptureProperties captureProps) { + return new CameraCaptureCallback(cameraStateListener, captureTimeouts, captureProps); + } + + /** + * Gets the current {@link CameraState}. + * + * @return the current {@link CameraState}. + */ + public CameraState getCameraState() { + return cameraState; + } + + /** + * Sets the {@link CameraState}. + * + * @param state the camera is currently in. + */ + public void setCameraState(@NonNull CameraState state) { + cameraState = state; + } + + private void process(CaptureResult result) { + Integer aeState = result.get(CaptureResult.CONTROL_AE_STATE); + Integer afState = result.get(CaptureResult.CONTROL_AF_STATE); + + // Update capture properties + if (result instanceof TotalCaptureResult) { + Float lensAperture = result.get(CaptureResult.LENS_APERTURE); + Long sensorExposureTime = result.get(CaptureResult.SENSOR_EXPOSURE_TIME); + Integer sensorSensitivity = result.get(CaptureResult.SENSOR_SENSITIVITY); + this.captureProps.setLastLensAperture(lensAperture); + this.captureProps.setLastSensorExposureTime(sensorExposureTime); + this.captureProps.setLastSensorSensitivity(sensorSensitivity); + } + + if (cameraState != CameraState.STATE_PREVIEW) { + Log.d( + TAG, + "CameraCaptureCallback | state: " + + cameraState + + " | afState: " + + afState + + " | aeState: " + + aeState); + } + + switch (cameraState) { + case STATE_PREVIEW: + { + // We have nothing to do when the camera preview is working normally. + break; + } + case STATE_WAITING_FOCUS: + { + if (afState == null) { + return; + } else if (afState == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED + || afState == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) { + handleWaitingFocusState(aeState); + } else if (captureTimeouts.getPreCaptureFocusing().getIsExpired()) { + Log.w(TAG, "Focus timeout, moving on with capture"); + handleWaitingFocusState(aeState); + } + + break; + } + case STATE_WAITING_PRECAPTURE_START: + { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null + || aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED + || aeState == CaptureResult.CONTROL_AE_STATE_PRECAPTURE + || aeState == CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED) { + setCameraState(CameraState.STATE_WAITING_PRECAPTURE_DONE); + } else if (captureTimeouts.getPreCaptureMetering().getIsExpired()) { + Log.w(TAG, "Metering timeout waiting for pre-capture to start, moving on with capture"); + + setCameraState(CameraState.STATE_WAITING_PRECAPTURE_DONE); + } + break; + } + case STATE_WAITING_PRECAPTURE_DONE: + { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null || aeState != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) { + cameraStateListener.onConverged(); + } else if (captureTimeouts.getPreCaptureMetering().getIsExpired()) { + Log.w( + TAG, "Metering timeout waiting for pre-capture to finish, moving on with capture"); + cameraStateListener.onConverged(); + } + + break; + } + } + } + + private void handleWaitingFocusState(Integer aeState) { + // CONTROL_AE_STATE can be null on some devices + if (aeState == null || aeState == CaptureRequest.CONTROL_AE_STATE_CONVERGED) { + cameraStateListener.onConverged(); + } else { + cameraStateListener.onPrecapture(); + } + } + + @Override + public void onCaptureProgressed( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull CaptureResult partialResult) { + process(partialResult); + } + + @Override + public void onCaptureCompleted( + @NonNull CameraCaptureSession session, + @NonNull CaptureRequest request, + @NonNull TotalCaptureResult result) { + process(result); + } + + /** An interface that describes the different state changes implementers can be informed about. */ + interface CameraCaptureStateListener { + + /** Called when the {@link android.hardware.camera2.CaptureRequest} has been converged. */ + void onConverged(); + + /** + * Called when the {@link android.hardware.camera2.CaptureRequest} enters the pre-capture state. + */ + void onPrecapture(); + } +} 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 87% 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 c1c3fa893317..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; @@ -7,12 +11,12 @@ import androidx.annotation.VisibleForTesting; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; -import io.flutter.plugin.common.PluginRegistry; -import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener; final class CameraPermissions { interface PermissionsRegistry { - void addListener(RequestPermissionsResultListener handler); + @SuppressWarnings("deprecation") + void addListener( + io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener handler); } interface ResultCallback { @@ -61,8 +65,9 @@ private boolean hasAudioPermission(Activity activity) { } @VisibleForTesting + @SuppressWarnings("deprecation") static final class CameraRequestPermissionsListener - implements PluginRegistry.RequestPermissionsResultListener { + implements io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener { // There's no way to unregister permission listeners in the v1 embedding, so we'll be called // duplicate times in cases where the user denies and then grants a permission. Keep track of if 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 83% 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 2511898038bb..067ed0295e2e 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. @@ -12,7 +12,6 @@ import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.PluginRegistry.Registrar; import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry; import io.flutter.view.TextureRegistry; @@ -22,8 +21,8 @@ *

      Instantiate this in an add to app scenario to gracefully handle activity and context changes. * See {@code io.flutter.plugins.camera.MainActivity} for an example. * - *

      Call {@link #registerWith(Registrar)} to register an implementation of this that uses the - * stable {@code io.flutter.plugin.common} package. + *

      Call {@link #registerWith(io.flutter.plugin.common.PluginRegistry.Registrar)} to register an + * implementation of this that uses the stable {@code io.flutter.plugin.common} package. */ public final class CameraPlugin implements FlutterPlugin, ActivityAware { @@ -45,7 +44,8 @@ public CameraPlugin() {} *

      Calling this automatically initializes the plugin. However plugins initialized this way * won't react to changes in activity or context, unlike {@link CameraPlugin}. */ - public static void registerWith(Registrar registrar) { + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { CameraPlugin plugin = new CameraPlugin(); plugin.maybeStartListening( registrar.activity(), @@ -70,18 +70,16 @@ public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) { binding.getActivity(), flutterPluginBinding.getBinaryMessenger(), binding::addRequestPermissionsResultListener, - flutterPluginBinding.getFlutterEngine().getRenderer()); + flutterPluginBinding.getTextureRegistry()); } @Override public void onDetachedFromActivity() { - if (methodCallHandler == null) { - // Could be on too low of an SDK to have started listening originally. - return; + // Could be on too low of an SDK to have started listening originally. + if (methodCallHandler != null) { + methodCallHandler.stopListening(); + methodCallHandler = null; } - - methodCallHandler.stopListening(); - methodCallHandler = null; } @Override 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..95efebbf6488 --- /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/CameraRegionUtils.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java new file mode 100644 index 000000000000..951a2797d68f --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraRegionUtils.java @@ -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. + +package io.flutter.plugins.camera; + +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 androidx.annotation.VisibleForTesting; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import java.util.Arrays; + +/** + * Utility class offering functions to calculate values regarding the camera boundaries. + * + *

      The functions are used to calculate focus and exposure settings. + */ +public final class CameraRegionUtils { + + /** + * Obtains the boundaries for the currently active camera, that can be used for calculating + * MeteringRectangle instances required for setting focus or exposure settings. + * + * @param cameraProperties - Collection of the characteristics for the current camera device. + * @param requestBuilder - The request builder for the current capture request. + * @return The boundaries for the current camera device. + */ + public static Size getCameraBoundaries( + @NonNull CameraProperties cameraProperties, @NonNull CaptureRequest.Builder requestBuilder) { + 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(); + } + + return SizeFactory.create(rect.width(), rect.height()); + } else { + // No distortion correction support. + return cameraProperties.getSensorInfoPixelArraySize(); + } + } + + /** + * Converts a point into a {@link MeteringRectangle} with the supplied coordinates as the center + * 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 center point. + * + * @param boundaries - The camera boundaries to calculate the metering rectangle for. + * @param x x - 1 >= coordinate >= 0. + * @param y y - 1 >= coordinate >= 0. + * @return The dimensions of the metering rectangle based on the supplied coordinates and + * boundaries. + */ + public static MeteringRectangle convertPointToMeteringRectangle( + @NonNull Size boundaries, + double x, + double y, + @NonNull PlatformChannel.DeviceOrientation orientation) { + assert (boundaries.getWidth() > 0 && boundaries.getHeight() > 0); + assert (x >= 0 && x <= 1); + assert (y >= 0 && y <= 1); + // Rotate the coordinates to match the device orientation. + double oldX = x, oldY = y; + switch (orientation) { + case PORTRAIT_UP: // 90 ccw. + y = 1 - oldX; + x = oldY; + break; + case PORTRAIT_DOWN: // 90 cw. + x = 1 - oldY; + y = oldX; + break; + case LANDSCAPE_LEFT: + // No rotation required. + break; + case LANDSCAPE_RIGHT: // 180. + x = 1 - x; + y = 1 - y; + break; + } + // Interpolate the target coordinate. + int targetX = (int) Math.round(x * ((double) (boundaries.getWidth() - 1))); + int targetY = (int) Math.round(y * ((double) (boundaries.getHeight() - 1))); + // 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 MeteringRectangleFactory.create(targetX, targetY, targetWidth, targetHeight, 1); + } + + @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; + } + + /** Factory class that assists in creating a {@link MeteringRectangle} instance. */ + static class MeteringRectangleFactory { + /** + * Creates a new instance of the {@link MeteringRectangle} class. + * + *

      This method is visible for testing purposes only and should never be used outside this * + * class. + * + * @param x coordinate >= 0. + * @param y coordinate >= 0. + * @param width width >= 0. + * @param height height >= 0. + * @param meteringWeight weight between {@value MeteringRectangle#METERING_WEIGHT_MIN} and + * {@value MeteringRectangle#METERING_WEIGHT_MAX} inclusively. + * @return new instance of the {@link MeteringRectangle} class. + * @throws IllegalArgumentException if any of the parameters were negative. + */ + @VisibleForTesting + public static MeteringRectangle create( + int x, int y, int width, int height, int meteringWeight) { + return new MeteringRectangle(x, y, width, height, meteringWeight); + } + } + + /** Factory class that assists in creating a {@link Size} instance. */ + static class SizeFactory { + /** + * Creates a new instance of the {@link Size} class. + * + *

      This method is visible for testing purposes only and should never be used outside this * + * class. + * + * @param width width >= 0. + * @param height height >= 0. + * @return new instance of the {@link Size} class. + */ + @VisibleForTesting + public static Size create(int width, int height) { + return new Size(width, height); + } + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.java new file mode 100644 index 000000000000..ac48caf18ac6 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraState.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 io.flutter.plugins.camera; + +/** + * These are the states that the camera can be in. The camera can only take one photo at a time so + * this state describes the state of the camera itself. The camera works like a pipeline where we + * feed it requests through. It can only process one tasks at a time. + */ +public enum CameraState { + /** Idle, showing preview and not capturing anything. */ + STATE_PREVIEW, + + /** Starting and waiting for autofocus to complete. */ + STATE_WAITING_FOCUS, + + /** Start performing autoexposure. */ + STATE_WAITING_PRECAPTURE_START, + + /** waiting for autoexposure to complete. */ + STATE_WAITING_PRECAPTURE_DONE, + + /** Capturing an image. */ + STATE_CAPTURING, +} 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..11b6eeaa5b50 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/CameraUtils.java @@ -0,0 +1,132 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import android.app.Activity; +import android.content.Context; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CameraMetadata; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** Provides various utilities for camera. */ +public final class CameraUtils { + + private CameraUtils() {} + + /** + * Gets the {@link CameraManager} singleton. + * + * @param context The context to get the {@link CameraManager} singleton from. + * @return The {@link CameraManager} singleton. + */ + static CameraManager getCameraManager(Context context) { + return (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); + } + + /** + * Serializes the {@link PlatformChannel.DeviceOrientation} to a string value. + * + * @param orientation The orientation to serialize. + * @return The serialized orientation. + * @throws UnsupportedOperationException when the provided orientation not have a corresponding + * string value. + */ + 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()); + } + } + + /** + * Deserializes a string value to its corresponding {@link PlatformChannel.DeviceOrientation} + * value. + * + * @param orientation The string value to deserialize. + * @return The deserialized orientation. + * @throws UnsupportedOperationException when the provided string value does not have a + * corresponding {@link PlatformChannel.DeviceOrientation}. + */ + 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); + } + } + + /** + * Gets all the available cameras for the device. + * + * @param activity The current Android activity. + * @return A map of all the available cameras, with their name as their key. + * @throws CameraAccessException when the camera could not be accessed. + */ + 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) { + int cameraId; + try { + cameraId = Integer.parseInt(cameraName, 10); + } catch (NumberFormatException e) { + cameraId = -1; + } + if (cameraId < 0) { + continue; + } + + HashMap details = new HashMap<>(); + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraName); + details.put("name", cameraName); + 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; + } +} 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..dc62fce524d3 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/DartMessenger.java @@ -0,0 +1,205 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import 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.features.autofocus.FocusMode; +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; +import java.util.HashMap; +import java.util.Map; + +/** Utility class that facilitates communication to the Flutter client */ +public class DartMessenger { + @NonNull private final Handler handler; + @Nullable private MethodChannel cameraChannel; + @Nullable private MethodChannel deviceChannel; + + /** Specifies the different device related message types. */ + enum DeviceEventType { + /** Indicates the device's orientation has changed. */ + ORIENTATION_CHANGED("orientation_changed"); + private final String method; + + DeviceEventType(String method) { + this.method = method; + } + } + + /** Specifies the different camera related message types. */ + enum CameraEventType { + /** Indicates that an error occurred while interacting with the camera. */ + ERROR("error"), + /** Indicates that the camera is closing. */ + CLOSING("camera_closing"), + /** Indicates that the camera is initialized. */ + INITIALIZED("initialized"); + + private final String method; + + /** + * Converts the supplied method name to the matching {@link CameraEventType}. + * + * @param method name to be converted into a {@link CameraEventType}. + */ + CameraEventType(String method) { + this.method = method; + } + } + + /** + * Creates a new instance of the {@link DartMessenger} class. + * + * @param messenger is the {@link BinaryMessenger} that is used to communicate with Flutter. + * @param cameraId identifies the camera which is the source of the communication. + * @param handler the handler used to manage the thread's message queue. This should always be a + * handler managing the main thread since communication with Flutter should always happen on + * the main thread. The handler is mainly supplied so it will be easier test this class. + */ + DartMessenger(BinaryMessenger messenger, long cameraId, @NonNull Handler handler) { + cameraChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/camera" + cameraId); + deviceChannel = new MethodChannel(messenger, "flutter.io/cameraPlugin/device"); + this.handler = handler; + } + + /** + * Sends a message to the Flutter client informing the orientation of the device has been changed. + * + * @param orientation specifies the new orientation of the device. + */ + public void sendDeviceOrientationChangeEvent(PlatformChannel.DeviceOrientation orientation) { + assert (orientation != null); + this.send( + DeviceEventType.ORIENTATION_CHANGED, + new HashMap() { + { + put("orientation", CameraUtils.serializeDeviceOrientation(orientation)); + } + }); + } + + /** + * Sends a message to the Flutter client informing that the camera has been initialized. + * + * @param previewWidth describes the preview width that is supported by the camera. + * @param previewHeight describes the preview height that is supported by the camera. + * @param exposureMode describes the current exposure mode that is set on the camera. + * @param focusMode describes the current focus mode that is set on the camera. + * @param exposurePointSupported indicates if the camera supports setting an exposure point. + * @param focusPointSupported indicates if the camera supports setting a focus point. + */ + 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); + } + }); + } + + /** Sends a message to the Flutter client informing that the camera is closing. */ + void sendCameraClosingEvent() { + send(CameraEventType.CLOSING); + } + + /** + * Sends a message to the Flutter client informing that an error occurred while interacting with + * the camera. + * + * @param description contains details regarding the error that occurred. + */ + void sendCameraErrorEvent(@Nullable String description) { + this.send( + CameraEventType.ERROR, + new HashMap() { + { + if (!TextUtils.isEmpty(description)) put("description", description); + } + }); + } + + private void send(CameraEventType eventType) { + send(eventType, new HashMap<>()); + } + + private void send(CameraEventType eventType, Map args) { + if (cameraChannel == null) { + return; + } + + handler.post( + new Runnable() { + @Override + public void run() { + cameraChannel.invokeMethod(eventType.method, args); + } + }); + } + + private void send(DeviceEventType eventType) { + send(eventType, new HashMap<>()); + } + + private void send(DeviceEventType eventType, Map args) { + if (deviceChannel == null) { + return; + } + + handler.post( + new Runnable() { + @Override + public void run() { + deviceChannel.invokeMethod(eventType.method, args); + } + }); + } + + /** + * Send a success payload to a {@link MethodChannel.Result} on the main thread. + * + * @param payload The payload to send. + */ + public void finish(MethodChannel.Result result, Object payload) { + handler.post(() -> result.success(payload)); + } + + /** + * Send an error payload to a {@link MethodChannel.Result} on the main thread. + * + * @param errorCode error code. + * @param errorMessage error message. + * @param errorDetails error details. + */ + public void error( + MethodChannel.Result result, + String errorCode, + @Nullable String errorMessage, + @Nullable Object errorDetails) { + handler.post(() -> result.error(errorCode, errorMessage, errorDetails)); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java new file mode 100644 index 000000000000..821c9a50c13f --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/ImageSaver.java @@ -0,0 +1,105 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import android.media.Image; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; + +/** Saves a JPEG {@link Image} into the specified {@link File}. */ +public class ImageSaver implements Runnable { + + /** The JPEG image */ + private final Image image; + + /** The file we save the image into. */ + private final File file; + + /** Used to report the status of the save action. */ + private final Callback callback; + + /** + * Creates an instance of the ImageSaver runnable + * + * @param image - The image to save + * @param file - The file to save the image to + * @param callback - The callback that is run on completion, or when an error is encountered. + */ + ImageSaver(@NonNull Image image, @NonNull File file, @NonNull Callback callback) { + this.image = image; + this.file = file; + this.callback = callback; + } + + @Override + public void run() { + ByteBuffer buffer = image.getPlanes()[0].getBuffer(); + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + FileOutputStream output = null; + try { + output = FileOutputStreamFactory.create(file); + output.write(bytes); + + callback.onComplete(file.getAbsolutePath()); + + } catch (IOException e) { + callback.onError("IOError", "Failed saving image"); + } finally { + image.close(); + if (null != output) { + try { + output.close(); + } catch (IOException e) { + callback.onError("cameraAccess", e.getMessage()); + } + } + } + } + + /** + * The interface for the callback that is passed to ImageSaver, for detecting completion or + * failure of the image saving task. + */ + public interface Callback { + /** + * Called when the image file has been saved successfully. + * + * @param absolutePath - The absolute path of the file that was saved. + */ + void onComplete(String absolutePath); + + /** + * Called when an error is encountered while saving the image file. + * + * @param errorCode - The error code. + * @param errorMessage - The human readable error message. + */ + void onError(String errorCode, String errorMessage); + } + + /** Factory class that assists in creating a {@link FileOutputStream} instance. */ + static class FileOutputStreamFactory { + /** + * Creates a new instance of the {@link FileOutputStream} class. + * + *

      This method is visible for testing purposes only and should never be used outside this * + * class. + * + * @param file - The file to create the output stream for + * @return new instance of the {@link FileOutputStream} class. + * @throws FileNotFoundException when the supplied file could not be found. + */ + @VisibleForTesting + public static FileOutputStream create(File file) throws FileNotFoundException { + return new FileOutputStream(file); + } + } +} 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..35cc2b081bae --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/MethodCallHandlerImpl.java @@ -0,0 +1,413 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import android.app.Activity; +import android.hardware.camera2.CameraAccessException; +import android.os.Handler; +import android.os.Looper; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.EventChannel; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugins.camera.CameraPermissions.PermissionsRegistry; +import io.flutter.plugins.camera.features.CameraFeatureFactoryImpl; +import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.autofocus.FocusMode; +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; +import io.flutter.plugins.camera.features.flash.FlashMode; +import io.flutter.plugins.camera.features.resolution.ResolutionPreset; +import io.flutter.view.TextureRegistry; +import java.util.HashMap; +import java.util.Map; + +final class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { + private final Activity activity; + private final BinaryMessenger messenger; + private final CameraPermissions cameraPermissions; + private final PermissionsRegistry permissionsRegistry; + private final TextureRegistry textureRegistry; + private final MethodChannel methodChannel; + private final EventChannel imageStreamChannel; + private @Nullable Camera camera; + + MethodCallHandlerImpl( + Activity activity, + BinaryMessenger messenger, + CameraPermissions cameraPermissions, + PermissionsRegistry permissionsAdder, + TextureRegistry textureRegistry) { + this.activity = activity; + this.messenger = messenger; + this.cameraPermissions = cameraPermissions; + this.permissionsRegistry = permissionsAdder; + this.textureRegistry = textureRegistry; + + methodChannel = new MethodChannel(messenger, "plugins.flutter.io/camera"); + imageStreamChannel = new EventChannel(messenger, "plugins.flutter.io/camera/imageStream"); + methodChannel.setMethodCallHandler(this); + } + + @Override + public void onMethodCall(@NonNull MethodCall call, @NonNull final Result result) { + switch (call.method) { + case "availableCameras": + try { + result.success(CameraUtils.getAvailableCameras(activity)); + } catch (Exception e) { + handleException(e, result); + } + break; + case "create": + { + if (camera != null) { + camera.close(); + } + + cameraPermissions.requestPermissions( + activity, + permissionsRegistry, + call.argument("enableAudio"), + (String errCode, String errDesc) -> { + if (errCode == null) { + try { + instantiateCamera(call, result); + } catch (Exception e) { + handleException(e, result); + } + } else { + result.error(errCode, errDesc, null); + } + }); + break; + } + case "initialize": + { + if (camera != null) { + try { + camera.open(call.argument("imageFormatGroup")); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + } else { + result.error( + "cameraNotFound", + "Camera not found. Please call the 'create' method before calling 'initialize'.", + null); + } + break; + } + case "takePicture": + { + camera.takePicture(result); + break; + } + case "prepareForVideoRecording": + { + // This optimization is not required for Android. + result.success(null); + break; + } + case "startVideoRecording": + { + camera.startVideoRecording(result); + break; + } + case "stopVideoRecording": + { + camera.stopVideoRecording(result); + break; + } + case "pauseVideoRecording": + { + camera.pauseVideoRecording(result); + break; + } + case "resumeVideoRecording": + { + camera.resumeVideoRecording(result); + break; + } + case "setFlashMode": + { + String modeStr = call.argument("mode"); + FlashMode mode = FlashMode.getValueForString(modeStr); + if (mode == null) { + result.error("setFlashModeFailed", "Unknown flash mode " + modeStr, null); + return; + } + try { + camera.setFlashMode(result, mode); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setExposureMode": + { + String modeStr = call.argument("mode"); + ExposureMode mode = ExposureMode.getValueForString(modeStr); + if (mode == null) { + result.error("setExposureModeFailed", "Unknown exposure mode " + modeStr, null); + return; + } + try { + camera.setExposureMode(result, mode); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setExposurePoint": + { + Boolean reset = call.argument("reset"); + Double x = null; + Double y = null; + if (reset == null || !reset) { + x = call.argument("x"); + y = call.argument("y"); + } + try { + camera.setExposurePoint(result, new Point(x, y)); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getMinExposureOffset": + { + try { + result.success(camera.getMinExposureOffset()); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getMaxExposureOffset": + { + try { + result.success(camera.getMaxExposureOffset()); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getExposureOffsetStepSize": + { + try { + result.success(camera.getExposureOffsetStepSize()); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setExposureOffset": + { + try { + camera.setExposureOffset(result, call.argument("offset")); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setFocusMode": + { + String modeStr = call.argument("mode"); + FocusMode mode = FocusMode.getValueForString(modeStr); + if (mode == null) { + result.error("setFocusModeFailed", "Unknown focus mode " + modeStr, null); + return; + } + try { + camera.setFocusMode(result, mode); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setFocusPoint": + { + Boolean reset = call.argument("reset"); + Double x = null; + Double y = null; + if (reset == null || !reset) { + x = call.argument("x"); + y = call.argument("y"); + } + try { + camera.setFocusPoint(result, new Point(x, y)); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "startImageStream": + { + try { + camera.startPreviewWithImageStream(imageStreamChannel); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "stopImageStream": + { + try { + camera.startPreview(); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getMaxZoomLevel": + { + assert camera != null; + + try { + float maxZoomLevel = camera.getMaxZoomLevel(); + result.success(maxZoomLevel); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "getMinZoomLevel": + { + assert camera != null; + + try { + float minZoomLevel = camera.getMinZoomLevel(); + result.success(minZoomLevel); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "setZoomLevel": + { + assert camera != null; + + Double zoom = call.argument("zoom"); + + if (zoom == null) { + result.error( + "ZOOM_ERROR", "setZoomLevel is called without specifying a zoom level.", null); + return; + } + + try { + camera.setZoomLevel(result, zoom.floatValue()); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "lockCaptureOrientation": + { + PlatformChannel.DeviceOrientation orientation = + CameraUtils.deserializeDeviceOrientation(call.argument("orientation")); + + try { + camera.lockCaptureOrientation(orientation); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "unlockCaptureOrientation": + { + try { + camera.unlockCaptureOrientation(); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "pausePreview": + { + try { + camera.pausePreview(); + result.success(null); + } catch (Exception e) { + handleException(e, result); + } + break; + } + case "resumePreview": + { + camera.resumePreview(); + result.success(null); + break; + } + case "dispose": + { + if (camera != null) { + camera.dispose(); + } + result.success(null); + break; + } + default: + result.notImplemented(); + break; + } + } + + void stopListening() { + methodChannel.setMethodCallHandler(null); + } + + private void instantiateCamera(MethodCall call, Result result) throws CameraAccessException { + String cameraName = call.argument("cameraName"); + String preset = call.argument("resolutionPreset"); + boolean enableAudio = call.argument("enableAudio"); + + TextureRegistry.SurfaceTextureEntry flutterSurfaceTexture = + textureRegistry.createSurfaceTexture(); + DartMessenger dartMessenger = + new DartMessenger( + messenger, flutterSurfaceTexture.id(), new Handler(Looper.getMainLooper())); + CameraProperties cameraProperties = + new CameraPropertiesImpl(cameraName, CameraUtils.getCameraManager(activity)); + ResolutionPreset resolutionPreset = ResolutionPreset.valueOf(preset); + + camera = + new Camera( + activity, + flutterSurfaceTexture, + new CameraFeatureFactoryImpl(), + dartMessenger, + cameraProperties, + resolutionPreset, + enableAudio); + + Map reply = new HashMap<>(); + reply.put("cameraId", flutterSurfaceTexture.id()); + result.success(reply); + } + + // We move catching CameraAccessException out of onMethodCall because it causes a crash + // on plugin registration for sdks incompatible with Camera2 (< 21). We want this plugin to + // to be able to compile with <21 sdks for apps that want the camera and support earlier version. + @SuppressWarnings("ConstantConditions") + private void handleException(Exception exception, Result result) { + if (exception instanceof CameraAccessException) { + result.error("CameraAccess", exception.getMessage(), null); + return; + } + + // CameraAccessException can not be cast to a RuntimeException. + throw (RuntimeException) exception; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeature.java b/packages/camera/camera/android/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/CameraFeatureFactory.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java new file mode 100644 index 000000000000..b91f9a1c03f7 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactory.java @@ -0,0 +1,149 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.features; + +import android.app.Activity; +import androidx.annotation.NonNull; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.DartMessenger; +import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; +import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature; +import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature; +import io.flutter.plugins.camera.features.exposurepoint.ExposurePointFeature; +import io.flutter.plugins.camera.features.flash.FlashFeature; +import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature; +import io.flutter.plugins.camera.features.fpsrange.FpsRangeFeature; +import io.flutter.plugins.camera.features.noisereduction.NoiseReductionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionPreset; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; +import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; + +/** + * Factory for creating the supported feature implementation controlling different aspects of the + * {@link android.hardware.camera2.CaptureRequest}. + */ +public interface CameraFeatureFactory { + + /** + * Creates a new instance of the auto focus feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. + * @param recordingVideo indicates if the camera is currently recording. + * @return newly created instance of the AutoFocusFeature class. + */ + AutoFocusFeature createAutoFocusFeature( + @NonNull CameraProperties cameraProperties, boolean recordingVideo); + + /** + * Creates a new instance of the exposure lock feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. + * @return newly created instance of the ExposureLockFeature class. + */ + ExposureLockFeature createExposureLockFeature(@NonNull CameraProperties cameraProperties); + + /** + * Creates a new instance of the exposure offset feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. + * @return newly created instance of the ExposureOffsetFeature class. + */ + ExposureOffsetFeature createExposureOffsetFeature(@NonNull CameraProperties cameraProperties); + + /** + * Creates a new instance of the flash feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. + * @return newly created instance of the FlashFeature class. + */ + FlashFeature createFlashFeature(@NonNull CameraProperties cameraProperties); + + /** + * Creates a new instance of the resolution feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. + * @param initialSetting initial resolution preset. + * @param cameraName the name of the camera which can be used to identify the camera device. + * @return newly created instance of the ResolutionFeature class. + */ + ResolutionFeature createResolutionFeature( + @NonNull CameraProperties cameraProperties, + ResolutionPreset initialSetting, + String cameraName); + + /** + * Creates a new instance of the focus point feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. + * @param sensorOrientationFeature instance of the SensorOrientationFeature class containing + * information about the sensor and device orientation. + * @return newly created instance of the FocusPointFeature class. + */ + FocusPointFeature createFocusPointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature); + + /** + * Creates a new instance of the FPS range feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. + * @return newly created instance of the FpsRangeFeature class. + */ + FpsRangeFeature createFpsRangeFeature(@NonNull CameraProperties cameraProperties); + + /** + * Creates a new instance of the sensor orientation feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. + * @param activity current activity associated with the camera plugin. + * @param dartMessenger instance of the DartMessenger class, used to send state updates back to + * Dart. + * @return newly created instance of the SensorOrientationFeature class. + */ + SensorOrientationFeature createSensorOrientationFeature( + @NonNull CameraProperties cameraProperties, + @NonNull Activity activity, + @NonNull DartMessenger dartMessenger); + + /** + * Creates a new instance of the zoom level feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. + * @return newly created instance of the ZoomLevelFeature class. + */ + ZoomLevelFeature createZoomLevelFeature(@NonNull CameraProperties cameraProperties); + + /** + * Creates a new instance of the exposure point feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. + * @param sensorOrientationFeature instance of the SensorOrientationFeature class containing + * information about the sensor and device orientation. + * @return newly created instance of the ExposurePointFeature class. + */ + ExposurePointFeature createExposurePointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature); + + /** + * Creates a new instance of the noise reduction feature. + * + * @param cameraProperties instance of the CameraProperties class containing information about the + * cameras features. + * @return newly created instance of the NoiseReductionFeature class. + */ + NoiseReductionFeature createNoiseReductionFeature(@NonNull CameraProperties cameraProperties); +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java new file mode 100644 index 000000000000..95a8c06caa0a --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatureFactoryImpl.java @@ -0,0 +1,98 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.features; + +import android.app.Activity; +import androidx.annotation.NonNull; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.DartMessenger; +import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; +import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature; +import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature; +import io.flutter.plugins.camera.features.exposurepoint.ExposurePointFeature; +import io.flutter.plugins.camera.features.flash.FlashFeature; +import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature; +import io.flutter.plugins.camera.features.fpsrange.FpsRangeFeature; +import io.flutter.plugins.camera.features.noisereduction.NoiseReductionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionPreset; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; +import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; + +/** + * Implementation of the {@link CameraFeatureFactory} interface creating the supported feature + * implementation controlling different aspects of the {@link + * android.hardware.camera2.CaptureRequest}. + */ +public class CameraFeatureFactoryImpl implements CameraFeatureFactory { + + @Override + public AutoFocusFeature createAutoFocusFeature( + @NonNull CameraProperties cameraProperties, boolean recordingVideo) { + return new AutoFocusFeature(cameraProperties, recordingVideo); + } + + @Override + public ExposureLockFeature createExposureLockFeature(@NonNull CameraProperties cameraProperties) { + return new ExposureLockFeature(cameraProperties); + } + + @Override + public ExposureOffsetFeature createExposureOffsetFeature( + @NonNull CameraProperties cameraProperties) { + return new ExposureOffsetFeature(cameraProperties); + } + + @Override + public FlashFeature createFlashFeature(@NonNull CameraProperties cameraProperties) { + return new FlashFeature(cameraProperties); + } + + @Override + public ResolutionFeature createResolutionFeature( + @NonNull CameraProperties cameraProperties, + ResolutionPreset initialSetting, + String cameraName) { + return new ResolutionFeature(cameraProperties, initialSetting, cameraName); + } + + @Override + public FocusPointFeature createFocusPointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature) { + return new FocusPointFeature(cameraProperties, sensorOrientationFeature); + } + + @Override + public FpsRangeFeature createFpsRangeFeature(@NonNull CameraProperties cameraProperties) { + return new FpsRangeFeature(cameraProperties); + } + + @Override + public SensorOrientationFeature createSensorOrientationFeature( + @NonNull CameraProperties cameraProperties, + @NonNull Activity activity, + @NonNull DartMessenger dartMessenger) { + return new SensorOrientationFeature(cameraProperties, activity, dartMessenger); + } + + @Override + public ZoomLevelFeature createZoomLevelFeature(@NonNull CameraProperties cameraProperties) { + return new ZoomLevelFeature(cameraProperties); + } + + @Override + public ExposurePointFeature createExposurePointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature) { + return new ExposurePointFeature(cameraProperties, sensorOrientationFeature); + } + + @Override + public NoiseReductionFeature createNoiseReductionFeature( + @NonNull CameraProperties cameraProperties) { + return new NoiseReductionFeature(cameraProperties); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java new file mode 100644 index 000000000000..659fd15963e9 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/CameraFeatures.java @@ -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. + +package io.flutter.plugins.camera.features; + +import android.app.Activity; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.DartMessenger; +import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; +import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature; +import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature; +import io.flutter.plugins.camera.features.exposurepoint.ExposurePointFeature; +import io.flutter.plugins.camera.features.flash.FlashFeature; +import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature; +import io.flutter.plugins.camera.features.fpsrange.FpsRangeFeature; +import io.flutter.plugins.camera.features.noisereduction.NoiseReductionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionPreset; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; +import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * These are all of our available features in the camera. Used in the Camera to access all features + * in a simpler way. + */ +public class CameraFeatures { + private static final String AUTO_FOCUS = "AUTO_FOCUS"; + private static final String EXPOSURE_LOCK = "EXPOSURE_LOCK"; + private static final String EXPOSURE_OFFSET = "EXPOSURE_OFFSET"; + private static final String EXPOSURE_POINT = "EXPOSURE_POINT"; + private static final String FLASH = "FLASH"; + private static final String FOCUS_POINT = "FOCUS_POINT"; + private static final String FPS_RANGE = "FPS_RANGE"; + private static final String NOISE_REDUCTION = "NOISE_REDUCTION"; + private static final String REGION_BOUNDARIES = "REGION_BOUNDARIES"; + private static final String RESOLUTION = "RESOLUTION"; + private static final String SENSOR_ORIENTATION = "SENSOR_ORIENTATION"; + private static final String ZOOM_LEVEL = "ZOOM_LEVEL"; + + public static CameraFeatures init( + CameraFeatureFactory cameraFeatureFactory, + CameraProperties cameraProperties, + Activity activity, + DartMessenger dartMessenger, + ResolutionPreset resolutionPreset) { + CameraFeatures cameraFeatures = new CameraFeatures(); + cameraFeatures.setAutoFocus( + cameraFeatureFactory.createAutoFocusFeature(cameraProperties, false)); + cameraFeatures.setExposureLock( + cameraFeatureFactory.createExposureLockFeature(cameraProperties)); + cameraFeatures.setExposureOffset( + cameraFeatureFactory.createExposureOffsetFeature(cameraProperties)); + SensorOrientationFeature sensorOrientationFeature = + cameraFeatureFactory.createSensorOrientationFeature( + cameraProperties, activity, dartMessenger); + cameraFeatures.setSensorOrientation(sensorOrientationFeature); + cameraFeatures.setExposurePoint( + cameraFeatureFactory.createExposurePointFeature( + cameraProperties, sensorOrientationFeature)); + cameraFeatures.setFlash(cameraFeatureFactory.createFlashFeature(cameraProperties)); + cameraFeatures.setFocusPoint( + cameraFeatureFactory.createFocusPointFeature(cameraProperties, sensorOrientationFeature)); + cameraFeatures.setFpsRange(cameraFeatureFactory.createFpsRangeFeature(cameraProperties)); + cameraFeatures.setNoiseReduction( + cameraFeatureFactory.createNoiseReductionFeature(cameraProperties)); + cameraFeatures.setResolution( + cameraFeatureFactory.createResolutionFeature( + cameraProperties, resolutionPreset, cameraProperties.getCameraName())); + cameraFeatures.setZoomLevel(cameraFeatureFactory.createZoomLevelFeature(cameraProperties)); + return cameraFeatures; + } + + private Map featureMap = new HashMap<>(); + + /** + * Gets a collection of all features that have been set. + * + * @return A collection of all features that have been set. + */ + public Collection getAllFeatures() { + return this.featureMap.values(); + } + + /** + * Gets the auto focus feature if it has been set. + * + * @return the auto focus feature. + */ + public AutoFocusFeature getAutoFocus() { + return (AutoFocusFeature) featureMap.get(AUTO_FOCUS); + } + + /** + * Sets the instance of the auto focus feature. + * + * @param autoFocus the {@link AutoFocusFeature} instance to set. + */ + public void setAutoFocus(AutoFocusFeature autoFocus) { + this.featureMap.put(AUTO_FOCUS, autoFocus); + } + + /** + * Gets the exposure lock feature if it has been set. + * + * @return the exposure lock feature. + */ + public ExposureLockFeature getExposureLock() { + return (ExposureLockFeature) featureMap.get(EXPOSURE_LOCK); + } + + /** + * Sets the instance of the exposure lock feature. + * + * @param exposureLock the {@link ExposureLockFeature} instance to set. + */ + public void setExposureLock(ExposureLockFeature exposureLock) { + this.featureMap.put(EXPOSURE_LOCK, exposureLock); + } + + /** + * Gets the exposure offset feature if it has been set. + * + * @return the exposure offset feature. + */ + public ExposureOffsetFeature getExposureOffset() { + return (ExposureOffsetFeature) featureMap.get(EXPOSURE_OFFSET); + } + + /** + * Sets the instance of the exposure offset feature. + * + * @param exposureOffset the {@link ExposureOffsetFeature} instance to set. + */ + public void setExposureOffset(ExposureOffsetFeature exposureOffset) { + this.featureMap.put(EXPOSURE_OFFSET, exposureOffset); + } + + /** + * Gets the exposure point feature if it has been set. + * + * @return the exposure point feature. + */ + public ExposurePointFeature getExposurePoint() { + return (ExposurePointFeature) featureMap.get(EXPOSURE_POINT); + } + + /** + * Sets the instance of the exposure point feature. + * + * @param exposurePoint the {@link ExposurePointFeature} instance to set. + */ + public void setExposurePoint(ExposurePointFeature exposurePoint) { + this.featureMap.put(EXPOSURE_POINT, exposurePoint); + } + + /** + * Gets the flash feature if it has been set. + * + * @return the flash feature. + */ + public FlashFeature getFlash() { + return (FlashFeature) featureMap.get(FLASH); + } + + /** + * Sets the instance of the flash feature. + * + * @param flash the {@link FlashFeature} instance to set. + */ + public void setFlash(FlashFeature flash) { + this.featureMap.put(FLASH, flash); + } + + /** + * Gets the focus point feature if it has been set. + * + * @return the focus point feature. + */ + public FocusPointFeature getFocusPoint() { + return (FocusPointFeature) featureMap.get(FOCUS_POINT); + } + + /** + * Sets the instance of the focus point feature. + * + * @param focusPoint the {@link FocusPointFeature} instance to set. + */ + public void setFocusPoint(FocusPointFeature focusPoint) { + this.featureMap.put(FOCUS_POINT, focusPoint); + } + + /** + * Gets the fps range feature if it has been set. + * + * @return the fps range feature. + */ + public FpsRangeFeature getFpsRange() { + return (FpsRangeFeature) featureMap.get(FPS_RANGE); + } + + /** + * Sets the instance of the fps range feature. + * + * @param fpsRange the {@link FpsRangeFeature} instance to set. + */ + public void setFpsRange(FpsRangeFeature fpsRange) { + this.featureMap.put(FPS_RANGE, fpsRange); + } + + /** + * Gets the noise reduction feature if it has been set. + * + * @return the noise reduction feature. + */ + public NoiseReductionFeature getNoiseReduction() { + return (NoiseReductionFeature) featureMap.get(NOISE_REDUCTION); + } + + /** + * Sets the instance of the noise reduction feature. + * + * @param noiseReduction the {@link NoiseReductionFeature} instance to set. + */ + public void setNoiseReduction(NoiseReductionFeature noiseReduction) { + this.featureMap.put(NOISE_REDUCTION, noiseReduction); + } + + /** + * Gets the resolution feature if it has been set. + * + * @return the resolution feature. + */ + public ResolutionFeature getResolution() { + return (ResolutionFeature) featureMap.get(RESOLUTION); + } + + /** + * Sets the instance of the resolution feature. + * + * @param resolution the {@link ResolutionFeature} instance to set. + */ + public void setResolution(ResolutionFeature resolution) { + this.featureMap.put(RESOLUTION, resolution); + } + + /** + * Gets the sensor orientation feature if it has been set. + * + * @return the sensor orientation feature. + */ + public SensorOrientationFeature getSensorOrientation() { + return (SensorOrientationFeature) featureMap.get(SENSOR_ORIENTATION); + } + + /** + * Sets the instance of the sensor orientation feature. + * + * @param sensorOrientation the {@link SensorOrientationFeature} instance to set. + */ + public void setSensorOrientation(SensorOrientationFeature sensorOrientation) { + this.featureMap.put(SENSOR_ORIENTATION, sensorOrientation); + } + + /** + * Gets the zoom level feature if it has been set. + * + * @return the zoom level feature. + */ + public ZoomLevelFeature getZoomLevel() { + return (ZoomLevelFeature) featureMap.get(ZOOM_LEVEL); + } + + /** + * Sets the instance of the zoom level feature. + * + * @param zoomLevel the {@link ZoomLevelFeature} instance to set. + */ + public void setZoomLevel(ZoomLevelFeature zoomLevel) { + this.featureMap.put(ZOOM_LEVEL, zoomLevel); + } +} 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..336e756e9ed8 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeature.java @@ -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. + +package io.flutter.plugins.camera.features.exposurepoint; + +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.MeteringRectangle; +import android.util.Size; +import androidx.annotation.NonNull; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.CameraRegionUtils; +import io.flutter.plugins.camera.features.CameraFeature; +import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; + +/** Exposure point controls where in the frame exposure metering will come from. */ +public class ExposurePointFeature extends CameraFeature { + + private Size cameraBoundaries; + private Point exposurePoint; + private MeteringRectangle exposureRectangle; + private final SensorOrientationFeature sensorOrientationFeature; + + /** + * Creates a new instance of the {@link ExposurePointFeature}. + * + * @param cameraProperties Collection of the characteristics for the current camera device. + */ + public ExposurePointFeature( + CameraProperties cameraProperties, SensorOrientationFeature sensorOrientationFeature) { + super(cameraProperties); + this.sensorOrientationFeature = sensorOrientationFeature; + } + + /** + * Sets the camera boundaries that are required for the exposure point feature to function. + * + * @param cameraBoundaries - The camera boundaries to set. + */ + public void setCameraBoundaries(@NonNull Size cameraBoundaries) { + this.cameraBoundaries = cameraBoundaries; + this.buildExposureRectangle(); + } + + @Override + public String getDebugName() { + return "ExposurePointFeature"; + } + + @Override + public Point getValue() { + return exposurePoint; + } + + @Override + public void setValue(Point value) { + this.exposurePoint = (value == null || value.x == null || value.y == null) ? null : value; + this.buildExposureRectangle(); + } + + // 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; + } + requestBuilder.set( + CaptureRequest.CONTROL_AE_REGIONS, + exposureRectangle == null ? null : new MeteringRectangle[] {exposureRectangle}); + } + + private void buildExposureRectangle() { + if (this.cameraBoundaries == null) { + throw new AssertionError( + "The cameraBoundaries should be set (using `ExposurePointFeature.setCameraBoundaries(Size)`) before updating the exposure point."); + } + if (this.exposurePoint == null) { + this.exposureRectangle = null; + } else { + PlatformChannel.DeviceOrientation orientation = + this.sensorOrientationFeature.getLockedCaptureOrientation(); + if (orientation == null) { + orientation = + this.sensorOrientationFeature.getDeviceOrientationManager().getLastUIOrientation(); + } + this.exposureRectangle = + CameraRegionUtils.convertPointToMeteringRectangle( + this.cameraBoundaries, this.exposurePoint.x, this.exposurePoint.y, orientation); + } + } +} 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..788c768e0b3c --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/flash/FlashMode.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.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; + } + + /** + * Tries to convert the supplied string into a {@see FlashMode} enum value. + * + *

      When the supplied string doesn't match a valid {@see FlashMode} enum value, null is + * returned. + * + * @param modeStr String value to convert into an {@see FlashMode} enum value. + * @return Matching {@see FlashMode} enum value, or null if no match is found. + */ + 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/focuspoint/FocusPointFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java new file mode 100644 index 000000000000..a3a0172d3c37 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeature.java @@ -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. + +package io.flutter.plugins.camera.features.focuspoint; + +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.params.MeteringRectangle; +import android.util.Size; +import androidx.annotation.NonNull; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.CameraRegionUtils; +import io.flutter.plugins.camera.features.CameraFeature; +import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; + +/** Focus point controls where in the frame focus will come from. */ +public class FocusPointFeature extends CameraFeature { + + private Size cameraBoundaries; + private Point focusPoint; + private MeteringRectangle focusRectangle; + private final SensorOrientationFeature sensorOrientationFeature; + + /** + * Creates a new instance of the {@link FocusPointFeature}. + * + * @param cameraProperties Collection of the characteristics for the current camera device. + */ + public FocusPointFeature( + CameraProperties cameraProperties, SensorOrientationFeature sensorOrientationFeature) { + super(cameraProperties); + this.sensorOrientationFeature = sensorOrientationFeature; + } + + /** + * Sets the camera boundaries that are required for the focus point feature to function. + * + * @param cameraBoundaries - The camera boundaries to set. + */ + public void setCameraBoundaries(@NonNull Size cameraBoundaries) { + this.cameraBoundaries = cameraBoundaries; + this.buildFocusRectangle(); + } + + @Override + public String getDebugName() { + return "FocusPointFeature"; + } + + @Override + public Point getValue() { + return focusPoint; + } + + @Override + public void setValue(Point value) { + this.focusPoint = value == null || value.x == null || value.y == null ? null : value; + this.buildFocusRectangle(); + } + + // Whether or not this camera can set the focus point. + @Override + public boolean checkIsSupported() { + Integer supportedRegions = cameraProperties.getControlMaxRegionsAutoFocus(); + return supportedRegions != null && supportedRegions > 0; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + if (!checkIsSupported()) { + return; + } + requestBuilder.set( + CaptureRequest.CONTROL_AF_REGIONS, + focusRectangle == null ? null : new MeteringRectangle[] {focusRectangle}); + } + + private void buildFocusRectangle() { + if (this.cameraBoundaries == null) { + throw new AssertionError( + "The cameraBoundaries should be set (using `FocusPointFeature.setCameraBoundaries(Size)`) before updating the focus point."); + } + if (this.focusPoint == null) { + this.focusRectangle = null; + } else { + PlatformChannel.DeviceOrientation orientation = + this.sensorOrientationFeature.getLockedCaptureOrientation(); + if (orientation == null) { + orientation = + this.sensorOrientationFeature.getDeviceOrientationManager().getLastUIOrientation(); + } + this.focusRectangle = + CameraRegionUtils.convertPointToMeteringRectangle( + this.cameraBoundaries, this.focusPoint.x, this.focusPoint.y, orientation); + } + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java new file mode 100644 index 000000000000..500f2aa28dc2 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeature.java @@ -0,0 +1,87 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.features.fpsrange; + +import android.hardware.camera2.CaptureRequest; +import android.os.Build; +import android.util.Range; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; + +/** + * Controls the frames per seconds (FPS) range configuration on the {@link android.hardware.camera2} + * API. + */ +public class FpsRangeFeature extends CameraFeature> { + private static final Range MAX_PIXEL4A_RANGE = new Range<>(30, 30); + private Range currentSetting; + + /** + * Creates a new instance of the {@link FpsRangeFeature}. + * + * @param cameraProperties Collection of characteristics for the current camera device. + */ + public FpsRangeFeature(CameraProperties cameraProperties) { + super(cameraProperties); + + if (isPixel4A()) { + // HACK: There is a bug in the Pixel 4A where it cannot support 60fps modes + // even though they are reported as supported by + // `getControlAutoExposureAvailableTargetFpsRanges`. + // For max device compatibility we will keep FPS under 60 even if they report they are + // capable of achieving 60 fps. Highest working FPS is 30. + // https://issuetracker.google.com/issues/189237151 + currentSetting = MAX_PIXEL4A_RANGE; + } else { + Range[] ranges = cameraProperties.getControlAutoExposureAvailableTargetFpsRanges(); + + if (ranges != null) { + for (Range range : ranges) { + int upper = range.getUpper(); + + if (upper >= 10) { + if (currentSetting == null || upper > currentSetting.getUpper()) { + currentSetting = range; + } + } + } + } + } + } + + private boolean isPixel4A() { + return Build.BRAND.equals("google") && Build.MODEL.equals("Pixel 4a"); + } + + @Override + public String getDebugName() { + return "FpsRangeFeature"; + } + + @Override + public Range getValue() { + return currentSetting; + } + + @Override + public void setValue(Range value) { + this.currentSetting = value; + } + + // Always supported + @Override + public boolean checkIsSupported() { + return true; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + if (!checkIsSupported()) { + return; + } + + requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, currentSetting); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java new file mode 100644 index 000000000000..408575b375e6 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeature.java @@ -0,0 +1,91 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.features.noisereduction; + +import android.hardware.camera2.CaptureRequest; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.util.Log; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; +import java.util.HashMap; + +/** + * This can either be enabled or disabled. Only full capability devices can set this to off. Legacy + * and full support the fast mode. + * https://developer.android.com/reference/android/hardware/camera2/CameraCharacteristics#NOISE_REDUCTION_AVAILABLE_NOISE_REDUCTION_MODES + */ +public class NoiseReductionFeature extends CameraFeature { + private NoiseReductionMode currentSetting = NoiseReductionMode.fast; + + private final HashMap NOISE_REDUCTION_MODES = new HashMap<>(); + + /** + * Creates a new instance of the {@link NoiseReductionFeature}. + * + * @param cameraProperties Collection of the characteristics for the current camera device. + */ + public NoiseReductionFeature(CameraProperties cameraProperties) { + super(cameraProperties); + NOISE_REDUCTION_MODES.put(NoiseReductionMode.off, CaptureRequest.NOISE_REDUCTION_MODE_OFF); + NOISE_REDUCTION_MODES.put(NoiseReductionMode.fast, CaptureRequest.NOISE_REDUCTION_MODE_FAST); + NOISE_REDUCTION_MODES.put( + NoiseReductionMode.highQuality, CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY); + if (VERSION.SDK_INT >= VERSION_CODES.M) { + NOISE_REDUCTION_MODES.put( + NoiseReductionMode.minimal, CaptureRequest.NOISE_REDUCTION_MODE_MINIMAL); + NOISE_REDUCTION_MODES.put( + NoiseReductionMode.zeroShutterLag, CaptureRequest.NOISE_REDUCTION_MODE_ZERO_SHUTTER_LAG); + } + } + + @Override + public String getDebugName() { + return "NoiseReductionFeature"; + } + + @Override + public NoiseReductionMode getValue() { + return currentSetting; + } + + @Override + public void setValue(NoiseReductionMode value) { + this.currentSetting = value; + } + + @Override + public boolean checkIsSupported() { + /* + * Available settings: public static final int NOISE_REDUCTION_MODE_FAST = 1; public static + * final int NOISE_REDUCTION_MODE_HIGH_QUALITY = 2; public static final int + * NOISE_REDUCTION_MODE_MINIMAL = 3; public static final int NOISE_REDUCTION_MODE_OFF = 0; + * public static final int NOISE_REDUCTION_MODE_ZERO_SHUTTER_LAG = 4; + * + *

      Full-capability camera devices will always support OFF and FAST. Camera devices that + * support YUV_REPROCESSING or PRIVATE_REPROCESSING will support ZERO_SHUTTER_LAG. + * Legacy-capability camera devices will only support FAST mode. + */ + + // Can be null on some devices. + int[] modes = cameraProperties.getAvailableNoiseReductionModes(); + + /// If there's at least one mode available then we are supported. + return modes != null && modes.length > 0; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + if (!checkIsSupported()) { + return; + } + + Log.i("Camera", "updateNoiseReduction | currentSetting: " + currentSetting); + + // Always use fast mode. + requestBuilder.set( + CaptureRequest.NOISE_REDUCTION_MODE, NOISE_REDUCTION_MODES.get(currentSetting)); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java new file mode 100644 index 000000000000..425a458e2a2b --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionMode.java @@ -0,0 +1,41 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.features.noisereduction; + +/** Only supports fast mode for now. */ +public enum NoiseReductionMode { + off("off"), + fast("fast"), + highQuality("highQuality"), + minimal("minimal"), + zeroShutterLag("zeroShutterLag"); + + private final String strValue; + + NoiseReductionMode(String strValue) { + this.strValue = strValue; + } + + /** + * Tries to convert the supplied string into a {@see NoiseReductionMode} enum value. + * + *

      When the supplied string doesn't match a valid {@see NoiseReductionMode} enum value, null is + * returned. + * + * @param modeStr String value to convert into an {@see NoiseReductionMode} enum value. + * @return Matching {@see NoiseReductionMode} enum value, or null if no match is found. + */ + public static NoiseReductionMode getValueForString(String modeStr) { + for (NoiseReductionMode 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/resolution/ResolutionFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.java new file mode 100644 index 000000000000..67763dde9be4 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionFeature.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.resolution; + +import android.hardware.camera2.CaptureRequest; +import android.media.CamcorderProfile; +import android.util.Size; +import androidx.annotation.VisibleForTesting; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.features.CameraFeature; + +/** + * Controls the resolutions configuration on the {@link android.hardware.camera2} API. + * + *

      The {@link ResolutionFeature} is responsible for converting the platform independent {@link + * ResolutionPreset} into a {@link android.media.CamcorderProfile} which contains all the properties + * required to configure the resolution using the {@link android.hardware.camera2} API. + */ +public class ResolutionFeature extends CameraFeature { + private Size captureSize; + private Size previewSize; + private CamcorderProfile recordingProfile; + private ResolutionPreset currentSetting; + private int cameraId; + + /** + * Creates a new instance of the {@link ResolutionFeature}. + * + * @param cameraProperties Collection of characteristics for the current camera device. + * @param resolutionPreset Platform agnostic enum containing resolution information. + * @param cameraName Camera identifier of the camera for which to configure the resolution. + */ + public ResolutionFeature( + CameraProperties cameraProperties, ResolutionPreset resolutionPreset, String cameraName) { + super(cameraProperties); + this.currentSetting = resolutionPreset; + try { + this.cameraId = Integer.parseInt(cameraName, 10); + } catch (NumberFormatException e) { + this.cameraId = -1; + return; + } + configureResolution(resolutionPreset, cameraId); + } + + /** + * Gets the {@link android.media.CamcorderProfile} containing the information to configure the + * resolution using the {@link android.hardware.camera2} API. + * + * @return Resolution information to configure the {@link android.hardware.camera2} API. + */ + public CamcorderProfile getRecordingProfile() { + return this.recordingProfile; + } + + /** + * Gets the optimal preview size based on the configured resolution. + * + * @return The optimal preview size. + */ + public Size getPreviewSize() { + return this.previewSize; + } + + /** + * Gets the optimal capture size based on the configured resolution. + * + * @return The optimal capture size. + */ + public Size getCaptureSize() { + return this.captureSize; + } + + @Override + public String getDebugName() { + return "ResolutionFeature"; + } + + @Override + public ResolutionPreset getValue() { + return currentSetting; + } + + @Override + public void setValue(ResolutionPreset value) { + this.currentSetting = value; + configureResolution(currentSetting, cameraId); + } + + @Override + public boolean checkIsSupported() { + return cameraId >= 0; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + // No-op: when setting a resolution there is no need to update the request builder. + } + + @VisibleForTesting + static Size computeBestPreviewSize(int cameraId, ResolutionPreset preset) { + if (preset.ordinal() > ResolutionPreset.high.ordinal()) { + preset = ResolutionPreset.high; + } + + CamcorderProfile profile = + getBestAvailableCamcorderProfileForResolutionPreset(cameraId, preset); + return new Size(profile.videoFrameWidth, profile.videoFrameHeight); + } + + /** + * Gets the best possible {@link android.media.CamcorderProfile} for the supplied {@link + * ResolutionPreset}. + * + * @param cameraId Camera identifier which indicates the device's camera for which to select a + * {@link android.media.CamcorderProfile}. + * @param preset The {@link ResolutionPreset} for which is to be translated to a {@link + * android.media.CamcorderProfile}. + * @return The best possible {@link android.media.CamcorderProfile} that matches the supplied + * {@link ResolutionPreset}. + */ + public static CamcorderProfile getBestAvailableCamcorderProfileForResolutionPreset( + int cameraId, ResolutionPreset preset) { + if (cameraId < 0) { + throw new AssertionError( + "getBestAvailableCamcorderProfileForResolutionPreset can only be used with valid (>=0) camera identifiers."); + } + + switch (preset) { + // All of these cases deliberately fall through to get the best available profile. + case max: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_HIGH)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_HIGH); + } + case ultraHigh: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_2160P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_2160P); + } + case veryHigh: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_1080P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_1080P); + } + case high: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_720P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_720P); + } + case medium: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_480P)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_480P); + } + case low: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_QVGA)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_QVGA); + } + default: + if (CamcorderProfile.hasProfile(cameraId, CamcorderProfile.QUALITY_LOW)) { + return CamcorderProfile.get(cameraId, CamcorderProfile.QUALITY_LOW); + } else { + throw new IllegalArgumentException( + "No capture session available for current capture session."); + } + } + } + + private void configureResolution(ResolutionPreset resolutionPreset, int cameraId) { + if (!checkIsSupported()) { + return; + } + recordingProfile = + getBestAvailableCamcorderProfileForResolutionPreset(cameraId, resolutionPreset); + captureSize = new Size(recordingProfile.videoFrameWidth, recordingProfile.videoFrameHeight); + previewSize = computeBestPreviewSize(cameraId, resolutionPreset); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionPreset.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/ResolutionPreset.java new file mode 100644 index 000000000000..359300305d40 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/resolution/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.features.resolution; + +// Mirrors camera.dart +public enum ResolutionPreset { + low, + medium, + high, + veryHigh, + ultraHigh, + max, +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java new file mode 100644 index 000000000000..dd1e489e6225 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManager.java @@ -0,0 +1,329 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.features.sensororientation; + +import android.app.Activity; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Configuration; +import android.view.Display; +import android.view.Surface; +import android.view.WindowManager; +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; +import io.flutter.plugins.camera.DartMessenger; + +/** + * Support class to help to determine the media orientation based on the orientation of the device. + */ +public 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 BroadcastReceiver broadcastReceiver; + + /** Factory method to create a device orientation manager. */ + public static DeviceOrientationManager create( + @NonNull Activity activity, + @NonNull DartMessenger messenger, + boolean isFrontFacing, + int sensorOrientation) { + return new DeviceOrientationManager(activity, messenger, isFrontFacing, sensorOrientation); + } + + private DeviceOrientationManager( + @NonNull Activity activity, + @NonNull DartMessenger messenger, + boolean isFrontFacing, + int sensorOrientation) { + this.activity = activity; + this.messenger = messenger; + this.isFrontFacing = isFrontFacing; + this.sensorOrientation = sensorOrientation; + } + + /** + * Starts listening to the device's sensors or UI for orientation updates. + * + *

      When orientation information is updated the new orientation is send to the client using the + * {@link DartMessenger}. This latest value can also be retrieved through the {@link + * #getVideoOrientation()} accessor. + * + *

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

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

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

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

      Returns one of 0, 90, 180 or 270. + * + * @param orientation The {@link PlatformChannel.DeviceOrientation} value that is to be converted + * into degrees. + * @return The device's video orientation in degrees. + */ + public int getVideoOrientation(PlatformChannel.DeviceOrientation orientation) { + int angle = 0; + + // Fallback to device orientation when the orientation value is null. + if (orientation == null) { + orientation = getUIOrientation(); + } + + switch (orientation) { + case PORTRAIT_UP: + angle = 0; + break; + case PORTRAIT_DOWN: + angle = 180; + break; + case LANDSCAPE_LEFT: + angle = 90; + break; + case LANDSCAPE_RIGHT: + angle = 270; + break; + } + + if (isFrontFacing) { + angle *= -1; + } + + return (angle + sensorOrientation + 360) % 360; + } + + /** @return the last received UI orientation. */ + public PlatformChannel.DeviceOrientation getLastUIOrientation() { + return this.lastOrientation; + } + + /** + * Handles orientation changes based on change events triggered by the OrientationIntentFilter. + * + *

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

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

      This method is visible for testing purposes only and should never be used outside this + * class. + * + * @return The current user interface orientation. + */ + @VisibleForTesting + PlatformChannel.DeviceOrientation getUIOrientation() { + final int rotation = getDisplay().getRotation(); + final int orientation = activity.getResources().getConfiguration().orientation; + + switch (orientation) { + case Configuration.ORIENTATION_PORTRAIT: + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + } else { + return PlatformChannel.DeviceOrientation.PORTRAIT_DOWN; + } + case Configuration.ORIENTATION_LANDSCAPE: + if (rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_90) { + return PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT; + } else { + return PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT; + } + default: + return PlatformChannel.DeviceOrientation.PORTRAIT_UP; + } + } + + /** + * Calculates the sensor orientation based on the supplied angle. + * + *

      This method is visible for testing purposes only and should never be used outside this + * class. + * + * @param angle Orientation angle. + * @return The sensor orientation based on the supplied angle. + */ + @VisibleForTesting + PlatformChannel.DeviceOrientation calculateSensorOrientation(int angle) { + final int tolerance = 45; + angle += tolerance; + + // Orientation is 0 in the default orientation mode. This is portrait-mode for phones + // and landscape for tablets. We have to compensate for this by calculating the default + // orientation, and apply an offset accordingly. + int defaultDeviceOrientation = getDeviceDefaultOrientation(); + if (defaultDeviceOrientation == Configuration.ORIENTATION_LANDSCAPE) { + angle += 90; + } + // Determine the orientation + angle = angle % 360; + return new PlatformChannel.DeviceOrientation[] { + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + } + [angle / 90]; + } + + /** + * Gets the default orientation of the device. + * + *

      This method is visible for testing purposes only and should never be used outside this + * class. + * + * @return The default orientation of the device. + */ + @VisibleForTesting + int getDeviceDefaultOrientation() { + Configuration config = activity.getResources().getConfiguration(); + int rotation = getDisplay().getRotation(); + if (((rotation == Surface.ROTATION_0 || rotation == Surface.ROTATION_180) + && config.orientation == Configuration.ORIENTATION_LANDSCAPE) + || ((rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270) + && config.orientation == Configuration.ORIENTATION_PORTRAIT)) { + return Configuration.ORIENTATION_LANDSCAPE; + } else { + return Configuration.ORIENTATION_PORTRAIT; + } + } + + /** + * Gets an instance of the Android {@link android.view.Display}. + * + *

      This method is visible for testing purposes only and should never be used outside this + * class. + * + * @return An instance of the Android {@link android.view.Display}. + */ + @SuppressWarnings("deprecation") + @VisibleForTesting + Display getDisplay() { + return ((WindowManager) activity.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay(); + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeature.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeature.java new file mode 100644 index 000000000000..9e316f741805 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeature.java @@ -0,0 +1,105 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.features.sensororientation; + +import android.app.Activity; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.CaptureRequest; +import androidx.annotation.NonNull; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.DartMessenger; +import io.flutter.plugins.camera.features.CameraFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionFeature; + +/** Provides access to the sensor orientation of the camera devices. */ +public class SensorOrientationFeature extends CameraFeature { + private Integer currentSetting = 0; + private final DeviceOrientationManager deviceOrientationListener; + private PlatformChannel.DeviceOrientation lockedCaptureOrientation; + + /** + * Creates a new instance of the {@link ResolutionFeature}. + * + * @param cameraProperties Collection of characteristics for the current camera device. + * @param activity Current Android {@link android.app.Activity}, used to detect UI orientation + * changes. + * @param dartMessenger Instance of a {@link DartMessenger} used to communicate orientation + * updates back to the client. + */ + public SensorOrientationFeature( + @NonNull CameraProperties cameraProperties, + @NonNull Activity activity, + @NonNull DartMessenger dartMessenger) { + super(cameraProperties); + setValue(cameraProperties.getSensorOrientation()); + + boolean isFrontFacing = cameraProperties.getLensFacing() == CameraMetadata.LENS_FACING_FRONT; + deviceOrientationListener = + DeviceOrientationManager.create(activity, dartMessenger, isFrontFacing, currentSetting); + deviceOrientationListener.start(); + } + + @Override + public String getDebugName() { + return "SensorOrientationFeature"; + } + + @Override + public Integer getValue() { + return currentSetting; + } + + @Override + public void setValue(Integer value) { + this.currentSetting = value; + } + + @Override + public boolean checkIsSupported() { + return true; + } + + @Override + public void updateBuilder(CaptureRequest.Builder requestBuilder) { + // Noop: when setting the sensor orientation there is no need to update the request builder. + } + + /** + * Gets the instance of the {@link DeviceOrientationManager} used to detect orientation changes. + * + * @return The instance of the {@link DeviceOrientationManager}. + */ + public DeviceOrientationManager getDeviceOrientationManager() { + return this.deviceOrientationListener; + } + + /** + * Lock the capture orientation, indicating that the device orientation should not influence the + * capture orientation. + * + * @param orientation The orientation in which to lock the capture orientation. + */ + public void lockCaptureOrientation(PlatformChannel.DeviceOrientation orientation) { + this.lockedCaptureOrientation = orientation; + } + + /** + * Unlock the capture orientation, indicating that the device orientation should be used to + * configure the capture orientation. + */ + public void unlockCaptureOrientation() { + this.lockedCaptureOrientation = null; + } + + /** + * Gets the configured locked capture orientation. + * + * @return The configured locked capture orientation. + */ + public PlatformChannel.DeviceOrientation getLockedCaptureOrientation() { + return this.lockedCaptureOrientation; + } +} 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/CameraCaptureProperties.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CameraCaptureProperties.java new file mode 100644 index 000000000000..68177f4ecfd6 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CameraCaptureProperties.java @@ -0,0 +1,67 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.types; + +public class CameraCaptureProperties { + + private Float lastLensAperture; + private Long lastSensorExposureTime; + private Integer lastSensorSensitivity; + + /** + * Gets the last known lens aperture. (As f-stop value) + * + * @return the last known lens aperture. (As f-stop value) + */ + public Float getLastLensAperture() { + return lastLensAperture; + } + + /** + * Sets the last known lens aperture. (As f-stop value) + * + * @param lastLensAperture - The last known lens aperture to set. (As f-stop value) + */ + public void setLastLensAperture(Float lastLensAperture) { + this.lastLensAperture = lastLensAperture; + } + + /** + * Gets the last known sensor exposure time in nanoseconds. + * + * @return the last known sensor exposure time in nanoseconds. + */ + public Long getLastSensorExposureTime() { + return lastSensorExposureTime; + } + + /** + * Sets the last known sensor exposure time in nanoseconds. + * + * @param lastSensorExposureTime - The last known sensor exposure time to set, in nanoseconds. + */ + public void setLastSensorExposureTime(Long lastSensorExposureTime) { + this.lastSensorExposureTime = lastSensorExposureTime; + } + + /** + * Gets the last known sensor sensitivity in ISO arithmetic units. + * + * @return the last known sensor sensitivity in ISO arithmetic units. + */ + public Integer getLastSensorSensitivity() { + return lastSensorSensitivity; + } + + /** + * Sets the last known sensor sensitivity in ISO arithmetic units. + * + * @param lastSensorSensitivity - The last known sensor sensitivity to set, in ISO arithmetic + * units. + */ + public void setLastSensorSensitivity(Integer lastSensorSensitivity) { + this.lastSensorSensitivity = lastSensorSensitivity; + } +} diff --git a/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.java new file mode 100644 index 000000000000..ad59bd09c754 --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/CaptureTimeoutsWrapper.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.types; + +/** + * Wrapper class that provides a container for all {@link Timeout} instances that are required for + * the capture flow. + */ +public class CaptureTimeoutsWrapper { + private Timeout preCaptureFocusing; + private Timeout preCaptureMetering; + private final long preCaptureFocusingTimeoutMs; + private final long preCaptureMeteringTimeoutMs; + + /** + * Create a new wrapper instance with the specified timeout values. + * + * @param preCaptureFocusingTimeoutMs focusing timeout milliseconds. + * @param preCaptureMeteringTimeoutMs metering timeout milliseconds. + */ + public CaptureTimeoutsWrapper( + long preCaptureFocusingTimeoutMs, long preCaptureMeteringTimeoutMs) { + this.preCaptureFocusingTimeoutMs = preCaptureFocusingTimeoutMs; + this.preCaptureMeteringTimeoutMs = preCaptureMeteringTimeoutMs; + } + + /** Reset all timeouts to the current timestamp. */ + public void reset() { + this.preCaptureFocusing = Timeout.create(preCaptureFocusingTimeoutMs); + this.preCaptureMetering = Timeout.create(preCaptureMeteringTimeoutMs); + } + + /** + * Returns the timeout instance related to precapture focusing. + * + * @return - The timeout object + */ + public Timeout getPreCaptureFocusing() { + return preCaptureFocusing; + } + + /** + * Returns the timeout instance related to precapture metering. + * + * @return - The timeout object + */ + public Timeout getPreCaptureMetering() { + return preCaptureMetering; + } +} 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/camera/android/src/main/java/io/flutter/plugins/camera/types/Timeout.java b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/Timeout.java new file mode 100644 index 000000000000..67e05499d47a --- /dev/null +++ b/packages/camera/camera/android/src/main/java/io/flutter/plugins/camera/types/Timeout.java @@ -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. + +package io.flutter.plugins.camera.types; + +import android.os.SystemClock; + +/** + * This is a simple class for managing a timeout. In the camera we generally keep two timeouts: one + * for focusing and one for pre-capture metering. + * + *

      We use timeouts to ensure a picture is always captured within a reasonable amount of time even + * if the settings don't converge and focus can't be locked. + * + *

      You generally check the status of the timeout in the CameraCaptureCallback during the capture + * sequence and use it to move to the next state if the timeout has passed. + */ +public class Timeout { + + /** The timeout time in milliseconds */ + private final long timeoutMs; + + /** When this timeout was started. Will be used later to check if the timeout has expired yet. */ + private final long timeStarted; + + /** + * Factory method to create a new Timeout. + * + * @param timeoutMs timeout to use. + * @return returns a new Timeout. + */ + public static Timeout create(long timeoutMs) { + return new Timeout(timeoutMs); + } + + /** + * Create a new timeout. + * + * @param timeoutMs the time in milliseconds for this timeout to lapse. + */ + private Timeout(long timeoutMs) { + this.timeoutMs = timeoutMs; + this.timeStarted = SystemClock.elapsedRealtime(); + } + + /** Will return true when the timeout period has lapsed. */ + public boolean getIsExpired() { + return (SystemClock.elapsedRealtime() - timeStarted) > timeoutMs; + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java new file mode 100644 index 000000000000..934aff857ec7 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackStatesTest.java @@ -0,0 +1,381 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import static org.mockito.Mockito.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.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.CaptureResult.Key; +import android.hardware.camera2.TotalCaptureResult; +import io.flutter.plugins.camera.CameraCaptureCallback.CameraCaptureStateListener; +import io.flutter.plugins.camera.types.CameraCaptureProperties; +import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper; +import io.flutter.plugins.camera.types.Timeout; +import io.flutter.plugins.camera.utils.TestUtils; +import java.util.HashMap; +import java.util.Map; +import junit.framework.TestCase; +import junit.framework.TestSuite; +import org.mockito.MockedStatic; + +public class CameraCaptureCallbackStatesTest extends TestCase { + private final Integer aeState; + private final Integer afState; + private final CameraState cameraState; + private final boolean isTimedOut; + + private Runnable validate; + + private CameraCaptureCallback cameraCaptureCallback; + private CameraCaptureStateListener mockCaptureStateListener; + private CameraCaptureSession mockCameraCaptureSession; + private CaptureRequest mockCaptureRequest; + private CaptureResult mockPartialCaptureResult; + private CaptureTimeoutsWrapper mockCaptureTimeouts; + private CameraCaptureProperties mockCaptureProps; + private TotalCaptureResult mockTotalCaptureResult; + private MockedStatic mockedStaticTimeout; + private Timeout mockTimeout; + + public static TestSuite suite() { + TestSuite suite = new TestSuite(); + + setUpPreviewStateTest(suite); + setUpWaitingFocusTests(suite); + setUpWaitingPreCaptureStartTests(suite); + setUpWaitingPreCaptureDoneTests(suite); + + return suite; + } + + protected CameraCaptureCallbackStatesTest( + String name, CameraState cameraState, Integer afState, Integer aeState) { + this(name, cameraState, afState, aeState, false); + } + + protected CameraCaptureCallbackStatesTest( + String name, CameraState cameraState, Integer afState, Integer aeState, boolean isTimedOut) { + super(name); + + this.aeState = aeState; + this.afState = afState; + this.cameraState = cameraState; + this.isTimedOut = isTimedOut; + } + + @Override + @SuppressWarnings("unchecked") + protected void setUp() throws Exception { + super.setUp(); + + mockedStaticTimeout = mockStatic(Timeout.class); + mockCaptureStateListener = mock(CameraCaptureStateListener.class); + mockCameraCaptureSession = mock(CameraCaptureSession.class); + mockCaptureRequest = mock(CaptureRequest.class); + mockPartialCaptureResult = mock(CaptureResult.class); + mockTotalCaptureResult = mock(TotalCaptureResult.class); + mockTimeout = mock(Timeout.class); + mockCaptureTimeouts = mock(CaptureTimeoutsWrapper.class); + mockCaptureProps = mock(CameraCaptureProperties.class); + when(mockCaptureTimeouts.getPreCaptureFocusing()).thenReturn(mockTimeout); + when(mockCaptureTimeouts.getPreCaptureMetering()).thenReturn(mockTimeout); + + Key mockAeStateKey = mock(Key.class); + Key mockAfStateKey = mock(Key.class); + + TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AE_STATE", mockAeStateKey); + TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AF_STATE", mockAfStateKey); + + mockedStaticTimeout.when(() -> Timeout.create(1000)).thenReturn(mockTimeout); + + cameraCaptureCallback = + CameraCaptureCallback.create( + mockCaptureStateListener, mockCaptureTimeouts, mockCaptureProps); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + + mockedStaticTimeout.close(); + + TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AE_STATE", null); + TestUtils.setFinalStatic(CaptureResult.class, "CONTROL_AF_STATE", null); + } + + @Override + protected void runTest() throws Throwable { + when(mockPartialCaptureResult.get(CaptureResult.CONTROL_AF_STATE)).thenReturn(afState); + when(mockPartialCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(aeState); + when(mockTotalCaptureResult.get(CaptureResult.CONTROL_AF_STATE)).thenReturn(afState); + when(mockTotalCaptureResult.get(CaptureResult.CONTROL_AE_STATE)).thenReturn(aeState); + + cameraCaptureCallback.setCameraState(cameraState); + if (isTimedOut) { + when(mockTimeout.getIsExpired()).thenReturn(true); + cameraCaptureCallback.onCaptureCompleted( + mockCameraCaptureSession, mockCaptureRequest, mockTotalCaptureResult); + } else { + cameraCaptureCallback.onCaptureProgressed( + mockCameraCaptureSession, mockCaptureRequest, mockPartialCaptureResult); + } + + validate.run(); + } + + private static void setUpPreviewStateTest(TestSuite suite) { + CameraCaptureCallbackStatesTest previewStateTest = + new CameraCaptureCallbackStatesTest( + "process_should_not_converge_or_pre_capture_when_state_is_preview", + CameraState.STATE_PREVIEW, + null, + null); + previewStateTest.validate = + () -> { + verify(previewStateTest.mockCaptureStateListener, never()).onConverged(); + verify(previewStateTest.mockCaptureStateListener, never()).onConverged(); + assertEquals( + CameraState.STATE_PREVIEW, previewStateTest.cameraCaptureCallback.getCameraState()); + }; + suite.addTest(previewStateTest); + } + + private static void setUpWaitingFocusTests(TestSuite suite) { + Integer[] actionableAfStates = + new Integer[] { + CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED, + CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED + }; + + Integer[] nonActionableAfStates = + new Integer[] { + CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN, + CaptureResult.CONTROL_AF_STATE_INACTIVE, + CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED, + CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN, + CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED + }; + + Map aeStatesConvergeMap = + new HashMap() { + { + put(null, true); + put(CaptureResult.CONTROL_AE_STATE_CONVERGED, true); + put(CaptureResult.CONTROL_AE_STATE_PRECAPTURE, false); + put(CaptureResult.CONTROL_AE_STATE_LOCKED, false); + put(CaptureResult.CONTROL_AE_STATE_SEARCHING, false); + put(CaptureResult.CONTROL_AE_STATE_INACTIVE, false); + put(CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED, false); + } + }; + + CameraCaptureCallbackStatesTest nullStateTest = + new CameraCaptureCallbackStatesTest( + "process_should_not_converge_or_pre_capture_when_afstate_is_null", + CameraState.STATE_WAITING_FOCUS, + null, + null); + nullStateTest.validate = + () -> { + verify(nullStateTest.mockCaptureStateListener, never()).onConverged(); + verify(nullStateTest.mockCaptureStateListener, never()).onConverged(); + assertEquals( + CameraState.STATE_WAITING_FOCUS, + nullStateTest.cameraCaptureCallback.getCameraState()); + }; + suite.addTest(nullStateTest); + + for (Integer afState : actionableAfStates) { + aeStatesConvergeMap.forEach( + (aeState, shouldConverge) -> { + CameraCaptureCallbackStatesTest focusLockedTest = + new CameraCaptureCallbackStatesTest( + "process_should_converge_when_af_state_is_" + + afState + + "_and_ae_state_is_" + + aeState, + CameraState.STATE_WAITING_FOCUS, + afState, + aeState); + focusLockedTest.validate = + () -> { + if (shouldConverge) { + verify(focusLockedTest.mockCaptureStateListener, times(1)).onConverged(); + verify(focusLockedTest.mockCaptureStateListener, never()).onPrecapture(); + } else { + verify(focusLockedTest.mockCaptureStateListener, times(1)).onPrecapture(); + verify(focusLockedTest.mockCaptureStateListener, never()).onConverged(); + } + assertEquals( + CameraState.STATE_WAITING_FOCUS, + focusLockedTest.cameraCaptureCallback.getCameraState()); + }; + suite.addTest(focusLockedTest); + }); + } + + for (Integer afState : nonActionableAfStates) { + CameraCaptureCallbackStatesTest focusLockedTest = + new CameraCaptureCallbackStatesTest( + "process_should_do_nothing_when_af_state_is_" + afState, + CameraState.STATE_WAITING_FOCUS, + afState, + null); + focusLockedTest.validate = + () -> { + verify(focusLockedTest.mockCaptureStateListener, never()).onConverged(); + verify(focusLockedTest.mockCaptureStateListener, never()).onPrecapture(); + assertEquals( + CameraState.STATE_WAITING_FOCUS, + focusLockedTest.cameraCaptureCallback.getCameraState()); + }; + suite.addTest(focusLockedTest); + } + + for (Integer afState : nonActionableAfStates) { + aeStatesConvergeMap.forEach( + (aeState, shouldConverge) -> { + CameraCaptureCallbackStatesTest focusLockedTest = + new CameraCaptureCallbackStatesTest( + "process_should_converge_when_af_state_is_" + + afState + + "_and_ae_state_is_" + + aeState, + CameraState.STATE_WAITING_FOCUS, + afState, + aeState, + true); + focusLockedTest.validate = + () -> { + if (shouldConverge) { + verify(focusLockedTest.mockCaptureStateListener, times(1)).onConverged(); + verify(focusLockedTest.mockCaptureStateListener, never()).onPrecapture(); + } else { + verify(focusLockedTest.mockCaptureStateListener, times(1)).onPrecapture(); + verify(focusLockedTest.mockCaptureStateListener, never()).onConverged(); + } + assertEquals( + CameraState.STATE_WAITING_FOCUS, + focusLockedTest.cameraCaptureCallback.getCameraState()); + }; + suite.addTest(focusLockedTest); + }); + } + } + + private static void setUpWaitingPreCaptureStartTests(TestSuite suite) { + Map cameraStateMap = + new HashMap() { + { + put(null, CameraState.STATE_WAITING_PRECAPTURE_DONE); + put( + CaptureResult.CONTROL_AE_STATE_INACTIVE, + CameraState.STATE_WAITING_PRECAPTURE_START); + put( + CaptureResult.CONTROL_AE_STATE_SEARCHING, + CameraState.STATE_WAITING_PRECAPTURE_START); + put( + CaptureResult.CONTROL_AE_STATE_CONVERGED, + CameraState.STATE_WAITING_PRECAPTURE_DONE); + put(CaptureResult.CONTROL_AE_STATE_LOCKED, CameraState.STATE_WAITING_PRECAPTURE_START); + put( + CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED, + CameraState.STATE_WAITING_PRECAPTURE_DONE); + put( + CaptureResult.CONTROL_AE_STATE_PRECAPTURE, + CameraState.STATE_WAITING_PRECAPTURE_DONE); + } + }; + + cameraStateMap.forEach( + (aeState, cameraState) -> { + CameraCaptureCallbackStatesTest testCase = + new CameraCaptureCallbackStatesTest( + "process_should_update_camera_state_to_waiting_pre_capture_done_when_ae_state_is_" + + aeState, + CameraState.STATE_WAITING_PRECAPTURE_START, + null, + aeState); + testCase.validate = + () -> assertEquals(cameraState, testCase.cameraCaptureCallback.getCameraState()); + suite.addTest(testCase); + }); + + cameraStateMap.forEach( + (aeState, cameraState) -> { + if (cameraState == CameraState.STATE_WAITING_PRECAPTURE_DONE) { + return; + } + + CameraCaptureCallbackStatesTest testCase = + new CameraCaptureCallbackStatesTest( + "process_should_update_camera_state_to_waiting_pre_capture_done_when_ae_state_is_" + + aeState, + CameraState.STATE_WAITING_PRECAPTURE_START, + null, + aeState, + true); + testCase.validate = + () -> + assertEquals( + CameraState.STATE_WAITING_PRECAPTURE_DONE, + testCase.cameraCaptureCallback.getCameraState()); + suite.addTest(testCase); + }); + } + + private static void setUpWaitingPreCaptureDoneTests(TestSuite suite) { + Integer[] onConvergeStates = + new Integer[] { + null, + CaptureResult.CONTROL_AE_STATE_CONVERGED, + CaptureResult.CONTROL_AE_STATE_LOCKED, + CaptureResult.CONTROL_AE_STATE_SEARCHING, + CaptureResult.CONTROL_AE_STATE_INACTIVE, + CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED, + }; + + for (Integer aeState : onConvergeStates) { + CameraCaptureCallbackStatesTest shouldConvergeTest = + new CameraCaptureCallbackStatesTest( + "process_should_converge_when_ae_state_is_" + aeState, + CameraState.STATE_WAITING_PRECAPTURE_DONE, + null, + null); + shouldConvergeTest.validate = + () -> verify(shouldConvergeTest.mockCaptureStateListener, times(1)).onConverged(); + suite.addTest(shouldConvergeTest); + } + + CameraCaptureCallbackStatesTest shouldNotConvergeTest = + new CameraCaptureCallbackStatesTest( + "process_should_not_converge_when_ae_state_is_pre_capture", + CameraState.STATE_WAITING_PRECAPTURE_DONE, + null, + CaptureResult.CONTROL_AE_STATE_PRECAPTURE); + shouldNotConvergeTest.validate = + () -> verify(shouldNotConvergeTest.mockCaptureStateListener, never()).onConverged(); + suite.addTest(shouldNotConvergeTest); + + CameraCaptureCallbackStatesTest shouldConvergeWhenTimedOutTest = + new CameraCaptureCallbackStatesTest( + "process_should_not_converge_when_ae_state_is_pre_capture", + CameraState.STATE_WAITING_PRECAPTURE_DONE, + null, + CaptureResult.CONTROL_AE_STATE_PRECAPTURE, + true); + shouldConvergeWhenTimedOutTest.validate = + () -> + verify(shouldConvergeWhenTimedOutTest.mockCaptureStateListener, times(1)).onConverged(); + suite.addTest(shouldConvergeWhenTimedOutTest); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java new file mode 100644 index 000000000000..75a5b25995e2 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraCaptureCallbackTest.java @@ -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. + +package io.flutter.plugins.camera; + +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +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.CameraCaptureSession; +import android.hardware.camera2.CaptureRequest; +import android.hardware.camera2.CaptureResult; +import android.hardware.camera2.TotalCaptureResult; +import io.flutter.plugins.camera.types.CameraCaptureProperties; +import io.flutter.plugins.camera.types.CaptureTimeoutsWrapper; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class CameraCaptureCallbackTest { + + private CameraCaptureCallback cameraCaptureCallback; + private CameraCaptureProperties mockCaptureProps; + + @Before + public void setUp() { + CameraCaptureCallback.CameraCaptureStateListener mockCaptureStateListener = + mock(CameraCaptureCallback.CameraCaptureStateListener.class); + CaptureTimeoutsWrapper mockCaptureTimeouts = mock(CaptureTimeoutsWrapper.class); + mockCaptureProps = mock(CameraCaptureProperties.class); + cameraCaptureCallback = + CameraCaptureCallback.create( + mockCaptureStateListener, mockCaptureTimeouts, mockCaptureProps); + } + + @Test + public void onCaptureProgressed_doesNotUpdateCameraCaptureProperties() { + CameraCaptureSession mockSession = mock(CameraCaptureSession.class); + CaptureRequest mockRequest = mock(CaptureRequest.class); + CaptureResult mockResult = mock(CaptureResult.class); + + cameraCaptureCallback.onCaptureProgressed(mockSession, mockRequest, mockResult); + + verify(mockCaptureProps, never()).setLastLensAperture(anyFloat()); + verify(mockCaptureProps, never()).setLastSensorExposureTime(anyLong()); + verify(mockCaptureProps, never()).setLastSensorSensitivity(anyInt()); + } + + @Test + public void onCaptureCompleted_updatesCameraCaptureProperties() { + CameraCaptureSession mockSession = mock(CameraCaptureSession.class); + CaptureRequest mockRequest = mock(CaptureRequest.class); + TotalCaptureResult mockResult = mock(TotalCaptureResult.class); + when(mockResult.get(CaptureResult.LENS_APERTURE)).thenReturn(1.0f); + when(mockResult.get(CaptureResult.SENSOR_EXPOSURE_TIME)).thenReturn(2L); + when(mockResult.get(CaptureResult.SENSOR_SENSITIVITY)).thenReturn(3); + + cameraCaptureCallback.onCaptureCompleted(mockSession, mockRequest, mockResult); + + verify(mockCaptureProps, times(1)).setLastLensAperture(1.0f); + verify(mockCaptureProps, times(1)).setLastSensorExposureTime(2L); + verify(mockCaptureProps, times(1)).setLastSensorSensitivity(3); + } +} 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..40db12ee0fc3 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraPropertiesImplTest.java @@ -0,0 +1,279 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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_shouldReturnValidInstance() 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_shouldReturnDoubleWhenRationalIsNotNull() { + 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_shouldReturnZeroWhenRationalIsNull() { + 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/CameraRegionUtils_convertPointToMeteringRectangleTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_convertPointToMeteringRectangleTest.java new file mode 100644 index 000000000000..2c6d9d9177e9 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_convertPointToMeteringRectangleTest.java @@ -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. +package io.flutter.plugins.camera; + +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.when; + +import android.hardware.camera2.params.MeteringRectangle; +import android.util.Size; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +public class CameraRegionUtils_convertPointToMeteringRectangleTest { + private MockedStatic mockedMeteringRectangleFactory; + private Size mockCameraBoundaries; + + @Before + public void setUp() { + this.mockCameraBoundaries = mock(Size.class); + when(this.mockCameraBoundaries.getWidth()).thenReturn(100); + when(this.mockCameraBoundaries.getHeight()).thenReturn(100); + mockedMeteringRectangleFactory = mockStatic(CameraRegionUtils.MeteringRectangleFactory.class); + + mockedMeteringRectangleFactory + .when( + () -> + CameraRegionUtils.MeteringRectangleFactory.create( + anyInt(), anyInt(), anyInt(), anyInt(), anyInt())) + .thenAnswer( + new Answer() { + @Override + public MeteringRectangle answer(InvocationOnMock createInvocation) throws Throwable { + MeteringRectangle mockMeteringRectangle = mock(MeteringRectangle.class); + when(mockMeteringRectangle.getX()).thenReturn(createInvocation.getArgument(0)); + when(mockMeteringRectangle.getY()).thenReturn(createInvocation.getArgument(1)); + when(mockMeteringRectangle.getWidth()).thenReturn(createInvocation.getArgument(2)); + when(mockMeteringRectangle.getHeight()).thenReturn(createInvocation.getArgument(3)); + when(mockMeteringRectangle.getMeteringWeight()) + .thenReturn(createInvocation.getArgument(4)); + when(mockMeteringRectangle.equals(any())) + .thenAnswer( + new Answer() { + @Override + public Boolean answer(InvocationOnMock equalsInvocation) + throws Throwable { + MeteringRectangle otherMockMeteringRectangle = + equalsInvocation.getArgument(0); + return mockMeteringRectangle.getX() == otherMockMeteringRectangle.getX() + && mockMeteringRectangle.getY() == otherMockMeteringRectangle.getY() + && mockMeteringRectangle.getWidth() + == otherMockMeteringRectangle.getWidth() + && mockMeteringRectangle.getHeight() + == otherMockMeteringRectangle.getHeight() + && mockMeteringRectangle.getMeteringWeight() + == otherMockMeteringRectangle.getMeteringWeight(); + } + }); + return mockMeteringRectangle; + } + }); + } + + @After + public void tearDown() { + mockedMeteringRectangleFactory.close(); + } + + @Test + public void convertPointToMeteringRectangle_shouldReturnValidMeteringRectangleForCenterCoord() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 0.5, 0.5, PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(45, 45, 10, 10, 1).equals(r)); + } + + @Test + public void convertPointToMeteringRectangle_shouldReturnValidMeteringRectangleForTopLeftCoord() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 0, 0, PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(0, 0, 10, 10, 1).equals(r)); + } + + @Test + public void convertPointToMeteringRectangle_ShouldReturnValidMeteringRectangleForTopRightCoord() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1, 0, PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(89, 0, 10, 10, 1).equals(r)); + } + + @Test + public void + convertPointToMeteringRectangle_shouldReturnValidMeteringRectangleForBottomLeftCoord() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 0, 1, PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(0, 89, 10, 10, 1).equals(r)); + } + + @Test + public void + convertPointToMeteringRectangle_shouldReturnValidMeteringRectangleForBottomRightCoord() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1, 1, PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(89, 89, 10, 10, 1).equals(r)); + } + + @Test(expected = AssertionError.class) + public void convertPointToMeteringRectangle_shouldThrowForXUpperBound() { + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1.5, 0, PlatformChannel.DeviceOrientation.PORTRAIT_UP); + } + + @Test(expected = AssertionError.class) + public void convertPointToMeteringRectangle_shouldThrowForXLowerBound() { + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, -0.5, 0, PlatformChannel.DeviceOrientation.PORTRAIT_UP); + } + + @Test(expected = AssertionError.class) + public void convertPointToMeteringRectangle_shouldThrowForYUpperBound() { + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 0, 1.5, PlatformChannel.DeviceOrientation.PORTRAIT_UP); + } + + @Test(expected = AssertionError.class) + public void convertPointToMeteringRectangle_shouldThrowForYLowerBound() { + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 0, -0.5, PlatformChannel.DeviceOrientation.PORTRAIT_UP); + } + + @Test() + public void + convertPointToMeteringRectangle_shouldRotateMeteringRectangleAccordingToUiOrientationForPortraitUp() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1, 1, PlatformChannel.DeviceOrientation.PORTRAIT_UP); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(89, 0, 10, 10, 1).equals(r)); + } + + @Test() + public void + convertPointToMeteringRectangle_shouldRotateMeteringRectangleAccordingToUiOrientationForPortraitDown() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1, 1, PlatformChannel.DeviceOrientation.PORTRAIT_DOWN); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(0, 89, 10, 10, 1).equals(r)); + } + + @Test() + public void + convertPointToMeteringRectangle_shouldRotateMeteringRectangleAccordingToUiOrientationForLandscapeLeft() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1, 1, PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(89, 89, 10, 10, 1).equals(r)); + } + + @Test() + public void + convertPointToMeteringRectangle_shouldRotateMeteringRectangleAccordingToUiOrientationForLandscapeRight() { + MeteringRectangle r = + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 1, 1, PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT); + assertTrue(CameraRegionUtils.MeteringRectangleFactory.create(0, 0, 10, 10, 1).equals(r)); + } + + @Test(expected = AssertionError.class) + public void convertPointToMeteringRectangle_shouldThrowFor0WidthBoundary() { + Size mockCameraBoundaries = mock(Size.class); + when(mockCameraBoundaries.getWidth()).thenReturn(0); + when(mockCameraBoundaries.getHeight()).thenReturn(50); + CameraRegionUtils.convertPointToMeteringRectangle( + mockCameraBoundaries, 0, -0.5, PlatformChannel.DeviceOrientation.PORTRAIT_UP); + } + + @Test(expected = AssertionError.class) + public void convertPointToMeteringRectangle_shouldThrowFor0HeightBoundary() { + Size mockCameraBoundaries = mock(Size.class); + when(mockCameraBoundaries.getWidth()).thenReturn(50); + when(mockCameraBoundaries.getHeight()).thenReturn(0); + CameraRegionUtils.convertPointToMeteringRectangle( + this.mockCameraBoundaries, 0, -0.5, PlatformChannel.DeviceOrientation.PORTRAIT_UP); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_getCameraBoundariesTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_getCameraBoundariesTest.java new file mode 100644 index 000000000000..4c0164981b74 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraRegionUtils_getCameraBoundariesTest.java @@ -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. +package io.flutter.plugins.camera; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.graphics.Rect; +import android.hardware.camera2.CaptureRequest; +import android.os.Build; +import android.util.Size; +import io.flutter.plugins.camera.utils.TestUtils; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.stubbing.Answer; + +public class CameraRegionUtils_getCameraBoundariesTest { + + Size mockCameraBoundaries; + + @Before + public void setUp() { + this.mockCameraBoundaries = mock(Size.class); + when(this.mockCameraBoundaries.getWidth()).thenReturn(100); + when(this.mockCameraBoundaries.getHeight()).thenReturn(100); + } + + @Test + public void getCameraBoundaries_shouldReturnSensorInfoPixelArraySizeWhenRunningPreAndroidP() { + updateSdkVersion(Build.VERSION_CODES.O_MR1); + + try { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + when(mockCameraProperties.getSensorInfoPixelArraySize()).thenReturn(mockCameraBoundaries); + + Size result = CameraRegionUtils.getCameraBoundaries(mockCameraProperties, mockBuilder); + + assertEquals(mockCameraBoundaries, result); + verify(mockCameraProperties, never()).getSensorInfoPreCorrectionActiveArraySize(); + verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); + } finally { + updateSdkVersion(0); + } + } + + @Test + public void + getCameraBoundaries_shouldReturnSensorInfoPixelArraySizeWhenDistortionCorrectionIsNull() { + updateSdkVersion(Build.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(mockCameraBoundaries); + + Size result = CameraRegionUtils.getCameraBoundaries(mockCameraProperties, mockBuilder); + + assertEquals(mockCameraBoundaries, result); + verify(mockCameraProperties, never()).getSensorInfoPreCorrectionActiveArraySize(); + verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); + } finally { + updateSdkVersion(0); + } + } + + @Test + public void + getCameraBoundaries_shouldReturnSensorInfoPixelArraySizeWhenDistortionCorrectionIsOff() { + updateSdkVersion(Build.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(mockCameraBoundaries); + + Size result = CameraRegionUtils.getCameraBoundaries(mockCameraProperties, mockBuilder); + + assertEquals(mockCameraBoundaries, result); + verify(mockCameraProperties, never()).getSensorInfoPreCorrectionActiveArraySize(); + verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); + } finally { + updateSdkVersion(0); + } + } + + @Test + public void + getCameraBoundaries_shouldReturnInfoPreCorrectionActiveArraySizeWhenDistortionCorrectionModeIsSetToNull() { + updateSdkVersion(Build.VERSION_CODES.P); + + try { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + Rect mockSensorInfoPreCorrectionActiveArraySize = mock(Rect.class); + when(mockSensorInfoPreCorrectionActiveArraySize.width()).thenReturn(100); + when(mockSensorInfoPreCorrectionActiveArraySize.height()).thenReturn(100); + + 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(mockSensorInfoPreCorrectionActiveArraySize); + + try (MockedStatic mockedSizeFactory = + mockStatic(CameraRegionUtils.SizeFactory.class)) { + mockedSizeFactory + .when(() -> CameraRegionUtils.SizeFactory.create(anyInt(), anyInt())) + .thenAnswer( + (Answer) + invocation -> { + Size mockSize = mock(Size.class); + when(mockSize.getWidth()).thenReturn(invocation.getArgument(0)); + when(mockSize.getHeight()).thenReturn(invocation.getArgument(1)); + return mockSize; + }); + + Size result = CameraRegionUtils.getCameraBoundaries(mockCameraProperties, mockBuilder); + + assertEquals(100, result.getWidth()); + assertEquals(100, result.getHeight()); + verify(mockCameraProperties, never()).getSensorInfoPixelArraySize(); + verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); + } + } finally { + updateSdkVersion(0); + } + } + + @Test + public void + getCameraBoundaries_shouldReturnInfoPreCorrectionActiveArraySizeWhenDistortionCorrectionModeIsSetToOff() { + updateSdkVersion(Build.VERSION_CODES.P); + + try { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + Rect mockSensorInfoPreCorrectionActiveArraySize = mock(Rect.class); + when(mockSensorInfoPreCorrectionActiveArraySize.width()).thenReturn(100); + when(mockSensorInfoPreCorrectionActiveArraySize.height()).thenReturn(100); + + 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(mockSensorInfoPreCorrectionActiveArraySize); + + try (MockedStatic mockedSizeFactory = + mockStatic(CameraRegionUtils.SizeFactory.class)) { + mockedSizeFactory + .when(() -> CameraRegionUtils.SizeFactory.create(anyInt(), anyInt())) + .thenAnswer( + (Answer) + invocation -> { + Size mockSize = mock(Size.class); + when(mockSize.getWidth()).thenReturn(invocation.getArgument(0)); + when(mockSize.getHeight()).thenReturn(invocation.getArgument(1)); + return mockSize; + }); + + Size result = CameraRegionUtils.getCameraBoundaries(mockCameraProperties, mockBuilder); + + assertEquals(100, result.getWidth()); + assertEquals(100, result.getHeight()); + verify(mockCameraProperties, never()).getSensorInfoPixelArraySize(); + verify(mockCameraProperties, never()).getSensorInfoActiveArraySize(); + } + } finally { + updateSdkVersion(0); + } + } + + @Test + public void + getCameraBoundaries_shouldReturnSensorInfoActiveArraySizeWhenDistortionCorrectionModeIsSet() { + updateSdkVersion(Build.VERSION_CODES.P); + + try { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + Rect mockSensorInfoActiveArraySize = mock(Rect.class); + when(mockSensorInfoActiveArraySize.width()).thenReturn(100); + when(mockSensorInfoActiveArraySize.height()).thenReturn(100); + + 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(mockSensorInfoActiveArraySize); + + try (MockedStatic mockedSizeFactory = + mockStatic(CameraRegionUtils.SizeFactory.class)) { + mockedSizeFactory + .when(() -> CameraRegionUtils.SizeFactory.create(anyInt(), anyInt())) + .thenAnswer( + (Answer) + invocation -> { + Size mockSize = mock(Size.class); + when(mockSize.getWidth()).thenReturn(invocation.getArgument(0)); + when(mockSize.getHeight()).thenReturn(invocation.getArgument(1)); + return mockSize; + }); + + Size result = CameraRegionUtils.getCameraBoundaries(mockCameraProperties, mockBuilder); + + assertEquals(100, result.getWidth()); + assertEquals(100, result.getHeight()); + verify(mockCameraProperties, never()).getSensorInfoPixelArraySize(); + verify(mockCameraProperties, never()).getSensorInfoPreCorrectionActiveArraySize(); + } + } finally { + updateSdkVersion(0); + } + } + + private static void updateSdkVersion(int version) { + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", version); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java new file mode 100644 index 000000000000..9d973195435e --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraTest.java @@ -0,0 +1,915 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraMetadata; +import android.hardware.camera2.CaptureRequest; +import android.media.CamcorderProfile; +import android.media.MediaRecorder; +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import androidx.annotation.NonNull; +import androidx.lifecycle.LifecycleObserver; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugins.camera.features.CameraFeatureFactory; +import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.autofocus.AutoFocusFeature; +import io.flutter.plugins.camera.features.autofocus.FocusMode; +import io.flutter.plugins.camera.features.exposurelock.ExposureLockFeature; +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; +import io.flutter.plugins.camera.features.exposureoffset.ExposureOffsetFeature; +import io.flutter.plugins.camera.features.exposurepoint.ExposurePointFeature; +import io.flutter.plugins.camera.features.flash.FlashFeature; +import io.flutter.plugins.camera.features.flash.FlashMode; +import io.flutter.plugins.camera.features.focuspoint.FocusPointFeature; +import io.flutter.plugins.camera.features.fpsrange.FpsRangeFeature; +import io.flutter.plugins.camera.features.noisereduction.NoiseReductionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionFeature; +import io.flutter.plugins.camera.features.resolution.ResolutionPreset; +import io.flutter.plugins.camera.features.sensororientation.DeviceOrientationManager; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; +import io.flutter.plugins.camera.features.zoomlevel.ZoomLevelFeature; +import io.flutter.plugins.camera.utils.TestUtils; +import io.flutter.view.TextureRegistry; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; + +public class CameraTest { + private CameraProperties mockCameraProperties; + private CameraFeatureFactory mockCameraFeatureFactory; + private DartMessenger mockDartMessenger; + private Camera camera; + private CameraCaptureSession mockCaptureSession; + private CaptureRequest.Builder mockPreviewRequestBuilder; + private MockedStatic mockHandlerThreadFactory; + private HandlerThread mockHandlerThread; + private MockedStatic mockHandlerFactory; + private Handler mockHandler; + + @Before + public void before() { + mockCameraProperties = mock(CameraProperties.class); + mockCameraFeatureFactory = new TestCameraFeatureFactory(); + mockDartMessenger = mock(DartMessenger.class); + mockCaptureSession = mock(CameraCaptureSession.class); + mockPreviewRequestBuilder = mock(CaptureRequest.Builder.class); + mockHandlerThreadFactory = mockStatic(Camera.HandlerThreadFactory.class); + mockHandlerThread = mock(HandlerThread.class); + mockHandlerFactory = mockStatic(Camera.HandlerFactory.class); + mockHandler = mock(Handler.class); + + final Activity mockActivity = mock(Activity.class); + final TextureRegistry.SurfaceTextureEntry mockFlutterTexture = + mock(TextureRegistry.SurfaceTextureEntry.class); + final String cameraName = "1"; + final ResolutionPreset resolutionPreset = ResolutionPreset.high; + final boolean enableAudio = false; + + when(mockCameraProperties.getCameraName()).thenReturn(cameraName); + mockHandlerFactory.when(() -> Camera.HandlerFactory.create(any())).thenReturn(mockHandler); + mockHandlerThreadFactory + .when(() -> Camera.HandlerThreadFactory.create(any())) + .thenReturn(mockHandlerThread); + + camera = + new Camera( + mockActivity, + mockFlutterTexture, + mockCameraFeatureFactory, + mockDartMessenger, + mockCameraProperties, + resolutionPreset, + enableAudio); + + TestUtils.setPrivateField(camera, "captureSession", mockCaptureSession); + TestUtils.setPrivateField(camera, "previewRequestBuilder", mockPreviewRequestBuilder); + } + + @After + public void after() { + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 0); + mockHandlerThreadFactory.close(); + mockHandlerFactory.close(); + } + + @Test + public void shouldNotImplementLifecycleObserverInterface() { + Class cameraClass = Camera.class; + + assertFalse(LifecycleObserver.class.isAssignableFrom(cameraClass)); + } + + @Test + public void shouldCreateCameraPluginAndSetAllFeatures() { + final Activity mockActivity = mock(Activity.class); + final TextureRegistry.SurfaceTextureEntry mockFlutterTexture = + mock(TextureRegistry.SurfaceTextureEntry.class); + final CameraFeatureFactory mockCameraFeatureFactory = mock(CameraFeatureFactory.class); + final String cameraName = "1"; + final ResolutionPreset resolutionPreset = ResolutionPreset.high; + final boolean enableAudio = false; + + when(mockCameraProperties.getCameraName()).thenReturn(cameraName); + SensorOrientationFeature mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + when(mockCameraFeatureFactory.createSensorOrientationFeature(any(), any(), any())) + .thenReturn(mockSensorOrientationFeature); + + Camera camera = + new Camera( + mockActivity, + mockFlutterTexture, + mockCameraFeatureFactory, + mockDartMessenger, + mockCameraProperties, + resolutionPreset, + enableAudio); + + verify(mockCameraFeatureFactory, times(1)) + .createSensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + verify(mockCameraFeatureFactory, times(1)).createAutoFocusFeature(mockCameraProperties, false); + verify(mockCameraFeatureFactory, times(1)).createExposureLockFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)) + .createExposurePointFeature(eq(mockCameraProperties), eq(mockSensorOrientationFeature)); + verify(mockCameraFeatureFactory, times(1)).createExposureOffsetFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)).createFlashFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)) + .createFocusPointFeature(eq(mockCameraProperties), eq(mockSensorOrientationFeature)); + verify(mockCameraFeatureFactory, times(1)).createFpsRangeFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)).createNoiseReductionFeature(mockCameraProperties); + verify(mockCameraFeatureFactory, times(1)) + .createResolutionFeature(mockCameraProperties, resolutionPreset, cameraName); + verify(mockCameraFeatureFactory, times(1)).createZoomLevelFeature(mockCameraProperties); + assertNotNull("should create a camera", camera); + } + + @Test + public void getDeviceOrientationManager() { + SensorOrientationFeature mockSensorOrientationFeature = + mockCameraFeatureFactory.createSensorOrientationFeature(mockCameraProperties, null, null); + DeviceOrientationManager mockDeviceOrientationManager = mock(DeviceOrientationManager.class); + + when(mockSensorOrientationFeature.getDeviceOrientationManager()) + .thenReturn(mockDeviceOrientationManager); + + DeviceOrientationManager actualDeviceOrientationManager = camera.getDeviceOrientationManager(); + + verify(mockSensorOrientationFeature, times(1)).getDeviceOrientationManager(); + assertEquals(mockDeviceOrientationManager, actualDeviceOrientationManager); + } + + @Test + public void getExposureOffsetStepSize() { + ExposureOffsetFeature mockExposureOffsetFeature = + mockCameraFeatureFactory.createExposureOffsetFeature(mockCameraProperties); + double stepSize = 2.3; + + when(mockExposureOffsetFeature.getExposureOffsetStepSize()).thenReturn(stepSize); + + double actualSize = camera.getExposureOffsetStepSize(); + + verify(mockExposureOffsetFeature, times(1)).getExposureOffsetStepSize(); + assertEquals(stepSize, actualSize, 0); + } + + @Test + public void getMaxExposureOffset() { + ExposureOffsetFeature mockExposureOffsetFeature = + mockCameraFeatureFactory.createExposureOffsetFeature(mockCameraProperties); + double expectedMaxOffset = 42.0; + + when(mockExposureOffsetFeature.getMaxExposureOffset()).thenReturn(expectedMaxOffset); + + double actualMaxOffset = camera.getMaxExposureOffset(); + + verify(mockExposureOffsetFeature, times(1)).getMaxExposureOffset(); + assertEquals(expectedMaxOffset, actualMaxOffset, 0); + } + + @Test + public void getMinExposureOffset() { + ExposureOffsetFeature mockExposureOffsetFeature = + mockCameraFeatureFactory.createExposureOffsetFeature(mockCameraProperties); + double expectedMinOffset = 21.5; + + when(mockExposureOffsetFeature.getMinExposureOffset()).thenReturn(21.5); + + double actualMinOffset = camera.getMinExposureOffset(); + + verify(mockExposureOffsetFeature, times(1)).getMinExposureOffset(); + assertEquals(expectedMinOffset, actualMinOffset, 0); + } + + @Test + public void getMaxZoomLevel() { + ZoomLevelFeature mockZoomLevelFeature = + mockCameraFeatureFactory.createZoomLevelFeature(mockCameraProperties); + float expectedMaxZoomLevel = 4.2f; + + when(mockZoomLevelFeature.getMaximumZoomLevel()).thenReturn(expectedMaxZoomLevel); + + float actualMaxZoomLevel = camera.getMaxZoomLevel(); + + verify(mockZoomLevelFeature, times(1)).getMaximumZoomLevel(); + assertEquals(expectedMaxZoomLevel, actualMaxZoomLevel, 0); + } + + @Test + public void getMinZoomLevel() { + ZoomLevelFeature mockZoomLevelFeature = + mockCameraFeatureFactory.createZoomLevelFeature(mockCameraProperties); + float expectedMinZoomLevel = 4.2f; + + when(mockZoomLevelFeature.getMinimumZoomLevel()).thenReturn(expectedMinZoomLevel); + + float actualMinZoomLevel = camera.getMinZoomLevel(); + + verify(mockZoomLevelFeature, times(1)).getMinimumZoomLevel(); + assertEquals(expectedMinZoomLevel, actualMinZoomLevel, 0); + } + + @Test + public void getRecordingProfile() { + ResolutionFeature mockResolutionFeature = + mockCameraFeatureFactory.createResolutionFeature(mockCameraProperties, null, null); + CamcorderProfile mockCamcorderProfile = mock(CamcorderProfile.class); + + when(mockResolutionFeature.getRecordingProfile()).thenReturn(mockCamcorderProfile); + + CamcorderProfile actualRecordingProfile = camera.getRecordingProfile(); + + verify(mockResolutionFeature, times(1)).getRecordingProfile(); + assertEquals(mockCamcorderProfile, actualRecordingProfile); + } + + @Test + public void setExposureMode_shouldUpdateExposureLockFeature() { + ExposureLockFeature mockExposureLockFeature = + mockCameraFeatureFactory.createExposureLockFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + ExposureMode exposureMode = ExposureMode.locked; + + camera.setExposureMode(mockResult, exposureMode); + + verify(mockExposureLockFeature, times(1)).setValue(exposureMode); + verify(mockResult, never()).error(any(), any(), any()); + verify(mockResult, times(1)).success(null); + } + + @Test + public void setExposureMode_shouldUpdateBuilder() { + ExposureLockFeature mockExposureLockFeature = + mockCameraFeatureFactory.createExposureLockFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + ExposureMode exposureMode = ExposureMode.locked; + + camera.setExposureMode(mockResult, exposureMode); + + verify(mockExposureLockFeature, times(1)).updateBuilder(any()); + } + + @Test + public void setExposureMode_shouldCallErrorOnResultOnCameraAccessException() + throws CameraAccessException { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + ExposureMode exposureMode = ExposureMode.locked; + when(mockCaptureSession.setRepeatingRequest(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + + camera.setExposureMode(mockResult, exposureMode); + + verify(mockResult, never()).success(any()); + verify(mockResult, times(1)) + .error("setExposureModeFailed", "Could not set exposure mode.", null); + } + + @Test + public void setExposurePoint_shouldUpdateExposurePointFeature() { + SensorOrientationFeature mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + ExposurePointFeature mockExposurePointFeature = + mockCameraFeatureFactory.createExposurePointFeature( + mockCameraProperties, mockSensorOrientationFeature); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + Point point = new Point(42d, 42d); + + camera.setExposurePoint(mockResult, point); + + verify(mockExposurePointFeature, times(1)).setValue(point); + verify(mockResult, never()).error(any(), any(), any()); + verify(mockResult, times(1)).success(null); + } + + @Test + public void setExposurePoint_shouldUpdateBuilder() { + SensorOrientationFeature mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + ExposurePointFeature mockExposurePointFeature = + mockCameraFeatureFactory.createExposurePointFeature( + mockCameraProperties, mockSensorOrientationFeature); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + Point point = new Point(42d, 42d); + + camera.setExposurePoint(mockResult, point); + + verify(mockExposurePointFeature, times(1)).updateBuilder(any()); + } + + @Test + public void setExposurePoint_shouldCallErrorOnResultOnCameraAccessException() + throws CameraAccessException { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + Point point = new Point(42d, 42d); + when(mockCaptureSession.setRepeatingRequest(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + + camera.setExposurePoint(mockResult, point); + + verify(mockResult, never()).success(any()); + verify(mockResult, times(1)) + .error("setExposurePointFailed", "Could not set exposure point.", null); + } + + @Test + public void setFlashMode_shouldUpdateFlashFeature() { + FlashFeature mockFlashFeature = + mockCameraFeatureFactory.createFlashFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + FlashMode flashMode = FlashMode.always; + + camera.setFlashMode(mockResult, flashMode); + + verify(mockFlashFeature, times(1)).setValue(flashMode); + verify(mockResult, never()).error(any(), any(), any()); + verify(mockResult, times(1)).success(null); + } + + @Test + public void setFlashMode_shouldUpdateBuilder() { + FlashFeature mockFlashFeature = + mockCameraFeatureFactory.createFlashFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + FlashMode flashMode = FlashMode.always; + + camera.setFlashMode(mockResult, flashMode); + + verify(mockFlashFeature, times(1)).updateBuilder(any()); + } + + @Test + public void setFlashMode_shouldCallErrorOnResultOnCameraAccessException() + throws CameraAccessException { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + FlashMode flashMode = FlashMode.always; + when(mockCaptureSession.setRepeatingRequest(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + + camera.setFlashMode(mockResult, flashMode); + + verify(mockResult, never()).success(any()); + verify(mockResult, times(1)).error("setFlashModeFailed", "Could not set flash mode.", null); + } + + @Test + public void setFocusPoint_shouldUpdateFocusPointFeature() { + SensorOrientationFeature mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + FocusPointFeature mockFocusPointFeature = + mockCameraFeatureFactory.createFocusPointFeature( + mockCameraProperties, mockSensorOrientationFeature); + AutoFocusFeature mockAutoFocusFeature = + mockCameraFeatureFactory.createAutoFocusFeature(mockCameraProperties, false); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + Point point = new Point(42d, 42d); + when(mockAutoFocusFeature.getValue()).thenReturn(FocusMode.auto); + + camera.setFocusPoint(mockResult, point); + + verify(mockFocusPointFeature, times(1)).setValue(point); + verify(mockResult, never()).error(any(), any(), any()); + verify(mockResult, times(1)).success(null); + } + + @Test + public void setFocusPoint_shouldUpdateBuilder() { + SensorOrientationFeature mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + FocusPointFeature mockFocusPointFeature = + mockCameraFeatureFactory.createFocusPointFeature( + mockCameraProperties, mockSensorOrientationFeature); + AutoFocusFeature mockAutoFocusFeature = + mockCameraFeatureFactory.createAutoFocusFeature(mockCameraProperties, false); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + Point point = new Point(42d, 42d); + when(mockAutoFocusFeature.getValue()).thenReturn(FocusMode.auto); + + camera.setFocusPoint(mockResult, point); + + verify(mockFocusPointFeature, times(1)).updateBuilder(any()); + } + + @Test + public void setFocusPoint_shouldCallErrorOnResultOnCameraAccessException() + throws CameraAccessException { + AutoFocusFeature mockAutoFocusFeature = + mockCameraFeatureFactory.createAutoFocusFeature(mockCameraProperties, false); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + Point point = new Point(42d, 42d); + when(mockAutoFocusFeature.getValue()).thenReturn(FocusMode.auto); + when(mockCaptureSession.setRepeatingRequest(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + + camera.setFocusPoint(mockResult, point); + + verify(mockResult, never()).success(any()); + verify(mockResult, times(1)).error("setFocusPointFailed", "Could not set focus point.", null); + } + + @Test + public void setZoomLevel_shouldUpdateZoomLevelFeature() throws CameraAccessException { + ZoomLevelFeature mockZoomLevelFeature = + mockCameraFeatureFactory.createZoomLevelFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + float zoomLevel = 1.0f; + + when(mockZoomLevelFeature.getValue()).thenReturn(zoomLevel); + when(mockZoomLevelFeature.getMinimumZoomLevel()).thenReturn(0f); + when(mockZoomLevelFeature.getMaximumZoomLevel()).thenReturn(2f); + + camera.setZoomLevel(mockResult, zoomLevel); + + verify(mockZoomLevelFeature, times(1)).setValue(zoomLevel); + verify(mockResult, never()).error(any(), any(), any()); + verify(mockResult, times(1)).success(null); + } + + @Test + public void setZoomLevel_shouldUpdateBuilder() throws CameraAccessException { + ZoomLevelFeature mockZoomLevelFeature = + mockCameraFeatureFactory.createZoomLevelFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + float zoomLevel = 1.0f; + + when(mockZoomLevelFeature.getValue()).thenReturn(zoomLevel); + when(mockZoomLevelFeature.getMinimumZoomLevel()).thenReturn(0f); + when(mockZoomLevelFeature.getMaximumZoomLevel()).thenReturn(2f); + + camera.setZoomLevel(mockResult, zoomLevel); + + verify(mockZoomLevelFeature, times(1)).updateBuilder(any()); + } + + @Test + public void setZoomLevel_shouldCallErrorOnResultOnCameraAccessException() + throws CameraAccessException { + ZoomLevelFeature mockZoomLevelFeature = + mockCameraFeatureFactory.createZoomLevelFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + float zoomLevel = 1.0f; + + when(mockZoomLevelFeature.getValue()).thenReturn(zoomLevel); + when(mockZoomLevelFeature.getMinimumZoomLevel()).thenReturn(0f); + when(mockZoomLevelFeature.getMaximumZoomLevel()).thenReturn(2f); + when(mockCaptureSession.setRepeatingRequest(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + + camera.setZoomLevel(mockResult, zoomLevel); + + verify(mockResult, never()).success(any()); + verify(mockResult, times(1)).error("setZoomLevelFailed", "Could not set zoom level.", null); + } + + @Test + public void pauseVideoRecording_shouldSendNullResultWhenNotRecording() { + TestUtils.setPrivateField(camera, "recordingVideo", false); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.pauseVideoRecording(mockResult); + + verify(mockResult, times(1)).success(null); + verify(mockResult, never()).error(any(), any(), any()); + } + + @Test + public void pauseVideoRecording_shouldCallPauseWhenRecordingAndOnAPIN() { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + TestUtils.setPrivateField(camera, "mediaRecorder", mockMediaRecorder); + TestUtils.setPrivateField(camera, "recordingVideo", true); + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 24); + + camera.pauseVideoRecording(mockResult); + + verify(mockMediaRecorder, times(1)).pause(); + verify(mockResult, times(1)).success(null); + verify(mockResult, never()).error(any(), any(), any()); + } + + @Test + public void pauseVideoRecording_shouldSendVideoRecordingFailedErrorWhenVersionCodeSmallerThenN() { + TestUtils.setPrivateField(camera, "recordingVideo", true); + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 23); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.pauseVideoRecording(mockResult); + + verify(mockResult, times(1)) + .error("videoRecordingFailed", "pauseVideoRecording requires Android API +24.", null); + verify(mockResult, never()).success(any()); + } + + @Test + public void + pauseVideoRecording_shouldSendVideoRecordingFailedErrorWhenMediaRecorderPauseThrowsIllegalStateException() { + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + TestUtils.setPrivateField(camera, "mediaRecorder", mockMediaRecorder); + TestUtils.setPrivateField(camera, "recordingVideo", true); + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 24); + + IllegalStateException expectedException = new IllegalStateException("Test error message"); + + doThrow(expectedException).when(mockMediaRecorder).pause(); + + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.pauseVideoRecording(mockResult); + + verify(mockResult, times(1)).error("videoRecordingFailed", "Test error message", null); + verify(mockResult, never()).success(any()); + } + + @Test + public void resumeVideoRecording_shouldSendNullResultWhenNotRecording() { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + TestUtils.setPrivateField(camera, "recordingVideo", false); + + camera.resumeVideoRecording(mockResult); + + verify(mockResult, times(1)).success(null); + verify(mockResult, never()).error(any(), any(), any()); + } + + @Test + public void resumeVideoRecording_shouldCallPauseWhenRecordingAndOnAPIN() { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + TestUtils.setPrivateField(camera, "mediaRecorder", mockMediaRecorder); + TestUtils.setPrivateField(camera, "recordingVideo", true); + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 24); + + camera.resumeVideoRecording(mockResult); + + verify(mockMediaRecorder, times(1)).resume(); + verify(mockResult, times(1)).success(null); + verify(mockResult, never()).error(any(), any(), any()); + } + + @Test + public void + resumeVideoRecording_shouldSendVideoRecordingFailedErrorWhenVersionCodeSmallerThanN() { + TestUtils.setPrivateField(camera, "recordingVideo", true); + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 23); + + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.resumeVideoRecording(mockResult); + + verify(mockResult, times(1)) + .error("videoRecordingFailed", "resumeVideoRecording requires Android API +24.", null); + verify(mockResult, never()).success(any()); + } + + @Test + public void + resumeVideoRecording_shouldSendVideoRecordingFailedErrorWhenMediaRecorderPauseThrowsIllegalStateException() { + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + TestUtils.setPrivateField(camera, "mediaRecorder", mockMediaRecorder); + TestUtils.setPrivateField(camera, "recordingVideo", true); + TestUtils.setFinalStatic(Build.VERSION.class, "SDK_INT", 24); + + IllegalStateException expectedException = new IllegalStateException("Test error message"); + + doThrow(expectedException).when(mockMediaRecorder).resume(); + + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.resumeVideoRecording(mockResult); + + verify(mockResult, times(1)).error("videoRecordingFailed", "Test error message", null); + verify(mockResult, never()).success(any()); + } + + @Test + public void setFocusMode_shouldUpdateAutoFocusFeature() { + AutoFocusFeature mockAutoFocusFeature = + mockCameraFeatureFactory.createAutoFocusFeature(mockCameraProperties, false); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.setFocusMode(mockResult, FocusMode.auto); + + verify(mockAutoFocusFeature, times(1)).setValue(FocusMode.auto); + verify(mockResult, never()).error(any(), any(), any()); + verify(mockResult, times(1)).success(null); + } + + @Test + public void setFocusMode_shouldUpdateBuilder() { + AutoFocusFeature mockAutoFocusFeature = + mockCameraFeatureFactory.createAutoFocusFeature(mockCameraProperties, false); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.setFocusMode(mockResult, FocusMode.auto); + + verify(mockAutoFocusFeature, times(1)).updateBuilder(any()); + } + + @Test + public void setFocusMode_shouldUnlockAutoFocusForAutoMode() { + camera.setFocusMode(mock(MethodChannel.Result.class), FocusMode.auto); + verify(mockPreviewRequestBuilder, times(1)) + .set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + verify(mockPreviewRequestBuilder, times(1)) + .set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + } + + @Test + public void setFocusMode_shouldSkipUnlockAutoFocusWhenNullCaptureSession() { + TestUtils.setPrivateField(camera, "captureSession", null); + camera.setFocusMode(mock(MethodChannel.Result.class), FocusMode.auto); + verify(mockPreviewRequestBuilder, never()) + .set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_CANCEL); + verify(mockPreviewRequestBuilder, never()) + .set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_IDLE); + } + + @Test + public void setFocusMode_shouldSendErrorEventOnUnlockAutoFocusCameraAccessException() + throws CameraAccessException { + when(mockCaptureSession.capture(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + camera.setFocusMode(mock(MethodChannel.Result.class), FocusMode.auto); + verify(mockDartMessenger, times(1)).sendCameraErrorEvent(any()); + } + + @Test + public void setFocusMode_shouldLockAutoFocusForLockedMode() throws CameraAccessException { + camera.setFocusMode(mock(MethodChannel.Result.class), FocusMode.locked); + verify(mockPreviewRequestBuilder, times(1)) + .set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START); + verify(mockCaptureSession, times(1)).capture(any(), any(), any()); + verify(mockCaptureSession, times(1)).setRepeatingRequest(any(), any(), any()); + } + + @Test + public void setFocusMode_shouldSkipLockAutoFocusWhenNullCaptureSession() { + TestUtils.setPrivateField(camera, "captureSession", null); + camera.setFocusMode(mock(MethodChannel.Result.class), FocusMode.locked); + verify(mockPreviewRequestBuilder, never()) + .set(CaptureRequest.CONTROL_AF_TRIGGER, CaptureRequest.CONTROL_AF_TRIGGER_START); + } + + @Test + public void setFocusMode_shouldSendErrorEventOnLockAutoFocusCameraAccessException() + throws CameraAccessException { + when(mockCaptureSession.capture(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + camera.setFocusMode(mock(MethodChannel.Result.class), FocusMode.locked); + verify(mockDartMessenger, times(1)).sendCameraErrorEvent(any()); + } + + @Test + public void setFocusMode_shouldCallErrorOnResultOnCameraAccessException() + throws CameraAccessException { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + when(mockCaptureSession.setRepeatingRequest(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + + camera.setFocusMode(mockResult, FocusMode.locked); + + verify(mockResult, never()).success(any()); + verify(mockResult, times(1)) + .error("setFocusModeFailed", "Error setting focus mode: null", null); + } + + @Test + public void setExposureOffset_shouldUpdateExposureOffsetFeature() { + ExposureOffsetFeature mockExposureOffsetFeature = + mockCameraFeatureFactory.createExposureOffsetFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + when(mockExposureOffsetFeature.getValue()).thenReturn(1.0); + + camera.setExposureOffset(mockResult, 1.0); + + verify(mockExposureOffsetFeature, times(1)).setValue(1.0); + verify(mockResult, never()).error(any(), any(), any()); + verify(mockResult, times(1)).success(1.0); + } + + @Test + public void setExposureOffset_shouldAndUpdateBuilder() { + ExposureOffsetFeature mockExposureOffsetFeature = + mockCameraFeatureFactory.createExposureOffsetFeature(mockCameraProperties); + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + camera.setExposureOffset(mockResult, 1.0); + + verify(mockExposureOffsetFeature, times(1)).updateBuilder(any()); + } + + @Test + public void setExposureOffset_shouldCallErrorOnResultOnCameraAccessException() + throws CameraAccessException { + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + when(mockCaptureSession.setRepeatingRequest(any(), any(), any())) + .thenThrow(new CameraAccessException(0, "")); + + camera.setExposureOffset(mockResult, 1.0); + + verify(mockResult, never()).success(any()); + verify(mockResult, times(1)) + .error("setExposureOffsetFailed", "Could not set exposure offset.", null); + } + + @Test + public void lockCaptureOrientation_shouldLockCaptureOrientation() { + final Activity mockActivity = mock(Activity.class); + SensorOrientationFeature mockSensorOrientationFeature = + mockCameraFeatureFactory.createSensorOrientationFeature( + mockCameraProperties, mockActivity, mockDartMessenger); + + camera.lockCaptureOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_UP); + + verify(mockSensorOrientationFeature, times(1)) + .lockCaptureOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_UP); + } + + @Test + public void unlockCaptureOrientation_shouldUnlockCaptureOrientation() { + final Activity mockActivity = mock(Activity.class); + SensorOrientationFeature mockSensorOrientationFeature = + mockCameraFeatureFactory.createSensorOrientationFeature( + mockCameraProperties, mockActivity, mockDartMessenger); + + camera.unlockCaptureOrientation(); + + verify(mockSensorOrientationFeature, times(1)).unlockCaptureOrientation(); + } + + @Test + public void pausePreview_shouldPausePreview() throws CameraAccessException { + camera.pausePreview(); + + assertEquals(TestUtils.getPrivateField(camera, "pausedPreview"), true); + verify(mockCaptureSession, times(1)).stopRepeating(); + } + + @Test + public void resumePreview_shouldResumePreview() throws CameraAccessException { + camera.resumePreview(); + + assertEquals(TestUtils.getPrivateField(camera, "pausedPreview"), false); + verify(mockCaptureSession, times(1)).setRepeatingRequest(any(), any(), any()); + } + + @Test + public void resumePreview_shouldSendErrorEventOnCameraAccessException() + throws CameraAccessException { + when(mockCaptureSession.setRepeatingRequest(any(), any(), any())) + .thenThrow(new CameraAccessException(0)); + + camera.resumePreview(); + + verify(mockDartMessenger, times(1)).sendCameraErrorEvent(any()); + } + + @Test + public void startBackgroundThread_shouldStartNewThread() { + camera.startBackgroundThread(); + + verify(mockHandlerThread, times(1)).start(); + assertEquals(mockHandler, TestUtils.getPrivateField(camera, "backgroundHandler")); + } + + @Test + public void startBackgroundThread_shouldNotStartNewThreadWhenAlreadyCreated() { + camera.startBackgroundThread(); + camera.startBackgroundThread(); + + verify(mockHandlerThread, times(1)).start(); + } + + private static class TestCameraFeatureFactory implements CameraFeatureFactory { + private final AutoFocusFeature mockAutoFocusFeature; + private final ExposureLockFeature mockExposureLockFeature; + private final ExposureOffsetFeature mockExposureOffsetFeature; + private final ExposurePointFeature mockExposurePointFeature; + private final FlashFeature mockFlashFeature; + private final FocusPointFeature mockFocusPointFeature; + private final FpsRangeFeature mockFpsRangeFeature; + private final NoiseReductionFeature mockNoiseReductionFeature; + private final ResolutionFeature mockResolutionFeature; + private final SensorOrientationFeature mockSensorOrientationFeature; + private final ZoomLevelFeature mockZoomLevelFeature; + + public TestCameraFeatureFactory() { + this.mockAutoFocusFeature = mock(AutoFocusFeature.class); + this.mockExposureLockFeature = mock(ExposureLockFeature.class); + this.mockExposureOffsetFeature = mock(ExposureOffsetFeature.class); + this.mockExposurePointFeature = mock(ExposurePointFeature.class); + this.mockFlashFeature = mock(FlashFeature.class); + this.mockFocusPointFeature = mock(FocusPointFeature.class); + this.mockFpsRangeFeature = mock(FpsRangeFeature.class); + this.mockNoiseReductionFeature = mock(NoiseReductionFeature.class); + this.mockResolutionFeature = mock(ResolutionFeature.class); + this.mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + this.mockZoomLevelFeature = mock(ZoomLevelFeature.class); + } + + @Override + public AutoFocusFeature createAutoFocusFeature( + @NonNull CameraProperties cameraProperties, boolean recordingVideo) { + return mockAutoFocusFeature; + } + + @Override + public ExposureLockFeature createExposureLockFeature( + @NonNull CameraProperties cameraProperties) { + return mockExposureLockFeature; + } + + @Override + public ExposureOffsetFeature createExposureOffsetFeature( + @NonNull CameraProperties cameraProperties) { + return mockExposureOffsetFeature; + } + + @Override + public FlashFeature createFlashFeature(@NonNull CameraProperties cameraProperties) { + return mockFlashFeature; + } + + @Override + public ResolutionFeature createResolutionFeature( + @NonNull CameraProperties cameraProperties, + ResolutionPreset initialSetting, + String cameraName) { + return mockResolutionFeature; + } + + @Override + public FocusPointFeature createFocusPointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrienttionFeature) { + return mockFocusPointFeature; + } + + @Override + public FpsRangeFeature createFpsRangeFeature(@NonNull CameraProperties cameraProperties) { + return mockFpsRangeFeature; + } + + @Override + public SensorOrientationFeature createSensorOrientationFeature( + @NonNull CameraProperties cameraProperties, + @NonNull Activity activity, + @NonNull DartMessenger dartMessenger) { + return mockSensorOrientationFeature; + } + + @Override + public ZoomLevelFeature createZoomLevelFeature(@NonNull CameraProperties cameraProperties) { + return mockZoomLevelFeature; + } + + @Override + public ExposurePointFeature createExposurePointFeature( + @NonNull CameraProperties cameraProperties, + @NonNull SensorOrientationFeature sensorOrientationFeature) { + return mockExposurePointFeature; + } + + @Override + public NoiseReductionFeature createNoiseReductionFeature( + @NonNull CameraProperties cameraProperties) { + return mockNoiseReductionFeature; + } + } +} diff --git a/packages/camera/camera/android/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..e59b05bf4fe3 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraUtilsTest.java @@ -0,0 +1,100 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.content.Context; +import android.hardware.camera2.CameraAccessException; +import android.hardware.camera2.CameraCharacteristics; +import android.hardware.camera2.CameraManager; +import android.hardware.camera2.CameraMetadata; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import java.util.List; +import java.util.Map; +import org.junit.Test; + +public class CameraUtilsTest { + + @Test + public void serializeDeviceOrientation_serializesCorrectly() { + assertEquals( + "portraitUp", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_UP)); + assertEquals( + "portraitDown", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.PORTRAIT_DOWN)); + assertEquals( + "landscapeLeft", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT)); + assertEquals( + "landscapeRight", + CameraUtils.serializeDeviceOrientation(PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT)); + } + + @Test(expected = UnsupportedOperationException.class) + public void serializeDeviceOrientation_throws_for_null() { + CameraUtils.serializeDeviceOrientation(null); + } + + @Test + public void deserializeDeviceOrientation_deserializesCorrectly() { + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_UP, + CameraUtils.deserializeDeviceOrientation("portraitUp")); + assertEquals( + PlatformChannel.DeviceOrientation.PORTRAIT_DOWN, + CameraUtils.deserializeDeviceOrientation("portraitDown")); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT, + CameraUtils.deserializeDeviceOrientation("landscapeLeft")); + assertEquals( + PlatformChannel.DeviceOrientation.LANDSCAPE_RIGHT, + CameraUtils.deserializeDeviceOrientation("landscapeRight")); + } + + @Test(expected = UnsupportedOperationException.class) + public void deserializeDeviceOrientation_throwsForNull() { + CameraUtils.deserializeDeviceOrientation(null); + } + + @Test + public void getAvailableCameras_retrievesValidCameras() + throws CameraAccessException, NumberFormatException { + final Activity mockActivity = mock(Activity.class); + final CameraManager mockCameraManager = mock(CameraManager.class); + final CameraCharacteristics mockCameraCharacteristics = mock(CameraCharacteristics.class); + final String[] mockCameraIds = {"1394902", "-192930", "0283835", "foobar"}; + final int mockSensorOrientation0 = 90; + final int mockSensorOrientation2 = 270; + final int mockLensFacing0 = CameraMetadata.LENS_FACING_FRONT; + final int mockLensFacing2 = CameraMetadata.LENS_FACING_EXTERNAL; + + when(mockActivity.getSystemService(Context.CAMERA_SERVICE)).thenReturn(mockCameraManager); + when(mockCameraManager.getCameraIdList()).thenReturn(mockCameraIds); + when(mockCameraManager.getCameraCharacteristics(anyString())) + .thenReturn(mockCameraCharacteristics); + when(mockCameraCharacteristics.get(any())) + .thenReturn(mockSensorOrientation0) + .thenReturn(mockLensFacing0) + .thenReturn(mockSensorOrientation2) + .thenReturn(mockLensFacing2); + + List> availableCameras = CameraUtils.getAvailableCameras(mockActivity); + + assertEquals(availableCameras.size(), 2); + assertEquals(availableCameras.get(0).get("name"), "1394902"); + assertEquals(availableCameras.get(0).get("sensorOrientation"), mockSensorOrientation0); + assertEquals(availableCameras.get(0).get("lensFacing"), "front"); + assertEquals(availableCameras.get(1).get("name"), "0283835"); + assertEquals(availableCameras.get(1).get("sensorOrientation"), mockSensorOrientation2); + assertEquals(availableCameras.get(1).get("lensFacing"), "external"); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/CameraZoomTest.java new file mode 100644 index 000000000000..d3e495551608 --- /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_whenParametersAreValid() { + 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_whenSensorSizeIsNull() { + 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_whenMaxZoomIsNull() { + 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_whenMaxZoomIsSmallerThenDefaultZoomFactor() { + 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_whenNoSupportShouldNotSetScalerCropRegion() { + final CameraZoom cameraZoom = new CameraZoom(null, null); + final Rect computedZoom = cameraZoom.computeZoom(2f); + + assertNull(computedZoom); + } + + @Test + public void setZoom_whenSensorSizeEqualsZeroShouldReturnCropRegionOfZero() { + 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_whenSensorSizeIsValidShouldReturnCropRegion() { + 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_whenZoomIsGreaterThenMaxZoomClampToMaxZoom() { + 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_whenZoomIsSmallerThenMinZoomClampToMinZoom() { + 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..0a2fc43d03cb --- /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.features.autofocus.FocusMode; +import io.flutter.plugins.camera.features.exposurelock.ExposureMode; +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/ImageSaverTests.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/ImageSaverTests.java new file mode 100644 index 000000000000..0358ce6cb785 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/ImageSaverTests.java @@ -0,0 +1,105 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.media.Image; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +public class ImageSaverTests { + + Image mockImage; + File mockFile; + ImageSaver.Callback mockCallback; + ImageSaver imageSaver; + Image.Plane mockPlane; + ByteBuffer mockBuffer; + MockedStatic mockFileOutputStreamFactory; + FileOutputStream mockFileOutputStream; + + @Before + public void setup() { + // Set up mocked file dependency + mockFile = mock(File.class); + when(mockFile.getAbsolutePath()).thenReturn("absolute/path"); + mockPlane = mock(Image.Plane.class); + mockBuffer = mock(ByteBuffer.class); + when(mockBuffer.remaining()).thenReturn(3); + when(mockBuffer.get(any())) + .thenAnswer( + new Answer() { + @Override + public Object answer(InvocationOnMock invocation) throws Throwable { + byte[] bytes = invocation.getArgument(0); + bytes[0] = 0x42; + bytes[1] = 0x00; + bytes[2] = 0x13; + return mockBuffer; + } + }); + + // Set up mocked image dependency + mockImage = mock(Image.class); + when(mockPlane.getBuffer()).thenReturn(mockBuffer); + when(mockImage.getPlanes()).thenReturn(new Image.Plane[] {mockPlane}); + + // Set up mocked FileOutputStream + mockFileOutputStreamFactory = mockStatic(ImageSaver.FileOutputStreamFactory.class); + mockFileOutputStream = mock(FileOutputStream.class); + mockFileOutputStreamFactory + .when(() -> ImageSaver.FileOutputStreamFactory.create(any())) + .thenReturn(mockFileOutputStream); + + // Set up testable ImageSaver instance + mockCallback = mock(ImageSaver.Callback.class); + imageSaver = new ImageSaver(mockImage, mockFile, mockCallback); + } + + @After + public void teardown() { + mockFileOutputStreamFactory.close(); + } + + @Test + public void runWritesBytesToFileAndFinishesWithPath() throws IOException { + imageSaver.run(); + + verify(mockFileOutputStream, times(1)).write(new byte[] {0x42, 0x00, 0x13}); + verify(mockCallback, times(1)).onComplete("absolute/path"); + verify(mockCallback, never()).onError(any(), any()); + } + + @Test + public void runCallsErrorOnWriteIoexception() throws IOException { + doThrow(new IOException()).when(mockFileOutputStream).write(any()); + imageSaver.run(); + verify(mockCallback, times(1)).onError("IOError", "Failed saving image"); + verify(mockCallback, never()).onComplete(any()); + } + + @Test + public void runCallsErrorOnCloseIoexception() throws IOException { + doThrow(new IOException("message")).when(mockFileOutputStream).close(); + imageSaver.run(); + verify(mockCallback, times(1)).onError("cameraAccess", "message"); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java new file mode 100644 index 000000000000..868e2e9e6d57 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/MethodCallHandlerImplTest.java @@ -0,0 +1,77 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera; + +import static org.junit.Assert.assertFalse; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import android.app.Activity; +import android.hardware.camera2.CameraAccessException; +import androidx.lifecycle.LifecycleObserver; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugins.camera.utils.TestUtils; +import io.flutter.view.TextureRegistry; +import org.junit.Before; +import org.junit.Test; + +public class MethodCallHandlerImplTest { + + MethodChannel.MethodCallHandler handler; + MethodChannel.Result mockResult; + Camera mockCamera; + + @Before + public void setUp() { + handler = + new MethodCallHandlerImpl( + mock(Activity.class), + mock(BinaryMessenger.class), + mock(CameraPermissions.class), + mock(CameraPermissions.PermissionsRegistry.class), + mock(TextureRegistry.class)); + mockResult = mock(MethodChannel.Result.class); + mockCamera = mock(Camera.class); + TestUtils.setPrivateField(handler, "camera", mockCamera); + } + + @Test + public void shouldNotImplementLifecycleObserverInterface() { + Class methodCallHandlerClass = MethodCallHandlerImpl.class; + + assertFalse(LifecycleObserver.class.isAssignableFrom(methodCallHandlerClass)); + } + + @Test + public void onMethodCall_pausePreview_shouldPausePreviewAndSendSuccessResult() + throws CameraAccessException { + handler.onMethodCall(new MethodCall("pausePreview", null), mockResult); + + verify(mockCamera, times(1)).pausePreview(); + verify(mockResult, times(1)).success(null); + } + + @Test + public void onMethodCall_pausePreview_shouldSendErrorResultOnCameraAccessException() + throws CameraAccessException { + doThrow(new CameraAccessException(0)).when(mockCamera).pausePreview(); + + handler.onMethodCall(new MethodCall("pausePreview", null), mockResult); + + verify(mockResult, times(1)).error("CameraAccess", null, null); + } + + @Test + public void onMethodCall_resumePreview_shouldResumePreviewAndSendSuccessResult() { + handler.onMethodCall(new MethodCall("resumePreview", null), mockResult); + + verify(mockCamera, times(1)).resumePreview(); + verify(mockResult, times(1)).success(null); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/autofocus/AutoFocusFeatureTest.java new file mode 100644 index 000000000000..fd8ef7c766a2 --- /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_shouldReturnTheNameOfTheFeature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); + + assertEquals("AutoFocusFeature", autoFocusFeature.getDebugName()); + } + + @Test + public void getValue_shouldReturnAutoIfNotSet() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + AutoFocusFeature autoFocusFeature = new AutoFocusFeature(mockCameraProperties, false); + + assertEquals(FocusMode.auto, autoFocusFeature.getValue()); + } + + @Test + public void getValue_shouldEchoTheSetValue() { + 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_shouldReturnFalseWhenMinimumFocusDistanceIsZero() { + 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_shouldReturnFalseWhenMinimumFocusDistanceIsNull() { + 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_shouldReturnFalseWhenNoFocusModesAreAvailable() { + 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_shouldReturnFalseWhenOnlyFocusOffIsAvailable() { + 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_shouldReturnTrueWhenOnlyMultipleFocusModesAreAvailable() { + 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 updateBuilderShouldReturnWhenCheckIsSupportedIsFalse() { + 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_shouldSetControlModeToAutoWhenFocusIsLocked() { + 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_shouldSetControlModeToContinuousVideoWhenFocusIsAutoAndRecordingVideo() { + 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_shouldSetControlModeToContinuousVideoWhenFocusIsAutoAndNotRecordingVideo() { + 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..f68ae7140601 --- /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_returnsCorrectValues() { + 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_returnsNullForNonexistantValue() { + assertEquals( + "Returns null for 'nonexistant'", FocusMode.getValueForString("nonexistant"), null); + } + + @Test + public void toString_returnsCorrectValue() { + 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..1cda0a86d575 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurelock/ExposureLockFeatureTest.java @@ -0,0 +1,77 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.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_shouldReturnTheNameOfTheFeature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); + + assertEquals("ExposureLockFeature", exposureLockFeature.getDebugName()); + } + + @Test + public void getValue_shouldReturnAutoIfNotSet() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); + + assertEquals(ExposureMode.auto, exposureLockFeature.getValue()); + } + + @Test + public void getValue_shouldEchoTheSetValue() { + 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_shouldReturnTrue() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposureLockFeature exposureLockFeature = new ExposureLockFeature(mockCameraProperties); + + assertTrue(exposureLockFeature.checkIsSupported()); + } + + @Test + public void updateBuilder_shouldSetControlAeLockToFalseWhenAutoExposureIsSetToAuto() { + 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_shouldSetControlAeLockToFalseWhenAutoExposureIsSetToLocked() { + 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..d5d47697776c --- /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_returnsCorrectValues() { + 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_returnsNullForNonexistantValue() { + assertEquals( + "Returns null for 'nonexistant'", ExposureMode.getValueForString("nonexistant"), null); + } + + @Test + public void toString_returnsCorrectValue() { + 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..ee428f3d5e02 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposureoffset/ExposureOffsetFeatureTest.java @@ -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. + +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_shouldReturnTheNameOfTheFeature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); + + assertEquals("ExposureOffsetFeature", exposureOffsetFeature.getDebugName()); + } + + @Test + public void getValue_shouldReturnZeroIfNotSet() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); + + final double actualValue = exposureOffsetFeature.getValue(); + + assertEquals(0.0, actualValue, 0); + } + + @Test + public void getValue_shouldEchoTheSetValue() { + 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_shouldReturnTheControlExposureCompensationStepValue() { + 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_shouldReturnTrue() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposureOffsetFeature exposureOffsetFeature = new ExposureOffsetFeature(mockCameraProperties); + + assertTrue(exposureOffsetFeature.checkIsSupported()); + } + + @Test + public void updateBuilder_shouldSetControlAeExposureCompensationToOffset() { + 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..b34a04fe26b7 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/exposurepoint/ExposurePointFeatureTest.java @@ -0,0 +1,316 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isNull; +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 android.util.Size; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.CameraRegionUtils; +import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.sensororientation.DeviceOrientationManager; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +public class ExposurePointFeatureTest { + + Size mockCameraBoundaries; + SensorOrientationFeature mockSensorOrientationFeature; + DeviceOrientationManager mockDeviceOrientationManager; + + @Before + public void setUp() { + this.mockCameraBoundaries = mock(Size.class); + when(this.mockCameraBoundaries.getWidth()).thenReturn(100); + when(this.mockCameraBoundaries.getHeight()).thenReturn(100); + mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + mockDeviceOrientationManager = mock(DeviceOrientationManager.class); + when(mockSensorOrientationFeature.getDeviceOrientationManager()) + .thenReturn(mockDeviceOrientationManager); + when(mockDeviceOrientationManager.getLastUIOrientation()) + .thenReturn(PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); + } + + @Test + public void getDebugName_shouldReturnTheNameOfTheFeature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); + + assertEquals("ExposurePointFeature", exposurePointFeature.getDebugName()); + } + + @Test + public void getValue_shouldReturnNullIfNotSet() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); + assertNull(exposurePointFeature.getValue()); + } + + @Test + public void getValue_shouldEchoTheSetValue() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); + exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); + Point expectedPoint = new Point(0.0, 0.0); + + exposurePointFeature.setValue(expectedPoint); + Point actualPoint = exposurePointFeature.getValue(); + + assertEquals(expectedPoint, actualPoint); + } + + @Test + public void setValue_shouldResetPointWhenXCoordIsNull() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); + exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); + + exposurePointFeature.setValue(new Point(null, 0.0)); + + assertNull(exposurePointFeature.getValue()); + } + + @Test + public void setValue_shouldResetPointWhenYCoordIsNull() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); + exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); + + exposurePointFeature.setValue(new Point(0.0, null)); + + assertNull(exposurePointFeature.getValue()); + } + + @Test + public void setValue_shouldSetPointWhenValidCoordsAreSupplied() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); + exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); + Point point = new Point(0.0, 0.0); + + exposurePointFeature.setValue(point); + + assertEquals(point, exposurePointFeature.getValue()); + } + + @Test + public void setValue_shouldDetermineMeteringRectangleWhenValidBoundariesAndCoordsAreSupplied() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); + Size mockedCameraBoundaries = mock(Size.class); + exposurePointFeature.setCameraBoundaries(mockedCameraBoundaries); + + try (MockedStatic mockedCameraRegionUtils = + Mockito.mockStatic(CameraRegionUtils.class)) { + + exposurePointFeature.setValue(new Point(0.5, 0.5)); + + mockedCameraRegionUtils.verify( + () -> + CameraRegionUtils.convertPointToMeteringRectangle( + mockedCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT), + times(1)); + } + } + + @Test(expected = AssertionError.class) + public void setValue_shouldThrowAssertionErrorWhenNoValidBoundariesAreSet() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); + + try (MockedStatic mockedCameraRegionUtils = + Mockito.mockStatic(CameraRegionUtils.class)) { + exposurePointFeature.setValue(new Point(0.5, 0.5)); + } + } + + @Test + public void setValue_shouldNotDetermineMeteringRectangleWhenNullCoordsAreSet() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); + Size mockedCameraBoundaries = mock(Size.class); + exposurePointFeature.setCameraBoundaries(mockedCameraBoundaries); + + try (MockedStatic mockedCameraRegionUtils = + Mockito.mockStatic(CameraRegionUtils.class)) { + + exposurePointFeature.setValue(null); + exposurePointFeature.setValue(new Point(null, 0.5)); + exposurePointFeature.setValue(new Point(0.5, null)); + + mockedCameraRegionUtils.verifyNoInteractions(); + } + } + + @Test + public void + setCameraBoundaries_shouldDetermineMeteringRectangleWhenValidBoundariesAndCoordsAreSupplied() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); + exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); + exposurePointFeature.setValue(new Point(0.5, 0.5)); + Size mockedCameraBoundaries = mock(Size.class); + + try (MockedStatic mockedCameraRegionUtils = + Mockito.mockStatic(CameraRegionUtils.class)) { + + exposurePointFeature.setCameraBoundaries(mockedCameraBoundaries); + + mockedCameraRegionUtils.verify( + () -> + CameraRegionUtils.convertPointToMeteringRectangle( + mockedCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT), + times(1)); + } + } + + @Test + public void checkIsSupported_shouldReturnFalseWhenMaxRegionsIsNull() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); + exposurePointFeature.setCameraBoundaries(new Size(100, 100)); + + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(null); + + assertFalse(exposurePointFeature.checkIsSupported()); + } + + @Test + public void checkIsSupported_shouldReturnFalseWhenMaxRegionsIsZero() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); + exposurePointFeature.setCameraBoundaries(new Size(100, 100)); + + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(0); + + assertFalse(exposurePointFeature.checkIsSupported()); + } + + @Test + public void checkIsSupported_shouldReturnTrueWhenMaxRegionsIsBiggerThenZero() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); + exposurePointFeature.setCameraBoundaries(new Size(100, 100)); + + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); + + assertTrue(exposurePointFeature.checkIsSupported()); + } + + @Test + public void updateBuilder_shouldReturnWhenCheckIsSupportedIsFalse() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); + + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(0); + + exposurePointFeature.updateBuilder(mockCaptureRequestBuilder); + + verify(mockCaptureRequestBuilder, never()).set(any(), any()); + } + + @Test + public void updateBuilder_shouldSetMeteringRectangleWhenValidBoundariesAndCoordsAreSupplied() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); + CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); + Size mockedCameraBoundaries = mock(Size.class); + MeteringRectangle mockedMeteringRectangle = mock(MeteringRectangle.class); + + try (MockedStatic mockedCameraRegionUtils = + Mockito.mockStatic(CameraRegionUtils.class)) { + mockedCameraRegionUtils + .when( + () -> + CameraRegionUtils.convertPointToMeteringRectangle( + mockedCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT)) + .thenReturn(mockedMeteringRectangle); + exposurePointFeature.setCameraBoundaries(mockedCameraBoundaries); + exposurePointFeature.setValue(new Point(0.5, 0.5)); + + exposurePointFeature.updateBuilder(mockCaptureRequestBuilder); + } + + verify(mockCaptureRequestBuilder, times(1)) + .set(CaptureRequest.CONTROL_AE_REGIONS, new MeteringRectangle[] {mockedMeteringRectangle}); + } + + @Test + public void updateBuilder_shouldNotSetMeteringRectangleWhenNoValidBoundariesAreSupplied() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); + CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); + + exposurePointFeature.updateBuilder(mockCaptureRequestBuilder); + + verify(mockCaptureRequestBuilder, times(1)).set(any(), isNull()); + } + + @Test + public void updateBuilder_shouldNotSetMeteringRectangleWhenNoValidCoordsAreSupplied() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoExposure()).thenReturn(1); + CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); + ExposurePointFeature exposurePointFeature = + new ExposurePointFeature(mockCameraProperties, mockSensorOrientationFeature); + exposurePointFeature.setCameraBoundaries(this.mockCameraBoundaries); + + exposurePointFeature.setValue(null); + exposurePointFeature.updateBuilder(mockCaptureRequestBuilder); + exposurePointFeature.setValue(new Point(0d, null)); + exposurePointFeature.updateBuilder(mockCaptureRequestBuilder); + exposurePointFeature.setValue(new Point(null, 0d)); + exposurePointFeature.updateBuilder(mockCaptureRequestBuilder); + verify(mockCaptureRequestBuilder, times(3)).set(any(), isNull()); + } +} 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..f2b4ffc8197c --- /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_shouldReturnTheNameOfTheFeature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + assertEquals("FlashFeature", flashFeature.getDebugName()); + } + + @Test + public void getValue_shouldReturnAutoIfNotSet() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + assertEquals(FlashMode.auto, flashFeature.getValue()); + } + + @Test + public void getValue_shouldEchoTheSetValue() { + 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_shouldReturnFalseWhenFlashInfoAvailableIsNull() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + when(mockCameraProperties.getFlashInfoAvailable()).thenReturn(null); + + assertFalse(flashFeature.checkIsSupported()); + } + + @Test + public void checkIsSupported_shouldReturnFalseWhenFlashInfoAvailableIsFalse() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + when(mockCameraProperties.getFlashInfoAvailable()).thenReturn(false); + + assertFalse(flashFeature.checkIsSupported()); + } + + @Test + public void checkIsSupported_shouldReturnTrueWhenFlashInfoAvailableIsTrue() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FlashFeature flashFeature = new FlashFeature(mockCameraProperties); + + when(mockCameraProperties.getFlashInfoAvailable()).thenReturn(true); + + assertTrue(flashFeature.checkIsSupported()); + } + + @Test + public void updateBuilder_shouldReturnWhenCheckIsSupportedIsFalse() { + 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_shouldSetAeModeAndFlashModeWhenFlashModeIsOff() { + 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_shouldSetAeModeAndFlashModeWhenFlashModeIsAlways() { + 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_shouldSetAeModeAndFlashModeWhenFlashModeIsTorch() { + 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_shouldSetAeModeAndFlashModeWhenFlashModeIsAuto() { + 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/focuspoint/FocusPointFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java new file mode 100644 index 000000000000..f03dc9f62e87 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/focuspoint/FocusPointFeatureTest.java @@ -0,0 +1,318 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.features.focuspoint; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.isNull; +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 android.util.Size; +import io.flutter.embedding.engine.systemchannels.PlatformChannel; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.CameraRegionUtils; +import io.flutter.plugins.camera.features.Point; +import io.flutter.plugins.camera.features.sensororientation.DeviceOrientationManager; +import io.flutter.plugins.camera.features.sensororientation.SensorOrientationFeature; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +public class FocusPointFeatureTest { + + Size mockCameraBoundaries; + SensorOrientationFeature mockSensorOrientationFeature; + DeviceOrientationManager mockDeviceOrientationManager; + + @Before + public void setUp() { + this.mockCameraBoundaries = mock(Size.class); + when(this.mockCameraBoundaries.getWidth()).thenReturn(100); + when(this.mockCameraBoundaries.getHeight()).thenReturn(100); + mockSensorOrientationFeature = mock(SensorOrientationFeature.class); + mockDeviceOrientationManager = mock(DeviceOrientationManager.class); + when(mockSensorOrientationFeature.getDeviceOrientationManager()) + .thenReturn(mockDeviceOrientationManager); + when(mockDeviceOrientationManager.getLastUIOrientation()) + .thenReturn(PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT); + } + + @Test + public void getDebugName_shouldReturnTheNameOfTheFeature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); + + assertEquals("FocusPointFeature", focusPointFeature.getDebugName()); + } + + @Test + public void getValue_shouldReturnNullIfNotSet() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); + Point actualPoint = focusPointFeature.getValue(); + assertNull(focusPointFeature.getValue()); + } + + @Test + public void getValue_shouldEchoTheSetValue() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); + focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); + Point expectedPoint = new Point(0.0, 0.0); + + focusPointFeature.setValue(expectedPoint); + Point actualPoint = focusPointFeature.getValue(); + + assertEquals(expectedPoint, actualPoint); + } + + @Test + public void setValue_shouldResetPointWhenXCoordIsNull() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); + focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); + + focusPointFeature.setValue(new Point(null, 0.0)); + + assertNull(focusPointFeature.getValue()); + } + + @Test + public void setValue_shouldResetPointWhenYCoordIsNull() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); + focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); + + focusPointFeature.setValue(new Point(0.0, null)); + + assertNull(focusPointFeature.getValue()); + } + + @Test + public void setValue_shouldSetPointWhenValidCoordsAreSupplied() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); + focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); + Point point = new Point(0.0, 0.0); + + focusPointFeature.setValue(point); + + assertEquals(point, focusPointFeature.getValue()); + } + + @Test + public void setValue_shouldDetermineMeteringRectangleWhenValidBoundariesAndCoordsAreSupplied() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); + Size mockedCameraBoundaries = mock(Size.class); + focusPointFeature.setCameraBoundaries(mockedCameraBoundaries); + + try (MockedStatic mockedCameraRegionUtils = + Mockito.mockStatic(CameraRegionUtils.class)) { + + focusPointFeature.setValue(new Point(0.5, 0.5)); + + mockedCameraRegionUtils.verify( + () -> + CameraRegionUtils.convertPointToMeteringRectangle( + mockedCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT), + times(1)); + } + } + + @Test(expected = AssertionError.class) + public void setValue_shouldThrowAssertionErrorWhenNoValidBoundariesAreSet() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); + + try (MockedStatic mockedCameraRegionUtils = + Mockito.mockStatic(CameraRegionUtils.class)) { + focusPointFeature.setValue(new Point(0.5, 0.5)); + } + } + + @Test + public void setValue_shouldNotDetermineMeteringRectangleWhenNullCoordsAreSet() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); + Size mockedCameraBoundaries = mock(Size.class); + focusPointFeature.setCameraBoundaries(mockedCameraBoundaries); + + try (MockedStatic mockedCameraRegionUtils = + Mockito.mockStatic(CameraRegionUtils.class)) { + + focusPointFeature.setValue(null); + focusPointFeature.setValue(new Point(null, 0.5)); + focusPointFeature.setValue(new Point(0.5, null)); + + mockedCameraRegionUtils.verifyNoInteractions(); + } + } + + @Test + public void + setCameraBoundaries_shouldDetermineMeteringRectangleWhenValidBoundariesAndCoordsAreSupplied() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); + focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); + focusPointFeature.setValue(new Point(0.5, 0.5)); + Size mockedCameraBoundaries = mock(Size.class); + + try (MockedStatic mockedCameraRegionUtils = + Mockito.mockStatic(CameraRegionUtils.class)) { + + focusPointFeature.setCameraBoundaries(mockedCameraBoundaries); + + mockedCameraRegionUtils.verify( + () -> + CameraRegionUtils.convertPointToMeteringRectangle( + mockedCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT), + times(1)); + } + } + + @Test + public void checkIsSupported_shouldReturnFalseWhenMaxRegionsIsNull() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); + focusPointFeature.setCameraBoundaries(new Size(100, 100)); + + when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(null); + + assertFalse(focusPointFeature.checkIsSupported()); + } + + @Test + public void checkIsSupported_shouldReturnFalseWhenMaxRegionsIsZero() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); + focusPointFeature.setCameraBoundaries(new Size(100, 100)); + + when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(0); + + assertFalse(focusPointFeature.checkIsSupported()); + } + + @Test + public void checkIsSupported_shouldReturnTrueWhenMaxRegionsIsBiggerThenZero() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); + focusPointFeature.setCameraBoundaries(new Size(100, 100)); + + when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); + + assertTrue(focusPointFeature.checkIsSupported()); + } + + @Test + public void updateBuilder_shouldReturnWhenCheckIsSupportedIsFalse() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); + + when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(0); + + focusPointFeature.updateBuilder(mockCaptureRequestBuilder); + + verify(mockCaptureRequestBuilder, never()).set(any(), any()); + } + + @Test + public void updateBuilder_shouldSetMeteringRectangleWhenValidBoundariesAndCoordsAreSupplied() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); + CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); + Size mockedCameraBoundaries = mock(Size.class); + MeteringRectangle mockedMeteringRectangle = mock(MeteringRectangle.class); + + try (MockedStatic mockedCameraRegionUtils = + Mockito.mockStatic(CameraRegionUtils.class)) { + mockedCameraRegionUtils + .when( + () -> + CameraRegionUtils.convertPointToMeteringRectangle( + mockedCameraBoundaries, + 0.5, + 0.5, + PlatformChannel.DeviceOrientation.LANDSCAPE_LEFT)) + .thenReturn(mockedMeteringRectangle); + focusPointFeature.setCameraBoundaries(mockedCameraBoundaries); + focusPointFeature.setValue(new Point(0.5, 0.5)); + + focusPointFeature.updateBuilder(mockCaptureRequestBuilder); + } + + verify(mockCaptureRequestBuilder, times(1)) + .set(CaptureRequest.CONTROL_AE_REGIONS, new MeteringRectangle[] {mockedMeteringRectangle}); + } + + @Test + public void updateBuilder_shouldNotSetMeteringRectangleWhenNoValidBoundariesAreSupplied() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); + CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); + MeteringRectangle mockedMeteringRectangle = mock(MeteringRectangle.class); + + focusPointFeature.updateBuilder(mockCaptureRequestBuilder); + + verify(mockCaptureRequestBuilder, times(1)).set(any(), isNull()); + } + + @Test + public void updateBuilder_shouldNotSetMeteringRectangleWhenNoValidCoordsAreSupplied() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + when(mockCameraProperties.getControlMaxRegionsAutoFocus()).thenReturn(1); + CaptureRequest.Builder mockCaptureRequestBuilder = mock(CaptureRequest.Builder.class); + FocusPointFeature focusPointFeature = + new FocusPointFeature(mockCameraProperties, mockSensorOrientationFeature); + focusPointFeature.setCameraBoundaries(this.mockCameraBoundaries); + + focusPointFeature.setValue(null); + focusPointFeature.updateBuilder(mockCaptureRequestBuilder); + focusPointFeature.setValue(new Point(0d, null)); + focusPointFeature.updateBuilder(mockCaptureRequestBuilder); + focusPointFeature.setValue(new Point(null, 0d)); + focusPointFeature.updateBuilder(mockCaptureRequestBuilder); + verify(mockCaptureRequestBuilder, times(3)).set(any(), isNull()); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java new file mode 100644 index 000000000000..93cfe5523df3 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeaturePixel4aTest.java @@ -0,0 +1,30 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.features.fpsrange; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.mock; + +import android.os.Build; +import android.util.Range; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.utils.TestUtils; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +@RunWith(RobolectricTestRunner.class) +public class FpsRangeFeaturePixel4aTest { + @Test + public void ctor_shouldInitializeFpsRangeWith30WhenDeviceIsPixel4a() { + TestUtils.setFinalStatic(Build.class, "BRAND", "google"); + TestUtils.setFinalStatic(Build.class, "MODEL", "Pixel 4a"); + + FpsRangeFeature fpsRangeFeature = new FpsRangeFeature(mock(CameraProperties.class)); + Range range = fpsRangeFeature.getValue(); + assertEquals(30, (int) range.getLower()); + assertEquals(30, (int) range.getUpper()); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java new file mode 100644 index 000000000000..2bb4d849a277 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/fpsrange/FpsRangeFeatureTest.java @@ -0,0 +1,108 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.features.fpsrange; + +import static org.junit.Assert.assertEquals; +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.verify; +import static org.mockito.Mockito.when; + +import android.hardware.camera2.CaptureRequest; +import android.os.Build; +import android.util.Range; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.utils.TestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class FpsRangeFeatureTest { + @Before + public void before() { + TestUtils.setFinalStatic(Build.class, "BRAND", "Test Brand"); + TestUtils.setFinalStatic(Build.class, "MODEL", "Test Model"); + } + + @After + public void after() { + TestUtils.setFinalStatic(Build.class, "BRAND", null); + TestUtils.setFinalStatic(Build.class, "MODEL", null); + } + + @Test + public void ctor_shouldInitializeFpsRangeWithHighestUpperValueFromRangeArray() { + FpsRangeFeature fpsRangeFeature = createTestInstance(); + assertEquals(13, (int) fpsRangeFeature.getValue().getUpper()); + } + + @Test + public void getDebugName_shouldReturnTheNameOfTheFeature() { + FpsRangeFeature fpsRangeFeature = createTestInstance(); + assertEquals("FpsRangeFeature", fpsRangeFeature.getDebugName()); + } + + @Test + public void getValue_shouldReturnHighestUpperRangeIfNotSet() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FpsRangeFeature fpsRangeFeature = createTestInstance(); + + assertEquals(13, (int) fpsRangeFeature.getValue().getUpper()); + } + + @Test + public void getValue_shouldEchoTheSetValue() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + FpsRangeFeature fpsRangeFeature = new FpsRangeFeature(mockCameraProperties); + @SuppressWarnings("unchecked") + Range expectedValue = mock(Range.class); + + fpsRangeFeature.setValue(expectedValue); + Range actualValue = fpsRangeFeature.getValue(); + + assertEquals(expectedValue, actualValue); + } + + @Test + public void checkIsSupported_shouldReturnTrue() { + FpsRangeFeature fpsRangeFeature = createTestInstance(); + assertTrue(fpsRangeFeature.checkIsSupported()); + } + + @Test + @SuppressWarnings("unchecked") + public void updateBuilder_shouldSetAeTargetFpsRange() { + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + FpsRangeFeature fpsRangeFeature = createTestInstance(); + + fpsRangeFeature.updateBuilder(mockBuilder); + + verify(mockBuilder).set(eq(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE), any(Range.class)); + } + + private static FpsRangeFeature createTestInstance() { + @SuppressWarnings("unchecked") + Range rangeOne = mock(Range.class); + @SuppressWarnings("unchecked") + Range rangeTwo = mock(Range.class); + @SuppressWarnings("unchecked") + Range rangeThree = mock(Range.class); + + when(rangeOne.getUpper()).thenReturn(11); + when(rangeTwo.getUpper()).thenReturn(12); + when(rangeThree.getUpper()).thenReturn(13); + + @SuppressWarnings("unchecked") + Range[] ranges = new Range[] {rangeOne, rangeTwo, rangeThree}; + + CameraProperties cameraProperties = mock(CameraProperties.class); + + when(cameraProperties.getControlAutoExposureAvailableTargetFpsRanges()).thenReturn(ranges); + + return new FpsRangeFeature(cameraProperties); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java new file mode 100644 index 000000000000..b89aad0f6773 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/noisereduction/NoiseReductionFeatureTest.java @@ -0,0 +1,150 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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.noisereduction; + +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 android.os.Build.VERSION; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.utils.TestUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +public class NoiseReductionFeatureTest { + @Before + public void before() { + // Make sure the VERSION.SDK_INT field returns 23, to allow using all available + // noise reduction modes in tests. + TestUtils.setFinalStatic(VERSION.class, "SDK_INT", 23); + } + + @After + public void after() { + // Make sure we reset the VERSION.SDK_INT field to it's original value. + TestUtils.setFinalStatic(VERSION.class, "SDK_INT", 0); + } + + @Test + public void getDebugName_shouldReturnTheNameOfTheFeature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); + + assertEquals("NoiseReductionFeature", noiseReductionFeature.getDebugName()); + } + + @Test + public void getValue_shouldReturnFastIfNotSet() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); + + assertEquals(NoiseReductionMode.fast, noiseReductionFeature.getValue()); + } + + @Test + public void getValue_shouldEchoTheSetValue() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); + NoiseReductionMode expectedValue = NoiseReductionMode.fast; + + noiseReductionFeature.setValue(expectedValue); + NoiseReductionMode actualValue = noiseReductionFeature.getValue(); + + assertEquals(expectedValue, actualValue); + } + + @Test + public void checkIsSupported_shouldReturnFalseWhenAvailableNoiseReductionModesIsNull() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); + + when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(null); + + assertFalse(noiseReductionFeature.checkIsSupported()); + } + + @Test + public void + checkIsSupported_shouldReturnFalseWhenAvailableNoiseReductionModesReturnsAnEmptyArray() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); + + when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(new int[] {}); + + assertFalse(noiseReductionFeature.checkIsSupported()); + } + + @Test + public void + checkIsSupported_shouldReturnTrueWhenAvailableNoiseReductionModesReturnsAtLeastOneItem() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); + + when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(new int[] {1}); + + assertTrue(noiseReductionFeature.checkIsSupported()); + } + + @Test + public void updateBuilder_shouldReturnWhenCheckIsSupportedIsFalse() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); + + when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(new int[] {}); + + noiseReductionFeature.updateBuilder(mockBuilder); + + verify(mockBuilder, never()).set(any(), any()); + } + + @Test + public void updateBuilder_shouldSetNoiseReductionModeOffWhenOff() { + testUpdateBuilderWith(NoiseReductionMode.off, CaptureRequest.NOISE_REDUCTION_MODE_OFF); + } + + @Test + public void updateBuilder_shouldSetNoiseReductionModeFastWhenFast() { + testUpdateBuilderWith(NoiseReductionMode.fast, CaptureRequest.NOISE_REDUCTION_MODE_FAST); + } + + @Test + public void updateBuilder_shouldSetNoiseReductionModeHighQualityWhenHighQuality() { + testUpdateBuilderWith( + NoiseReductionMode.highQuality, CaptureRequest.NOISE_REDUCTION_MODE_HIGH_QUALITY); + } + + @Test + public void updateBuilder_shouldSetNoiseReductionModeMinimalWhenMinimal() { + testUpdateBuilderWith(NoiseReductionMode.minimal, CaptureRequest.NOISE_REDUCTION_MODE_MINIMAL); + } + + @Test + public void updateBuilder_shouldSetNoiseReductionModeZeroShutterLagWhenZeroShutterLag() { + testUpdateBuilderWith( + NoiseReductionMode.zeroShutterLag, CaptureRequest.NOISE_REDUCTION_MODE_ZERO_SHUTTER_LAG); + } + + private static void testUpdateBuilderWith(NoiseReductionMode mode, int expectedResult) { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + CaptureRequest.Builder mockBuilder = mock(CaptureRequest.Builder.class); + NoiseReductionFeature noiseReductionFeature = new NoiseReductionFeature(mockCameraProperties); + + when(mockCameraProperties.getAvailableNoiseReductionModes()).thenReturn(new int[] {1}); + + noiseReductionFeature.setValue(mode); + noiseReductionFeature.updateBuilder(mockBuilder); + verify(mockBuilder, times(1)).set(CaptureRequest.NOISE_REDUCTION_MODE, expectedResult); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java new file mode 100644 index 000000000000..e09223dfabe9 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/resolution/ResolutionFeatureTest.java @@ -0,0 +1,190 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.camera.features.resolution; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; + +import android.media.CamcorderProfile; +import io.flutter.plugins.camera.CameraProperties; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; + +public class ResolutionFeatureTest { + private static final String cameraName = "1"; + private CamcorderProfile mockProfileLow; + private MockedStatic mockedStaticProfile; + + @Before + public void before() { + mockedStaticProfile = mockStatic(CamcorderProfile.class); + mockProfileLow = mock(CamcorderProfile.class); + CamcorderProfile mockProfile = mock(CamcorderProfile.class); + + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_HIGH)) + .thenReturn(true); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_2160P)) + .thenReturn(true); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_1080P)) + .thenReturn(true); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_720P)) + .thenReturn(true); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_480P)) + .thenReturn(true); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_QVGA)) + .thenReturn(true); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_LOW)) + .thenReturn(true); + + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_HIGH)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_2160P)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_1080P)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_480P)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_QVGA)) + .thenReturn(mockProfile); + mockedStaticProfile + .when(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_LOW)) + .thenReturn(mockProfileLow); + } + + @After + public void after() { + mockedStaticProfile.reset(); + mockedStaticProfile.close(); + } + + @Test + public void getDebugName_shouldReturnTheNameOfTheFeature() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ResolutionFeature resolutionFeature = + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); + + assertEquals("ResolutionFeature", resolutionFeature.getDebugName()); + } + + @Test + public void getValue_shouldReturnInitialValueWhenNotSet() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ResolutionFeature resolutionFeature = + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); + + assertEquals(ResolutionPreset.max, resolutionFeature.getValue()); + } + + @Test + public void getValue_shouldEchoSetValue() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ResolutionFeature resolutionFeature = + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); + + resolutionFeature.setValue(ResolutionPreset.high); + + assertEquals(ResolutionPreset.high, resolutionFeature.getValue()); + } + + @Test + public void checkIsSupport_returnsTrue() { + CameraProperties mockCameraProperties = mock(CameraProperties.class); + ResolutionFeature resolutionFeature = + new ResolutionFeature(mockCameraProperties, ResolutionPreset.max, cameraName); + + assertTrue(resolutionFeature.checkIsSupported()); + } + + @Test + public void getBestAvailableCamcorderProfileForResolutionPreset_shouldFallThrough() { + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_HIGH)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_2160P)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_1080P)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_720P)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_480P)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_QVGA)) + .thenReturn(false); + mockedStaticProfile + .when(() -> CamcorderProfile.hasProfile(1, CamcorderProfile.QUALITY_LOW)) + .thenReturn(true); + + assertEquals( + mockProfileLow, + ResolutionFeature.getBestAvailableCamcorderProfileForResolutionPreset( + 1, ResolutionPreset.max)); + } + + @Test + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetMax() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.max); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); + } + + @Test + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetUltraHigh() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.ultraHigh); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); + } + + @Test + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetVeryHigh() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.veryHigh); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); + } + + @Test + public void computeBestPreviewSize_shouldUse720PWhenResolutionPresetHigh() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.high); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_720P)); + } + + @Test + public void computeBestPreviewSize_shouldUse480PWhenResolutionPresetMedium() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.medium); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_480P)); + } + + @Test + public void computeBestPreviewSize_shouldUseQVGAWhenResolutionPresetLow() { + ResolutionFeature.computeBestPreviewSize(1, ResolutionPreset.low); + + mockedStaticProfile.verify(() -> CamcorderProfile.get(1, CamcorderProfile.QUALITY_QVGA)); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java new file mode 100644 index 000000000000..58f17cb758bf --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/DeviceOrientationManagerTest.java @@ -0,0 +1,300 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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.sensororientation; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.provider.Settings; +import android.view.Display; +import android.view.Surface; +import android.view.WindowManager; +import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; +import io.flutter.plugins.camera.DartMessenger; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; + +public class DeviceOrientationManagerTest { + private Activity mockActivity; + private DartMessenger mockDartMessenger; + private WindowManager mockWindowManager; + private Display mockDisplay; + private DeviceOrientationManager deviceOrientationManager; + + @Before + public void before() { + mockActivity = mock(Activity.class); + mockDartMessenger = mock(DartMessenger.class); + mockDisplay = mock(Display.class); + mockWindowManager = mock(WindowManager.class); + + when(mockActivity.getSystemService(Context.WINDOW_SERVICE)).thenReturn(mockWindowManager); + when(mockWindowManager.getDefaultDisplay()).thenReturn(mockDisplay); + + deviceOrientationManager = + DeviceOrientationManager.create(mockActivity, mockDartMessenger, false, 0); + } + + @Test + public void getVideoOrientation_whenNaturalScreenOrientationEqualsPortraitUp() { + int degreesPortraitUp = + deviceOrientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitDown = + deviceOrientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_DOWN); + int degreesLandscapeLeft = + deviceOrientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_LEFT); + int degreesLandscapeRight = + deviceOrientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); + + assertEquals(0, degreesPortraitUp); + assertEquals(90, degreesLandscapeLeft); + assertEquals(180, degreesPortraitDown); + assertEquals(270, degreesLandscapeRight); + } + + @Test + public void getVideoOrientation_whenNaturalScreenOrientationEqualsLandscapeLeft() { + DeviceOrientationManager orientationManager = + DeviceOrientationManager.create(mockActivity, mockDartMessenger, false, 90); + + int degreesPortraitUp = orientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitDown = + orientationManager.getVideoOrientation(DeviceOrientation.PORTRAIT_DOWN); + int degreesLandscapeLeft = + orientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_LEFT); + int degreesLandscapeRight = + orientationManager.getVideoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); + + assertEquals(90, degreesPortraitUp); + assertEquals(180, degreesLandscapeLeft); + assertEquals(270, degreesPortraitDown); + assertEquals(0, degreesLandscapeRight); + } + + @Test + public void getVideoOrientation_shouldFallbackToSensorOrientationWhenOrientationIsNull() { + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + + int degrees = deviceOrientationManager.getVideoOrientation(null); + + assertEquals(90, degrees); + } + + @Test + public void getPhotoOrientation_whenNaturalScreenOrientationEqualsPortraitUp() { + int degreesPortraitUp = + deviceOrientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitDown = + deviceOrientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_DOWN); + int degreesLandscapeLeft = + deviceOrientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_LEFT); + int degreesLandscapeRight = + deviceOrientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); + + assertEquals(0, degreesPortraitUp); + assertEquals(90, degreesLandscapeRight); + assertEquals(180, degreesPortraitDown); + assertEquals(270, degreesLandscapeLeft); + } + + @Test + public void getPhotoOrientation_whenNaturalScreenOrientationEqualsLandscapeLeft() { + DeviceOrientationManager orientationManager = + DeviceOrientationManager.create(mockActivity, mockDartMessenger, false, 90); + + int degreesPortraitUp = orientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_UP); + int degreesPortraitDown = + orientationManager.getPhotoOrientation(DeviceOrientation.PORTRAIT_DOWN); + int degreesLandscapeLeft = + orientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_LEFT); + int degreesLandscapeRight = + orientationManager.getPhotoOrientation(DeviceOrientation.LANDSCAPE_RIGHT); + + assertEquals(90, degreesPortraitUp); + assertEquals(180, degreesLandscapeRight); + assertEquals(270, degreesPortraitDown); + assertEquals(0, degreesLandscapeLeft); + } + + @Test + public void getPhotoOrientation_shouldFallbackToCurrentOrientationWhenOrientationIsNull() { + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + + int degrees = deviceOrientationManager.getPhotoOrientation(null); + + assertEquals(270, degrees); + } + + @Test + public void handleUIOrientationChange_shouldSendMessageWhenSensorAccessIsAllowed() { + try (MockedStatic mockedSystem = mockStatic(Settings.System.class)) { + mockedSystem + .when( + () -> + Settings.System.getInt(any(), eq(Settings.System.ACCELEROMETER_ROTATION), eq(0))) + .thenReturn(0); + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + + deviceOrientationManager.handleUIOrientationChange(); + } + + verify(mockDartMessenger, times(1)) + .sendDeviceOrientationChangeEvent(DeviceOrientation.LANDSCAPE_LEFT); + } + + @Test + public void handleOrientationChange_shouldSendMessageWhenOrientationIsUpdated() { + DeviceOrientation previousOrientation = DeviceOrientation.PORTRAIT_UP; + DeviceOrientation newOrientation = DeviceOrientation.LANDSCAPE_LEFT; + + DeviceOrientationManager.handleOrientationChange( + newOrientation, previousOrientation, mockDartMessenger); + + verify(mockDartMessenger, times(1)).sendDeviceOrientationChangeEvent(newOrientation); + } + + @Test + public void handleOrientationChange_shouldNotSendMessageWhenOrientationIsNotUpdated() { + DeviceOrientation previousOrientation = DeviceOrientation.PORTRAIT_UP; + DeviceOrientation newOrientation = DeviceOrientation.PORTRAIT_UP; + + DeviceOrientationManager.handleOrientationChange( + newOrientation, previousOrientation, mockDartMessenger); + + verify(mockDartMessenger, never()).sendDeviceOrientationChangeEvent(any()); + } + + @Test + public void getUIOrientation() { + // Orientation portrait and rotation of 0 should translate to "PORTRAIT_UP". + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + DeviceOrientation uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.PORTRAIT_UP, uiOrientation); + + // Orientation portrait and rotation of 90 should translate to "PORTRAIT_UP". + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_90); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.PORTRAIT_UP, uiOrientation); + + // Orientation portrait and rotation of 180 should translate to "PORTRAIT_DOWN". + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_180); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.PORTRAIT_DOWN, uiOrientation); + + // Orientation portrait and rotation of 270 should translate to "PORTRAIT_DOWN". + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_270); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.PORTRAIT_DOWN, uiOrientation); + + // Orientation landscape and rotation of 0 should translate to "LANDSCAPE_LEFT". + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.LANDSCAPE_LEFT, uiOrientation); + + // Orientation landscape and rotation of 90 should translate to "LANDSCAPE_LEFT". + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_90); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.LANDSCAPE_LEFT, uiOrientation); + + // Orientation landscape and rotation of 180 should translate to "LANDSCAPE_RIGHT". + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_180); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.LANDSCAPE_RIGHT, uiOrientation); + + // Orientation landscape and rotation of 270 should translate to "LANDSCAPE_RIGHT". + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_270); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.LANDSCAPE_RIGHT, uiOrientation); + + // Orientation undefined should default to "PORTRAIT_UP". + setUpUIOrientationMocks(Configuration.ORIENTATION_UNDEFINED, Surface.ROTATION_0); + uiOrientation = deviceOrientationManager.getUIOrientation(); + assertEquals(DeviceOrientation.PORTRAIT_UP, uiOrientation); + } + + @Test + public void getDeviceDefaultOrientation() { + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + int orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_PORTRAIT, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_180); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_PORTRAIT, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_90); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_LANDSCAPE, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_270); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_LANDSCAPE, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_0); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_LANDSCAPE, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_180); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_LANDSCAPE, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_90); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_PORTRAIT, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_LANDSCAPE, Surface.ROTATION_270); + orientation = deviceOrientationManager.getDeviceDefaultOrientation(); + assertEquals(Configuration.ORIENTATION_PORTRAIT, orientation); + } + + @Test + public void calculateSensorOrientation() { + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + DeviceOrientation orientation = deviceOrientationManager.calculateSensorOrientation(0); + assertEquals(DeviceOrientation.PORTRAIT_UP, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + orientation = deviceOrientationManager.calculateSensorOrientation(90); + assertEquals(DeviceOrientation.LANDSCAPE_LEFT, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + orientation = deviceOrientationManager.calculateSensorOrientation(180); + assertEquals(DeviceOrientation.PORTRAIT_DOWN, orientation); + + setUpUIOrientationMocks(Configuration.ORIENTATION_PORTRAIT, Surface.ROTATION_0); + orientation = deviceOrientationManager.calculateSensorOrientation(270); + assertEquals(DeviceOrientation.LANDSCAPE_RIGHT, orientation); + } + + private void setUpUIOrientationMocks(int orientation, int rotation) { + Resources mockResources = mock(Resources.class); + Configuration mockConfiguration = mock(Configuration.class); + + when(mockDisplay.getRotation()).thenReturn(rotation); + + mockConfiguration.orientation = orientation; + when(mockActivity.getResources()).thenReturn(mockResources); + when(mockResources.getConfiguration()).thenReturn(mockConfiguration); + } + + @Test + public void getDisplayTest() { + Display display = deviceOrientationManager.getDisplay(); + + assertEquals(mockDisplay, display); + } +} diff --git a/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeatureTest.java b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeatureTest.java new file mode 100644 index 000000000000..2c3a5ab46634 --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/features/sensororientation/SensorOrientationFeatureTest.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.features.sensororientation; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.hardware.camera2.CameraMetadata; +import io.flutter.embedding.engine.systemchannels.PlatformChannel.DeviceOrientation; +import io.flutter.plugins.camera.CameraProperties; +import io.flutter.plugins.camera.DartMessenger; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; + +public class SensorOrientationFeatureTest { + private MockedStatic mockedStaticDeviceOrientationManager; + private Activity mockActivity; + private CameraProperties mockCameraProperties; + private DartMessenger mockDartMessenger; + private DeviceOrientationManager mockDeviceOrientationManager; + + @Before + public void before() { + mockedStaticDeviceOrientationManager = mockStatic(DeviceOrientationManager.class); + mockActivity = mock(Activity.class); + mockCameraProperties = mock(CameraProperties.class); + mockDartMessenger = mock(DartMessenger.class); + mockDeviceOrientationManager = mock(DeviceOrientationManager.class); + + when(mockCameraProperties.getSensorOrientation()).thenReturn(0); + when(mockCameraProperties.getLensFacing()).thenReturn(CameraMetadata.LENS_FACING_BACK); + + mockedStaticDeviceOrientationManager + .when(() -> DeviceOrientationManager.create(mockActivity, mockDartMessenger, false, 0)) + .thenReturn(mockDeviceOrientationManager); + } + + @After + public void after() { + mockedStaticDeviceOrientationManager.close(); + } + + @Test + public void ctor_shouldStartDeviceOrientationManager() { + SensorOrientationFeature sensorOrientationFeature = + new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + + verify(mockDeviceOrientationManager, times(1)).start(); + } + + @Test + public void getDebugName_shouldReturnTheNameOfTheFeature() { + SensorOrientationFeature sensorOrientationFeature = + new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + + assertEquals("SensorOrientationFeature", sensorOrientationFeature.getDebugName()); + } + + @Test + public void getValue_shouldReturnNullIfNotSet() { + SensorOrientationFeature sensorOrientationFeature = + new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + + assertEquals(0, (int) sensorOrientationFeature.getValue()); + } + + @Test + public void getValue_shouldEchoSetValue() { + SensorOrientationFeature sensorOrientationFeature = + new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + + sensorOrientationFeature.setValue(90); + + assertEquals(90, (int) sensorOrientationFeature.getValue()); + } + + @Test + public void checkIsSupport_returnsTrue() { + SensorOrientationFeature sensorOrientationFeature = + new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + + assertTrue(sensorOrientationFeature.checkIsSupported()); + } + + @Test + public void getDeviceOrientationManager_shouldReturnInitializedDartOrientationManagerInstance() { + SensorOrientationFeature sensorOrientationFeature = + new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + + assertEquals( + mockDeviceOrientationManager, sensorOrientationFeature.getDeviceOrientationManager()); + } + + @Test + public void lockCaptureOrientation_shouldLockToSpecifiedOrientation() { + SensorOrientationFeature sensorOrientationFeature = + new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + + sensorOrientationFeature.lockCaptureOrientation(DeviceOrientation.PORTRAIT_DOWN); + + assertEquals( + DeviceOrientation.PORTRAIT_DOWN, sensorOrientationFeature.getLockedCaptureOrientation()); + } + + @Test + public void unlockCaptureOrientation_shouldSetLockToNull() { + SensorOrientationFeature sensorOrientationFeature = + new SensorOrientationFeature(mockCameraProperties, mockActivity, mockDartMessenger); + + sensorOrientationFeature.unlockCaptureOrientation(); + + assertNull(sensorOrientationFeature.getLockedCaptureOrientation()); + } +} 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..9f05cc255a8b --- /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_whenParametersAreValid() { + 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_whenSensorSizeIsNull() { + 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_whenMaxZoomIsNull() { + 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_whenMaxZoomIsSmallerThenDefaultZoomFactor() { + 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_shouldReturnTheNameOfTheFeature() { + ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); + + assertEquals("ZoomLevelFeature", zoomLevelFeature.getDebugName()); + } + + @Test + public void getValue_shouldReturnNullIfNotSet() { + ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); + + assertEquals(1.0, (float) zoomLevelFeature.getValue(), 0); + } + + @Test + public void getValue_shouldEchoSetValue() { + ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); + + zoomLevelFeature.setValue(2.3f); + + assertEquals(2.3f, (float) zoomLevelFeature.getValue(), 0); + } + + @Test + public void checkIsSupport_returnsFalseByDefault() { + ZoomLevelFeature zoomLevelFeature = new ZoomLevelFeature(mockCameraProperties); + + assertFalse(zoomLevelFeature.checkIsSupported()); + } + + @Test + public void updateBuilder_shouldSetScalarCropRegionWhenCheckIsSupportIsTrue() { + 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..28160ff30714 --- /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_whenSensorSizeEqualsZeroShouldReturnCropRegionOfZero() { + 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_whenSensorSizeIsValidShouldReturnCropRegion() { + 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_whenZoomIsGreaterThenMaxZoomClampToMaxZoom() { + 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_whenZoomIsSmallerThenMinZoomClampToMinZoom() { + 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..5425409c2f3a --- /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_shouldSetValuesInCorrectOrderWhenAudioIsDisabled() throws IOException { + CamcorderProfile recorderProfile = getEmptyCamcorderProfile(); + MediaRecorderBuilder.MediaRecorderFactory mockFactory = + mock(MediaRecorderBuilder.MediaRecorderFactory.class); + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + String outputFilePath = "mock_video_file_path"; + int mediaOrientation = 1; + MediaRecorderBuilder builder = + new MediaRecorderBuilder(recorderProfile, outputFilePath, mockFactory) + .setEnableAudio(false) + .setMediaOrientation(mediaOrientation); + + when(mockFactory.makeMediaRecorder()).thenReturn(mockMediaRecorder); + + MediaRecorder recorder = builder.build(); + + InOrder inOrder = inOrder(recorder); + inOrder.verify(recorder).setVideoSource(MediaRecorder.VideoSource.SURFACE); + inOrder.verify(recorder).setOutputFormat(recorderProfile.fileFormat); + inOrder.verify(recorder).setVideoEncoder(recorderProfile.videoCodec); + inOrder.verify(recorder).setVideoEncodingBitRate(recorderProfile.videoBitRate); + inOrder.verify(recorder).setVideoFrameRate(recorderProfile.videoFrameRate); + inOrder + .verify(recorder) + .setVideoSize(recorderProfile.videoFrameWidth, recorderProfile.videoFrameHeight); + inOrder.verify(recorder).setOutputFile(outputFilePath); + inOrder.verify(recorder).setOrientationHint(mediaOrientation); + inOrder.verify(recorder).prepare(); + } + + @Test + public void build_shouldSetValuesInCorrectOrderWhenAudioIsEnabled() throws IOException { + CamcorderProfile recorderProfile = getEmptyCamcorderProfile(); + MediaRecorderBuilder.MediaRecorderFactory mockFactory = + mock(MediaRecorderBuilder.MediaRecorderFactory.class); + MediaRecorder mockMediaRecorder = mock(MediaRecorder.class); + String outputFilePath = "mock_video_file_path"; + int mediaOrientation = 1; + MediaRecorderBuilder builder = + new MediaRecorderBuilder(recorderProfile, outputFilePath, mockFactory) + .setEnableAudio(true) + .setMediaOrientation(mediaOrientation); + + when(mockFactory.makeMediaRecorder()).thenReturn(mockMediaRecorder); + + MediaRecorder recorder = builder.build(); + + InOrder inOrder = inOrder(recorder); + inOrder.verify(recorder).setAudioSource(MediaRecorder.AudioSource.MIC); + inOrder.verify(recorder).setVideoSource(MediaRecorder.VideoSource.SURFACE); + inOrder.verify(recorder).setOutputFormat(recorderProfile.fileFormat); + inOrder.verify(recorder).setAudioEncoder(recorderProfile.audioCodec); + inOrder.verify(recorder).setAudioEncodingBitRate(recorderProfile.audioBitRate); + inOrder.verify(recorder).setAudioSamplingRate(recorderProfile.audioSampleRate); + inOrder.verify(recorder).setVideoEncoder(recorderProfile.videoCodec); + inOrder.verify(recorder).setVideoEncodingBitRate(recorderProfile.videoBitRate); + inOrder.verify(recorder).setVideoFrameRate(recorderProfile.videoFrameRate); + inOrder + .verify(recorder) + .setVideoSize(recorderProfile.videoFrameWidth, recorderProfile.videoFrameHeight); + inOrder.verify(recorder).setOutputFile(outputFilePath); + inOrder.verify(recorder).setOrientationHint(mediaOrientation); + inOrder.verify(recorder).prepare(); + } + + private CamcorderProfile getEmptyCamcorderProfile() { + try { + Constructor constructor = + CamcorderProfile.class.getDeclaredConstructor( + int.class, int.class, int.class, int.class, int.class, int.class, int.class, + int.class, int.class, int.class, int.class, int.class); + + constructor.setAccessible(true); + return constructor.newInstance(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + } catch (Exception ignored) { + } + + return null; + } +} diff --git a/packages/camera/camera/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..dbef8510e021 --- /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_returnsCorrectValues() { + 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_returnsNullForNonexistantValue() { + assertEquals( + "Returns null for 'nonexistant'", ExposureMode.getValueForString("nonexistant"), null); + } + + @Test + public void toString_returnsCorrectValue() { + 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..7ae175ee4649 --- /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_returnsCorrectValues() { + 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_returnsNullForNonexistantValue() { + assertEquals( + "Returns null for 'nonexistant'", FlashMode.getValueForString("nonexistant"), null); + } + + @Test + public void toString_returnsCorrectValue() { + 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..1d7b95c1b548 --- /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_returnsCorrectValues() { + 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_returnsNullForNonexistantValue() { + assertEquals( + "Returns null for 'nonexistant'", FocusMode.getValueForString("nonexistant"), null); + } + + @Test + public void toString_returnsCorrectValue() { + 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..fce99b54384b --- /dev/null +++ b/packages/camera/camera/android/src/test/java/io/flutter/plugins/camera/utils/TestUtils.java @@ -0,0 +1,47 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.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); + } + } + + public static void setPrivateField(T instance, String fieldName, Object newValue) { + try { + Field field = instance.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + field.set(instance, newValue); + } catch (Exception e) { + Assert.fail("Unable to mock private field: " + fieldName); + } + } + + public static Object getPrivateField(T instance, String fieldName) { + try { + Field field = instance.getClass().getDeclaredField(fieldName); + field.setAccessible(true); + return field.get(instance); + } catch (Exception e) { + Assert.fail("Unable to mock private field: " + fieldName); + return null; + } + } +} diff --git a/packages/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/battery/example/android.iml b/packages/camera/camera/example/android.iml similarity index 100% rename from packages/battery/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/DartIntegrationTest.java b/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/camera/camera/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..39cae489d9fa --- /dev/null +++ b/packages/camera/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/FlutterActivityTest.java @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.cameraexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class FlutterActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/camera/camera/example/android/app/src/main/AndroidManifest.xml b/packages/camera/camera/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..cef23162ddb6 --- /dev/null +++ b/packages/camera/camera/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + 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/camera/camera/example/ios/Flutter/AppFrameworkInfo.plist b/packages/camera/camera/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..3a9c234f96d4 --- /dev/null +++ b/packages/camera/camera/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + UIRequiredDeviceCapabilities + + arm64 + + MinimumOSVersion + 9.0 + + diff --git a/packages/camera/camera/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..8520bb00fb2f --- /dev/null +++ b/packages/camera/camera/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,640 @@ +// !$*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 */; }; + E487C86026D686A10034AC92 /* CameraPreviewPauseTests.m in Sources */ = {isa = PBXBuildFile; fileRef = E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 03BB766D2665316900CE5A93 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + 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 = ""; }; + E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CameraPreviewPauseTests.m; 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 */, + E487C85F26D686A10034AC92 /* CameraPreviewPauseTests.m */, + ); + 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 */, + E487C86026D686A10034AC92 /* CameraPreviewPauseTests.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 = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.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_intent/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/camera/camera/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/android_intent/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/camera/camera/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/e2e/example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/camera/camera/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from packages/e2e/example/macos/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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_intent/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/CameraExposureTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraExposureTests.m new file mode 100644 index 000000000000..ee43d3f155f4 --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/CameraExposureTests.m @@ -0,0 +1,55 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import camera; +@import XCTest; +@import AVFoundation; +#import + +@interface FLTCam : NSObject + +- (void)setExposurePointWithResult:(FlutterResult)result x:(double)x y:(double)y; +@end + +@interface CameraExposureTests : XCTestCase +@property(readonly, nonatomic) FLTCam *camera; +@property(readonly, nonatomic) id mockDevice; +@property(readonly, nonatomic) id mockUIDevice; +@end + +@implementation CameraExposureTests + +- (void)setUp { + _camera = [[FLTCam alloc] init]; + _mockDevice = OCMClassMock([AVCaptureDevice class]); + _mockUIDevice = OCMPartialMock([UIDevice currentDevice]); +} + +- (void)tearDown { + [_mockDevice stopMocking]; + [_mockUIDevice stopMocking]; +} + +- (void)testSetExpsourePointWithResult_SetsExposurePointOfInterest { + // UI is currently in landscape left orientation + OCMStub([(UIDevice *)_mockUIDevice orientation]).andReturn(UIDeviceOrientationLandscapeLeft); + // Exposure point of interest is supported + OCMStub([_mockDevice isExposurePointOfInterestSupported]).andReturn(true); + // Set mock device as the current capture device + [_camera setValue:_mockDevice forKey:@"captureDevice"]; + + // Run test + [_camera + setExposurePointWithResult:^void(id _Nullable result) { + } + x:1 + y:1]; + + // Verify the focus point of interest has been set + OCMVerify([_mockDevice setExposurePointOfInterest:CGPointMake(1, 1)]); +} + +@end 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..27537e7ebdac --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/CameraFocusTests.m @@ -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 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; +- (void)setFocusPointWithResult:(FlutterResult)result x:(double)x y:(double)y; +@end + +@interface CameraFocusTests : XCTestCase +@property(readonly, nonatomic) FLTCam *camera; +@property(readonly, nonatomic) id mockDevice; +@property(readonly, nonatomic) id mockUIDevice; +@end + +@implementation CameraFocusTests + +- (void)setUp { + _camera = [[FLTCam alloc] init]; + _mockDevice = OCMClassMock([AVCaptureDevice class]); + _mockUIDevice = OCMPartialMock([UIDevice currentDevice]); +} + +- (void)tearDown { + [_mockDevice stopMocking]; + [_mockUIDevice stopMocking]; +} + +- (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]; +} + +- (void)testSetFocusPointWithResult_SetsFocusPointOfInterest { + // UI is currently in landscape left orientation + OCMStub([(UIDevice *)_mockUIDevice orientation]).andReturn(UIDeviceOrientationLandscapeLeft); + // Focus point of interest is supported + OCMStub([_mockDevice isFocusPointOfInterestSupported]).andReturn(true); + // Set mock device as the current capture device + [_camera setValue:_mockDevice forKey:@"captureDevice"]; + + // Run test + [_camera + setFocusPointWithResult:^void(id _Nullable result) { + } + x:1 + y:1]; + + // Verify the focus point of interest has been set + OCMVerify([_mockDevice setFocusPointOfInterest:CGPointMake(1, 1)]); +} + +@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/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m new file mode 100644 index 000000000000..549b40a52e46 --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/CameraPreviewPauseTests.m @@ -0,0 +1,50 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import camera; +@import XCTest; +@import AVFoundation; +#import + +@interface FLTCam : NSObject +@property(assign, nonatomic) BOOL isPreviewPaused; +- (void)pausePreviewWithResult:(FlutterResult)result; +- (void)resumePreviewWithResult:(FlutterResult)result; +@end + +@interface CameraPreviewPauseTests : XCTestCase +@property(readonly, nonatomic) FLTCam* camera; +@end + +@implementation CameraPreviewPauseTests + +- (void)setUp { + _camera = [[FLTCam alloc] init]; +} + +- (void)testPausePreviewWithResult_shouldPausePreview { + XCTestExpectation* resultExpectation = + [self expectationWithDescription:@"Succeeding result with nil value"]; + [_camera pausePreviewWithResult:^void(id _Nullable result) { + XCTAssertNil(result); + [resultExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:2.0 handler:nil]; + XCTAssertTrue(_camera.isPreviewPaused); +} + +- (void)testResumePreviewWithResult_shouldResumePreview { + XCTestExpectation* resultExpectation = + [self expectationWithDescription:@"Succeeding result with nil value"]; + [_camera resumePreviewWithResult:^void(id _Nullable result) { + XCTAssertNil(result); + [resultExpectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:2.0 handler:nil]; + XCTAssertFalse(_camera.isPreviewPaused); +} + +@end diff --git a/packages/camera/camera/example/ios/RunnerTests/CameraUtilTests.m b/packages/camera/camera/example/ios/RunnerTests/CameraUtilTests.m new file mode 100644 index 000000000000..380f6e93de58 --- /dev/null +++ b/packages/camera/camera/example/ios/RunnerTests/CameraUtilTests.m @@ -0,0 +1,49 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import camera; +@import XCTest; +@import AVFoundation; +#import + +@interface FLTCam : NSObject + +- (CGPoint)getCGPointForCoordsWithOrientation:(UIDeviceOrientation)orientation + x:(double)x + y:(double)y; + +@end + +@interface CameraUtilTests : XCTestCase +@property(readonly, nonatomic) FLTCam *camera; + +@end + +@implementation CameraUtilTests + +- (void)setUp { + _camera = [[FLTCam alloc] init]; +} + +- (void)testGetCGPointForCoordsWithOrientation_ShouldRotateCoords { + CGPoint point; + point = [_camera getCGPointForCoordsWithOrientation:UIDeviceOrientationLandscapeLeft x:1 y:1]; + XCTAssertTrue(CGPointEqualToPoint(point, CGPointMake(1, 1)), + @"Resulting coordinates are invalid."); + point = [_camera getCGPointForCoordsWithOrientation:UIDeviceOrientationPortrait x:0 y:1]; + XCTAssertTrue(CGPointEqualToPoint(point, CGPointMake(1, 1)), + @"Resulting coordinates are invalid."); + point = [_camera getCGPointForCoordsWithOrientation:UIDeviceOrientationLandscapeRight x:0 y:0]; + XCTAssertTrue(CGPointEqualToPoint(point, CGPointMake(1, 1)), + @"Resulting coordinates are invalid."); + point = [_camera getCGPointForCoordsWithOrientation:UIDeviceOrientationPortraitUpsideDown + x:1 + y:0]; + XCTAssertTrue(CGPointEqualToPoint(point, CGPointMake(1, 1)), + @"Resulting coordinates are invalid."); +} + +@end diff --git a/packages/e2e/example/ios/RunnerTests/Info.plist b/packages/camera/camera/example/ios/RunnerTests/Info.plist similarity index 100% rename from packages/e2e/example/ios/RunnerTests/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..a3a5d1d46391 --- /dev/null +++ b/packages/camera/camera/example/lib/main.dart @@ -0,0 +1,1021 @@ +// Copyright 2013 The Flutter 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/foundation.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(); + _ambiguate(WidgetsBinding.instance)?.addObserver(this); + + _flashModeControlRowAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _flashModeControlRowAnimation = CurvedAnimation( + parent: _flashModeControlRowAnimationController, + curve: Curves.easeInCubic, + ); + _exposureModeControlRowAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _exposureModeControlRowAnimation = CurvedAnimation( + parent: _exposureModeControlRowAnimationController, + curve: Curves.easeInCubic, + ); + _focusModeControlRowAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _focusModeControlRowAnimation = CurvedAnimation( + parent: _focusModeControlRowAnimationController, + curve: Curves.easeInCubic, + ); + } + + @override + void dispose() { + _ambiguate(WidgetsBinding.instance)?.removeObserver(this); + _flashModeControlRowAnimationController.dispose(); + _exposureModeControlRowAnimationController.dispose(); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + final CameraController? cameraController = controller; + + // App state changed before we got the chance to initialize. + if (cameraController == null || !cameraController.value.isInitialized) { + return; + } + + if (state == AppLifecycleState.inactive) { + cameraController.dispose(); + } else if (state == AppLifecycleState.resumed) { + onNewCameraSelected(cameraController.description); + } + } + + 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) + ? ( + // The captured image on the web contains a network-accessible URL + // pointing to a location within the browser. It may be displayed + // either with Image.network or Image.memory after loading the image + // bytes to memory. + kIsWeb + ? Image.network(imageFile!.path) + : Image.file(File(imageFile!.path))) + : Container( + child: Center( + child: AspectRatio( + aspectRatio: + localVideoController.value.size != null + ? localVideoController + .value.aspectRatio + : 1.0, + child: VideoPlayer(localVideoController)), + ), + decoration: BoxDecoration( + border: Border.all(color: Colors.pink)), + ), + width: 64.0, + height: 64.0, + ), + ], + ), + ), + ); + } + + /// 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, + ), + // The exposure and focus mode are currently not supported on the web. + ...(!kIsWeb + ? [ + 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, + ), + TextButton( + child: Text('RESET OFFSET'), + style: styleLocked, + onPressed: controller != null + ? () => controller!.setExposureOffset(0.0) + : 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, + ), + IconButton( + icon: const Icon(Icons.pause_presentation), + color: + cameraController != null && cameraController.value.isPreviewPaused + ? Colors.red + : Colors.blue, + onPressed: + cameraController == null ? null : onPausePreviewButtonPressed, + ), + ], + ); + } + + /// Display a row of toggle to select the camera (or a message if no camera is available). + Widget _cameraTogglesRowWidget() { + final List toggles = []; + + 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, + kIsWeb ? ResolutionPreset.max : ResolutionPreset.medium, + enableAudio: enableAudio, + imageFormatGroup: ImageFormatGroup.jpeg, + ); + + controller = cameraController; + + // If the controller is updated then update the UI. + cameraController.addListener(() { + if (mounted) setState(() {}); + if (cameraController.value.hasError) { + showInSnackBar( + 'Camera error ${cameraController.value.errorDescription}'); + } + }); + + try { + await cameraController.initialize(); + await Future.wait([ + // The exposure mode is currently not supported on the web. + ...(!kIsWeb + ? [ + cameraController + .getMinExposureOffset() + .then((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 { + try { + if (controller != null) { + final CameraController cameraController = controller!; + if (cameraController.value.isCaptureOrientationLocked) { + await cameraController.unlockCaptureOrientation(); + showInSnackBar('Capture orientation unlocked'); + } else { + await cameraController.lockCaptureOrientation(); + showInSnackBar( + 'Capture orientation locked to ${cameraController.value.lockedCaptureOrientation.toString().split('.').last}'); + } + } + } on CameraException catch (e) { + _showCameraException(e); + } + } + + void onSetFlashModeButtonPressed(FlashMode mode) { + setFlashMode(mode).then((_) { + if (mounted) setState(() {}); + showInSnackBar('Flash mode set to ${mode.toString().split('.').last}'); + }); + } + + void onSetExposureModeButtonPressed(ExposureMode mode) { + setExposureMode(mode).then((_) { + if (mounted) setState(() {}); + showInSnackBar('Exposure mode set to ${mode.toString().split('.').last}'); + }); + } + + void onSetFocusModeButtonPressed(FocusMode mode) { + setFocusMode(mode).then((_) { + if (mounted) setState(() {}); + showInSnackBar('Focus mode set to ${mode.toString().split('.').last}'); + }); + } + + void onVideoRecordButtonPressed() { + startVideoRecording().then((_) { + if (mounted) setState(() {}); + }); + } + + void onStopButtonPressed() { + stopVideoRecording().then((file) { + if (mounted) setState(() {}); + if (file != null) { + showInSnackBar('Video recorded to ${file.path}'); + videoFile = file; + _startVideoPlayer(); + } + }); + } + + Future onPausePreviewButtonPressed() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isInitialized) { + showInSnackBar('Error: select a camera first.'); + return; + } + + if (cameraController.value.isPreviewPaused) { + await cameraController.resumePreview(); + } else { + await cameraController.pausePreview(); + } + + if (mounted) setState(() {}); + } + + void onPauseButtonPressed() { + pauseVideoRecording().then((_) { + if (mounted) setState(() {}); + showInSnackBar('Video recording paused'); + }); + } + + void onResumeButtonPressed() { + resumeVideoRecording().then((_) { + if (mounted) setState(() {}); + showInSnackBar('Video recording resumed'); + }); + } + + Future startVideoRecording() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isInitialized) { + showInSnackBar('Error: select a camera first.'); + return; + } + + if (cameraController.value.isRecordingVideo) { + // A recording is already started, do nothing. + return; + } + + try { + await cameraController.startVideoRecording(); + } on CameraException catch (e) { + _showCameraException(e); + return; + } + } + + Future stopVideoRecording() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isRecordingVideo) { + return null; + } + + try { + return cameraController.stopVideoRecording(); + } on CameraException catch (e) { + _showCameraException(e); + return null; + } + } + + Future pauseVideoRecording() async { + final CameraController? cameraController = controller; + + if (cameraController == null || !cameraController.value.isRecordingVideo) { + return 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 = kIsWeb + ? VideoPlayerController.network(videoFile!.path) + : VideoPlayerController.file(File(videoFile!.path)); + + videoPlayerListener = () { + if (videoController != null && videoController!.value.size != null) { + // Refreshing the state to update video player with the correct ratio. + if (mounted) setState(() {}); + videoController!.removeListener(videoPlayerListener!); + } + }; + vController.addListener(videoPlayerListener!); + await vController.setLooping(true); + await vController.initialize(); + await videoController?.dispose(); + if (mounted) { + setState(() { + imageFile = null; + videoController = vController; + }); + } + await vController.play(); + } + + Future takePicture() async { + final CameraController? cameraController = controller; + if (cameraController == null || !cameraController.value.isInitialized) { + showInSnackBar('Error: select a camera first.'); + return null; + } + + if (cameraController.value.isTakingPicture) { + // A capture is already pending, do nothing. + return null; + } + + try { + 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()); +} + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +// TODO(ianh): Remove this once we roll stable in late 2021. +T? _ambiguate(T? value) => value; diff --git a/packages/camera/camera/example/pubspec.yaml b/packages/camera/camera/example/pubspec.yaml new file mode 100644 index 000000000000..1899835aca50 --- /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.14.0 <3.0.0" + flutter: ">=2.5.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/e2e/example/web/favicon.png b/packages/camera/camera/example/web/favicon.png similarity index 100% rename from packages/e2e/example/web/favicon.png rename to packages/camera/camera/example/web/favicon.png diff --git a/packages/e2e/example/web/icons/Icon-192.png b/packages/camera/camera/example/web/icons/Icon-192.png similarity index 100% rename from packages/e2e/example/web/icons/Icon-192.png rename to packages/camera/camera/example/web/icons/Icon-192.png diff --git a/packages/e2e/example/web/icons/Icon-512.png b/packages/camera/camera/example/web/icons/Icon-512.png similarity index 100% rename from packages/e2e/example/web/icons/Icon-512.png rename to packages/camera/camera/example/web/icons/Icon-512.png diff --git a/packages/camera/camera/example/web/index.html b/packages/camera/camera/example/web/index.html new file mode 100644 index 000000000000..2a3117d29362 --- /dev/null +++ b/packages/camera/camera/example/web/index.html @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + Camera Web Example + + + + + + + + + + \ No newline at end of file diff --git a/packages/camera/camera/example/web/manifest.json b/packages/camera/camera/example/web/manifest.json new file mode 100644 index 000000000000..5fe0e048afe6 --- /dev/null +++ b/packages/camera/camera/example/web/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "camera example", + "short_name": "camera", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "An example of the camera on the web.", + "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/android_intent/ios/Assets/.gitkeep b/packages/camera/camera/ios/Assets/.gitkeep similarity index 100% rename from packages/android_intent/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..da560d6c4df7 --- /dev/null +++ b/packages/camera/camera/ios/Classes/CameraPlugin.m @@ -0,0 +1,1543 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "CameraPlugin.h" +#import +#import +#import +#import +#import + +static FlutterError *getFlutterError(NSError *error) { + return [FlutterError errorWithCode:[NSString stringWithFormat:@"Error %d", (int)error.code] + message:error.localizedDescription + details:error.domain]; +} + +@interface FLTSavePhotoDelegate : NSObject +@property(readonly, nonatomic) NSString *path; +@property(readonly, nonatomic) FlutterResult result; +@end + +@interface FLTImageStreamHandler : NSObject +@property FlutterEventSink eventSink; +@end + +@implementation FLTImageStreamHandler + +- (FlutterError *_Nullable)onCancelWithArguments:(id _Nullable)arguments { + _eventSink = nil; + return nil; +} + +- (FlutterError *_Nullable)onListenWithArguments:(id _Nullable)arguments + eventSink:(nonnull FlutterEventSink)events { + _eventSink = events; + return nil; +} +@end + +@implementation FLTSavePhotoDelegate { + /// Used to keep the delegate alive until didFinishProcessingPhotoSampleBuffer. + FLTSavePhotoDelegate *selfReference; +} + +- initWithPath:(NSString *)path result:(FlutterResult)result { + self = [super init]; + NSAssert(self, @"super init cannot be nil"); + _path = path; + selfReference = self; + _result = result; + return self; +} + +- (void)captureOutput:(AVCapturePhotoOutput *)output + didFinishProcessingPhotoSampleBuffer:(CMSampleBufferRef)photoSampleBuffer + previewPhotoSampleBuffer:(CMSampleBufferRef)previewPhotoSampleBuffer + resolvedSettings:(AVCaptureResolvedPhotoSettings *)resolvedSettings + bracketSettings:(AVCaptureBracketedStillImageSettings *)bracketSettings + error:(NSError *)error API_AVAILABLE(ios(10)) { + selfReference = nil; + if (error) { + _result(getFlutterError(error)); + return; + } + + NSData *data = [AVCapturePhotoOutput + JPEGPhotoDataRepresentationForJPEGSampleBuffer:photoSampleBuffer + previewPhotoSampleBuffer:previewPhotoSampleBuffer]; + + // TODO(sigurdm): Consider writing file asynchronously. + bool success = [data writeToFile:_path atomically:YES]; + + if (!success) { + _result([FlutterError errorWithCode:@"IOError" message:@"Unable to write file" details:nil]); + return; + } + _result(_path); +} + +- (void)captureOutput:(AVCapturePhotoOutput *)output + didFinishProcessingPhoto:(AVCapturePhoto *)photo + error:(NSError *)error API_AVAILABLE(ios(11.0)) { + selfReference = nil; + if (error) { + _result(getFlutterError(error)); + return; + } + + NSData *photoData = [photo fileDataRepresentation]; + + bool success = [photoData writeToFile:_path atomically:YES]; + if (!success) { + _result([FlutterError errorWithCode:@"IOError" message:@"Unable to write file" details:nil]); + return; + } + _result(_path); +} +@end + +// Mirrors FlashMode in flash_mode.dart +typedef enum { + FlashModeOff, + FlashModeAuto, + FlashModeAlways, + FlashModeTorch, +} FlashMode; + +static FlashMode getFlashModeForString(NSString *mode) { + if ([mode isEqualToString:@"off"]) { + return FlashModeOff; + } else if ([mode isEqualToString:@"auto"]) { + return FlashModeAuto; + } else if ([mode isEqualToString:@"always"]) { + return FlashModeAlways; + } else if ([mode isEqualToString:@"torch"]) { + return FlashModeTorch; + } else { + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Unknown flash mode %@", mode] + }]; + @throw error; + } +} + +static OSType getVideoFormatFromString(NSString *videoFormatString) { + if ([videoFormatString isEqualToString:@"bgra8888"]) { + return kCVPixelFormatType_32BGRA; + } else if ([videoFormatString isEqualToString:@"yuv420"]) { + return kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange; + } else { + NSLog(@"The selected imageFormatGroup is not supported by iOS. Defaulting to brga8888"); + return kCVPixelFormatType_32BGRA; + } +} + +static AVCaptureFlashMode getAVCaptureFlashModeForFlashMode(FlashMode mode) { + switch (mode) { + case FlashModeOff: + return AVCaptureFlashModeOff; + case FlashModeAuto: + return AVCaptureFlashModeAuto; + case FlashModeAlways: + return AVCaptureFlashModeOn; + case FlashModeTorch: + default: + return -1; + } +} + +// Mirrors ExposureMode in camera.dart +typedef enum { + ExposureModeAuto, + ExposureModeLocked, + +} ExposureMode; + +static NSString *getStringForExposureMode(ExposureMode mode) { + switch (mode) { + case ExposureModeAuto: + return @"auto"; + case ExposureModeLocked: + return @"locked"; + } + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Unknown string for exposure mode"] + }]; + @throw error; +} + +static ExposureMode getExposureModeForString(NSString *mode) { + if ([mode isEqualToString:@"auto"]) { + return ExposureModeAuto; + } else if ([mode isEqualToString:@"locked"]) { + return ExposureModeLocked; + } else { + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Unknown exposure mode %@", mode] + }]; + @throw error; + } +} + +static UIDeviceOrientation getUIDeviceOrientationForString(NSString *orientation) { + if ([orientation isEqualToString:@"portraitDown"]) { + return UIDeviceOrientationPortraitUpsideDown; + } else if ([orientation isEqualToString:@"landscapeLeft"]) { + return UIDeviceOrientationLandscapeRight; + } else if ([orientation isEqualToString:@"landscapeRight"]) { + return UIDeviceOrientationLandscapeLeft; + } else if ([orientation isEqualToString:@"portraitUp"]) { + return UIDeviceOrientationPortrait; + } else { + NSError *error = [NSError + errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : + [NSString stringWithFormat:@"Unknown device orientation %@", orientation] + }]; + @throw error; + } +} + +static NSString *getStringForUIDeviceOrientation(UIDeviceOrientation orientation) { + switch (orientation) { + case UIDeviceOrientationPortraitUpsideDown: + return @"portraitDown"; + case UIDeviceOrientationLandscapeRight: + return @"landscapeLeft"; + case UIDeviceOrientationLandscapeLeft: + return @"landscapeRight"; + case UIDeviceOrientationPortrait: + default: + return @"portraitUp"; + break; + }; +} + +// Mirrors FocusMode in camera.dart +typedef enum { + FocusModeAuto, + FocusModeLocked, +} FocusMode; + +static NSString *getStringForFocusMode(FocusMode mode) { + switch (mode) { + case FocusModeAuto: + return @"auto"; + case FocusModeLocked: + return @"locked"; + } + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Unknown string for focus mode"] + }]; + @throw error; +} + +static FocusMode getFocusModeForString(NSString *mode) { + if ([mode isEqualToString:@"auto"]) { + return FocusModeAuto; + } else if ([mode isEqualToString:@"locked"]) { + return FocusModeLocked; + } else { + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Unknown focus mode %@", mode] + }]; + @throw error; + } +} + +// Mirrors ResolutionPreset in camera.dart +typedef enum { + veryLow, + low, + medium, + high, + veryHigh, + ultraHigh, + max, +} ResolutionPreset; + +static ResolutionPreset getResolutionPresetForString(NSString *preset) { + if ([preset isEqualToString:@"veryLow"]) { + return veryLow; + } else if ([preset isEqualToString:@"low"]) { + return low; + } else if ([preset isEqualToString:@"medium"]) { + return medium; + } else if ([preset isEqualToString:@"high"]) { + return high; + } else if ([preset isEqualToString:@"veryHigh"]) { + return veryHigh; + } else if ([preset isEqualToString:@"ultraHigh"]) { + return ultraHigh; + } else if ([preset isEqualToString:@"max"]) { + return max; + } else { + NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : [NSString + stringWithFormat:@"Unknown resolution preset %@", preset] + }]; + @throw error; + } +} + +@interface FLTCam : NSObject +@property(readonly, nonatomic) int64_t textureId; +@property(nonatomic, copy) void (^onFrameAvailable)(void); +@property BOOL enableAudio; +@property(nonatomic) FLTImageStreamHandler *imageStreamHandler; +@property(nonatomic) FlutterMethodChannel *methodChannel; +@property(readonly, nonatomic) AVCaptureSession *captureSession; +@property(readonly, nonatomic) AVCaptureDevice *captureDevice; +@property(readonly, nonatomic) AVCapturePhotoOutput *capturePhotoOutput API_AVAILABLE(ios(10)); +@property(readonly, nonatomic) AVCaptureVideoDataOutput *captureVideoOutput; +@property(readonly, nonatomic) AVCaptureInput *captureVideoInput; +@property(readonly) CVPixelBufferRef volatile latestPixelBuffer; +@property(readonly, nonatomic) CGSize previewSize; +@property(readonly, nonatomic) CGSize captureSize; +@property(strong, nonatomic) AVAssetWriter *videoWriter; +@property(strong, nonatomic) AVAssetWriterInput *videoWriterInput; +@property(strong, nonatomic) AVAssetWriterInput *audioWriterInput; +@property(strong, nonatomic) AVAssetWriterInputPixelBufferAdaptor *assetWriterPixelBufferAdaptor; +@property(strong, nonatomic) AVCaptureVideoDataOutput *videoOutput; +@property(strong, nonatomic) AVCaptureAudioDataOutput *audioOutput; +@property(strong, nonatomic) NSString *videoRecordingPath; +@property(assign, nonatomic) BOOL isRecording; +@property(assign, nonatomic) BOOL isRecordingPaused; +@property(assign, nonatomic) BOOL videoIsDisconnected; +@property(assign, nonatomic) BOOL audioIsDisconnected; +@property(assign, nonatomic) BOOL isAudioSetup; +@property(assign, nonatomic) BOOL isStreamingImages; +@property(assign, nonatomic) BOOL isPreviewPaused; +@property(assign, nonatomic) ResolutionPreset resolutionPreset; +@property(assign, nonatomic) ExposureMode exposureMode; +@property(assign, nonatomic) FocusMode focusMode; +@property(assign, nonatomic) FlashMode flashMode; +@property(assign, nonatomic) UIDeviceOrientation lockedCaptureOrientation; +@property(assign, nonatomic) CMTime lastVideoSampleTime; +@property(assign, nonatomic) CMTime lastAudioSampleTime; +@property(assign, nonatomic) CMTime videoTimeOffset; +@property(assign, nonatomic) CMTime audioTimeOffset; +@property(nonatomic) CMMotionManager *motionManager; +@property AVAssetWriterInputPixelBufferAdaptor *videoAdaptor; +@end + +@implementation FLTCam { + dispatch_queue_t _dispatchQueue; + UIDeviceOrientation _deviceOrientation; +} +// Format used for video and image streaming. +FourCharCode videoFormat = kCVPixelFormatType_32BGRA; +NSString *const errorMethod = @"error"; + +- (instancetype)initWithCameraName:(NSString *)cameraName + resolutionPreset:(NSString *)resolutionPreset + enableAudio:(BOOL)enableAudio + orientation:(UIDeviceOrientation)orientation + dispatchQueue:(dispatch_queue_t)dispatchQueue + error:(NSError **)error { + self = [super init]; + NSAssert(self, @"super init cannot be nil"); + @try { + _resolutionPreset = getResolutionPresetForString(resolutionPreset); + } @catch (NSError *e) { + *error = e; + } + _enableAudio = enableAudio; + _dispatchQueue = dispatchQueue; + _captureSession = [[AVCaptureSession alloc] init]; + _captureDevice = [AVCaptureDevice deviceWithUniqueID:cameraName]; + _flashMode = _captureDevice.hasFlash ? FlashModeAuto : FlashModeOff; + _exposureMode = ExposureModeAuto; + _focusMode = FocusModeAuto; + _lockedCaptureOrientation = UIDeviceOrientationUnknown; + _deviceOrientation = orientation; + + NSError *localError = nil; + _captureVideoInput = [AVCaptureDeviceInput deviceInputWithDevice:_captureDevice + error:&localError]; + + if (localError) { + *error = localError; + return nil; + } + + _captureVideoOutput = [AVCaptureVideoDataOutput new]; + _captureVideoOutput.videoSettings = + @{(NSString *)kCVPixelBufferPixelFormatTypeKey : @(videoFormat)}; + [_captureVideoOutput setAlwaysDiscardsLateVideoFrames:YES]; + [_captureVideoOutput setSampleBufferDelegate:self queue:dispatch_get_main_queue()]; + + AVCaptureConnection *connection = + [AVCaptureConnection connectionWithInputPorts:_captureVideoInput.ports + output:_captureVideoOutput]; + + if ([_captureDevice position] == AVCaptureDevicePositionFront) { + connection.videoMirrored = YES; + } + + [_captureSession addInputWithNoConnections:_captureVideoInput]; + [_captureSession addOutputWithNoConnections:_captureVideoOutput]; + [_captureSession addConnection:connection]; + + if (@available(iOS 10.0, *)) { + _capturePhotoOutput = [AVCapturePhotoOutput new]; + [_capturePhotoOutput setHighResolutionCaptureEnabled:YES]; + [_captureSession addOutput:_capturePhotoOutput]; + } + _motionManager = [[CMMotionManager alloc] init]; + [_motionManager startAccelerometerUpdates]; + + [self setCaptureSessionPreset:_resolutionPreset]; + [self updateOrientation]; + + return self; +} + +- (void)start { + [_captureSession startRunning]; +} + +- (void)stop { + [_captureSession stopRunning]; +} + +- (void)setDeviceOrientation:(UIDeviceOrientation)orientation { + if (_deviceOrientation == orientation) { + return; + } + + _deviceOrientation = orientation; + [self updateOrientation]; +} + +- (void)updateOrientation { + if (_isRecording) { + return; + } + + UIDeviceOrientation orientation = (_lockedCaptureOrientation != UIDeviceOrientationUnknown) + ? _lockedCaptureOrientation + : _deviceOrientation; + + [self updateOrientation:orientation forCaptureOutput:_capturePhotoOutput]; + [self updateOrientation:orientation forCaptureOutput:_captureVideoOutput]; +} + +- (void)updateOrientation:(UIDeviceOrientation)orientation + forCaptureOutput:(AVCaptureOutput *)captureOutput { + if (!captureOutput) { + return; + } + + AVCaptureConnection *connection = [captureOutput connectionWithMediaType:AVMediaTypeVideo]; + if (connection && connection.isVideoOrientationSupported) { + connection.videoOrientation = [self getVideoOrientationForDeviceOrientation:orientation]; + } +} + +- (void)captureToFile:(FlutterResult)result API_AVAILABLE(ios(10)) { + AVCapturePhotoSettings *settings = [AVCapturePhotoSettings photoSettings]; + if (_resolutionPreset == max) { + [settings setHighResolutionPhotoEnabled:YES]; + } + + AVCaptureFlashMode avFlashMode = getAVCaptureFlashModeForFlashMode(_flashMode); + if (avFlashMode != -1) { + [settings setFlashMode:avFlashMode]; + } + NSError *error; + NSString *path = [self getTemporaryFilePathWithExtension:@"jpg" + subfolder:@"pictures" + prefix:@"CAP_" + error:error]; + if (error) { + result(getFlutterError(error)); + return; + } + + [_capturePhotoOutput capturePhotoWithSettings:settings + delegate:[[FLTSavePhotoDelegate alloc] initWithPath:path + result:result]]; +} + +- (AVCaptureVideoOrientation)getVideoOrientationForDeviceOrientation: + (UIDeviceOrientation)deviceOrientation { + if (deviceOrientation == UIDeviceOrientationPortrait) { + return AVCaptureVideoOrientationPortrait; + } else if (deviceOrientation == UIDeviceOrientationLandscapeLeft) { + // Note: device orientation is flipped compared to video orientation. When UIDeviceOrientation + // is landscape left the video orientation should be landscape right. + return AVCaptureVideoOrientationLandscapeRight; + } else if (deviceOrientation == UIDeviceOrientationLandscapeRight) { + // Note: device orientation is flipped compared to video orientation. When UIDeviceOrientation + // is landscape right the video orientation should be landscape left. + return AVCaptureVideoOrientationLandscapeLeft; + } else if (deviceOrientation == UIDeviceOrientationPortraitUpsideDown) { + return AVCaptureVideoOrientationPortraitUpsideDown; + } else { + return AVCaptureVideoOrientationPortrait; + } +} + +- (NSString *)getTemporaryFilePathWithExtension:(NSString *)extension + subfolder:(NSString *)subfolder + prefix:(NSString *)prefix + error:(NSError *)error { + NSString *docDir = + NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; + NSString *fileDir = + [[docDir stringByAppendingPathComponent:@"camera"] stringByAppendingPathComponent:subfolder]; + NSString *fileName = [prefix stringByAppendingString:[[NSUUID UUID] UUIDString]]; + NSString *file = + [[fileDir stringByAppendingPathComponent:fileName] stringByAppendingPathExtension:extension]; + + NSFileManager *fm = [NSFileManager defaultManager]; + if (![fm fileExistsAtPath:fileDir]) { + [[NSFileManager defaultManager] createDirectoryAtPath:fileDir + withIntermediateDirectories:true + attributes:nil + error:&error]; + if (error) { + return nil; + } + } + + return file; +} + +- (void)setCaptureSessionPreset:(ResolutionPreset)resolutionPreset { + switch (resolutionPreset) { + case max: + case ultraHigh: + if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset3840x2160]) { + _captureSession.sessionPreset = AVCaptureSessionPreset3840x2160; + _previewSize = CGSizeMake(3840, 2160); + break; + } + if ([_captureSession canSetSessionPreset:AVCaptureSessionPresetHigh]) { + _captureSession.sessionPreset = AVCaptureSessionPresetHigh; + _previewSize = + CGSizeMake(_captureDevice.activeFormat.highResolutionStillImageDimensions.width, + _captureDevice.activeFormat.highResolutionStillImageDimensions.height); + break; + } + case veryHigh: + if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset1920x1080]) { + _captureSession.sessionPreset = AVCaptureSessionPreset1920x1080; + _previewSize = CGSizeMake(1920, 1080); + break; + } + case high: + if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720]) { + _captureSession.sessionPreset = AVCaptureSessionPreset1280x720; + _previewSize = CGSizeMake(1280, 720); + break; + } + case medium: + if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset640x480]) { + _captureSession.sessionPreset = AVCaptureSessionPreset640x480; + _previewSize = CGSizeMake(640, 480); + break; + } + case low: + if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset352x288]) { + _captureSession.sessionPreset = AVCaptureSessionPreset352x288; + _previewSize = CGSizeMake(352, 288); + break; + } + default: + if ([_captureSession canSetSessionPreset:AVCaptureSessionPresetLow]) { + _captureSession.sessionPreset = AVCaptureSessionPresetLow; + _previewSize = CGSizeMake(352, 288); + } else { + NSError *error = + [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorUnknown + userInfo:@{ + NSLocalizedDescriptionKey : + @"No capture session available for current capture session." + }]; + @throw error; + } + } +} + +- (void)captureOutput:(AVCaptureOutput *)output + didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer + fromConnection:(AVCaptureConnection *)connection { + if (output == _captureVideoOutput) { + CVPixelBufferRef newBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); + CFRetain(newBuffer); + CVPixelBufferRef old = _latestPixelBuffer; + while (!OSAtomicCompareAndSwapPtrBarrier(old, newBuffer, (void **)&_latestPixelBuffer)) { + old = _latestPixelBuffer; + } + if (old != nil) { + CFRelease(old); + } + if (_onFrameAvailable) { + _onFrameAvailable(); + } + } + if (!CMSampleBufferDataIsReady(sampleBuffer)) { + [_methodChannel invokeMethod:errorMethod + arguments:@"sample buffer is not ready. Skipping sample"]; + return; + } + if (_isStreamingImages) { + if (_imageStreamHandler.eventSink) { + CVPixelBufferRef pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); + CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); + + size_t imageWidth = CVPixelBufferGetWidth(pixelBuffer); + size_t imageHeight = CVPixelBufferGetHeight(pixelBuffer); + + NSMutableArray *planes = [NSMutableArray array]; + + const Boolean isPlanar = CVPixelBufferIsPlanar(pixelBuffer); + size_t planeCount; + if (isPlanar) { + planeCount = CVPixelBufferGetPlaneCount(pixelBuffer); + } else { + planeCount = 1; + } + + for (int i = 0; i < planeCount; i++) { + void *planeAddress; + size_t bytesPerRow; + size_t height; + size_t width; + + if (isPlanar) { + planeAddress = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, i); + bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, i); + height = CVPixelBufferGetHeightOfPlane(pixelBuffer, i); + width = CVPixelBufferGetWidthOfPlane(pixelBuffer, i); + } else { + planeAddress = CVPixelBufferGetBaseAddress(pixelBuffer); + bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer); + height = CVPixelBufferGetHeight(pixelBuffer); + width = CVPixelBufferGetWidth(pixelBuffer); + } + + NSNumber *length = @(bytesPerRow * height); + NSData *bytes = [NSData dataWithBytes:planeAddress length:length.unsignedIntegerValue]; + + NSMutableDictionary *planeBuffer = [NSMutableDictionary dictionary]; + planeBuffer[@"bytesPerRow"] = @(bytesPerRow); + planeBuffer[@"width"] = @(width); + planeBuffer[@"height"] = @(height); + planeBuffer[@"bytes"] = [FlutterStandardTypedData typedDataWithBytes:bytes]; + + [planes addObject:planeBuffer]; + } + + NSMutableDictionary *imageBuffer = [NSMutableDictionary dictionary]; + imageBuffer[@"width"] = [NSNumber numberWithUnsignedLong:imageWidth]; + imageBuffer[@"height"] = [NSNumber numberWithUnsignedLong:imageHeight]; + imageBuffer[@"format"] = @(videoFormat); + imageBuffer[@"planes"] = planes; + imageBuffer[@"lensAperture"] = [NSNumber numberWithFloat:[_captureDevice lensAperture]]; + Float64 exposureDuration = CMTimeGetSeconds([_captureDevice exposureDuration]); + Float64 nsExposureDuration = 1000000000 * exposureDuration; + imageBuffer[@"sensorExposureTime"] = [NSNumber numberWithInt:nsExposureDuration]; + imageBuffer[@"sensorSensitivity"] = [NSNumber numberWithFloat:[_captureDevice ISO]]; + + _imageStreamHandler.eventSink(imageBuffer); + + CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); + } + } + if (_isRecording && !_isRecordingPaused) { + if (_videoWriter.status == AVAssetWriterStatusFailed) { + [_methodChannel invokeMethod:errorMethod + arguments:[NSString stringWithFormat:@"%@", _videoWriter.error]]; + return; + } + + CFRetain(sampleBuffer); + CMTime currentSampleTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer); + + if (_videoWriter.status != AVAssetWriterStatusWriting) { + [_videoWriter startWriting]; + [_videoWriter startSessionAtSourceTime:currentSampleTime]; + } + + if (output == _captureVideoOutput) { + if (_videoIsDisconnected) { + _videoIsDisconnected = NO; + + if (_videoTimeOffset.value == 0) { + _videoTimeOffset = CMTimeSubtract(currentSampleTime, _lastVideoSampleTime); + } else { + CMTime offset = CMTimeSubtract(currentSampleTime, _lastVideoSampleTime); + _videoTimeOffset = CMTimeAdd(_videoTimeOffset, offset); + } + + return; + } + + _lastVideoSampleTime = currentSampleTime; + + CVPixelBufferRef nextBuffer = CMSampleBufferGetImageBuffer(sampleBuffer); + CMTime nextSampleTime = CMTimeSubtract(_lastVideoSampleTime, _videoTimeOffset); + [_videoAdaptor appendPixelBuffer:nextBuffer withPresentationTime:nextSampleTime]; + } else { + CMTime dur = CMSampleBufferGetDuration(sampleBuffer); + + if (dur.value > 0) { + currentSampleTime = CMTimeAdd(currentSampleTime, dur); + } + + if (_audioIsDisconnected) { + _audioIsDisconnected = NO; + + if (_audioTimeOffset.value == 0) { + _audioTimeOffset = CMTimeSubtract(currentSampleTime, _lastAudioSampleTime); + } else { + CMTime offset = CMTimeSubtract(currentSampleTime, _lastAudioSampleTime); + _audioTimeOffset = CMTimeAdd(_audioTimeOffset, offset); + } + + return; + } + + _lastAudioSampleTime = currentSampleTime; + + if (_audioTimeOffset.value != 0) { + CFRelease(sampleBuffer); + sampleBuffer = [self adjustTime:sampleBuffer by:_audioTimeOffset]; + } + + [self newAudioSample:sampleBuffer]; + } + + CFRelease(sampleBuffer); + } +} + +- (CMSampleBufferRef)adjustTime:(CMSampleBufferRef)sample by:(CMTime)offset CF_RETURNS_RETAINED { + CMItemCount count; + CMSampleBufferGetSampleTimingInfoArray(sample, 0, nil, &count); + CMSampleTimingInfo *pInfo = malloc(sizeof(CMSampleTimingInfo) * count); + CMSampleBufferGetSampleTimingInfoArray(sample, count, pInfo, &count); + for (CMItemCount i = 0; i < count; i++) { + pInfo[i].decodeTimeStamp = CMTimeSubtract(pInfo[i].decodeTimeStamp, offset); + pInfo[i].presentationTimeStamp = CMTimeSubtract(pInfo[i].presentationTimeStamp, offset); + } + CMSampleBufferRef sout; + CMSampleBufferCreateCopyWithNewTiming(nil, sample, count, pInfo, &sout); + free(pInfo); + return sout; +} + +- (void)newVideoSample:(CMSampleBufferRef)sampleBuffer { + if (_videoWriter.status != AVAssetWriterStatusWriting) { + if (_videoWriter.status == AVAssetWriterStatusFailed) { + [_methodChannel invokeMethod:errorMethod + arguments:[NSString stringWithFormat:@"%@", _videoWriter.error]]; + } + return; + } + if (_videoWriterInput.readyForMoreMediaData) { + if (![_videoWriterInput appendSampleBuffer:sampleBuffer]) { + [_methodChannel + invokeMethod:errorMethod + arguments:[NSString stringWithFormat:@"%@", @"Unable to write to video input"]]; + } + } +} + +- (void)newAudioSample:(CMSampleBufferRef)sampleBuffer { + if (_videoWriter.status != AVAssetWriterStatusWriting) { + if (_videoWriter.status == AVAssetWriterStatusFailed) { + [_methodChannel invokeMethod:errorMethod + arguments:[NSString stringWithFormat:@"%@", _videoWriter.error]]; + } + return; + } + if (_audioWriterInput.readyForMoreMediaData) { + if (![_audioWriterInput appendSampleBuffer:sampleBuffer]) { + [_methodChannel + invokeMethod:errorMethod + arguments:[NSString stringWithFormat:@"%@", @"Unable to write to audio input"]]; + } + } +} + +- (void)close { + [_captureSession stopRunning]; + for (AVCaptureInput *input in [_captureSession inputs]) { + [_captureSession removeInput:input]; + } + for (AVCaptureOutput *output in [_captureSession outputs]) { + [_captureSession removeOutput:output]; + } +} + +- (void)dealloc { + if (_latestPixelBuffer) { + CFRelease(_latestPixelBuffer); + } + [_motionManager stopAccelerometerUpdates]; +} + +- (CVPixelBufferRef)copyPixelBuffer { + CVPixelBufferRef pixelBuffer = _latestPixelBuffer; + while (!OSAtomicCompareAndSwapPtrBarrier(pixelBuffer, nil, (void **)&_latestPixelBuffer)) { + pixelBuffer = _latestPixelBuffer; + } + + return pixelBuffer; +} + +- (void)startVideoRecordingWithResult:(FlutterResult)result { + if (!_isRecording) { + NSError *error; + _videoRecordingPath = [self getTemporaryFilePathWithExtension:@"mp4" + subfolder:@"videos" + prefix:@"REC_" + error:error]; + if (error) { + result(getFlutterError(error)); + return; + } + if (![self setupWriterForPath:_videoRecordingPath]) { + result([FlutterError errorWithCode:@"IOError" message:@"Setup Writer Failed" details:nil]); + return; + } + _isRecording = YES; + _isRecordingPaused = NO; + _videoTimeOffset = CMTimeMake(0, 1); + _audioTimeOffset = CMTimeMake(0, 1); + _videoIsDisconnected = NO; + _audioIsDisconnected = NO; + result(nil); + } else { + result([FlutterError errorWithCode:@"Error" message:@"Video is already recording" details:nil]); + } +} + +- (void)stopVideoRecordingWithResult:(FlutterResult)result { + if (_isRecording) { + _isRecording = NO; + + if (_videoWriter.status != AVAssetWriterStatusUnknown) { + [_videoWriter finishWritingWithCompletionHandler:^{ + if (self->_videoWriter.status == AVAssetWriterStatusCompleted) { + [self updateOrientation]; + result(self->_videoRecordingPath); + self->_videoRecordingPath = nil; + } else { + result([FlutterError errorWithCode:@"IOError" + message:@"AVAssetWriter could not finish writing!" + details:nil]); + } + }]; + } + } else { + NSError *error = + [NSError errorWithDomain:NSCocoaErrorDomain + code:NSURLErrorResourceUnavailable + userInfo:@{NSLocalizedDescriptionKey : @"Video is not recording!"}]; + result(getFlutterError(error)); + } +} + +- (void)pauseVideoRecordingWithResult:(FlutterResult)result { + _isRecordingPaused = YES; + _videoIsDisconnected = YES; + _audioIsDisconnected = YES; + result(nil); +} + +- (void)resumeVideoRecordingWithResult:(FlutterResult)result { + _isRecordingPaused = NO; + result(nil); +} + +- (void)lockCaptureOrientationWithResult:(FlutterResult)result + orientation:(NSString *)orientationStr { + UIDeviceOrientation orientation; + @try { + orientation = getUIDeviceOrientationForString(orientationStr); + } @catch (NSError *e) { + result(getFlutterError(e)); + return; + } + + if (_lockedCaptureOrientation != orientation) { + _lockedCaptureOrientation = orientation; + [self updateOrientation]; + } + + result(nil); +} + +- (void)unlockCaptureOrientationWithResult:(FlutterResult)result { + _lockedCaptureOrientation = UIDeviceOrientationUnknown; + [self updateOrientation]; + result(nil); +} + +- (void)setFlashModeWithResult:(FlutterResult)result mode:(NSString *)modeStr { + FlashMode mode; + @try { + mode = getFlashModeForString(modeStr); + } @catch (NSError *e) { + result(getFlutterError(e)); + return; + } + if (mode == FlashModeTorch) { + if (!_captureDevice.hasTorch) { + result([FlutterError errorWithCode:@"setFlashModeFailed" + message:@"Device does not support torch mode" + details:nil]); + return; + } + if (!_captureDevice.isTorchAvailable) { + result([FlutterError errorWithCode:@"setFlashModeFailed" + message:@"Torch mode is currently not available" + details:nil]); + return; + } + if (_captureDevice.torchMode != AVCaptureTorchModeOn) { + [_captureDevice lockForConfiguration:nil]; + [_captureDevice setTorchMode:AVCaptureTorchModeOn]; + [_captureDevice unlockForConfiguration]; + } + } else { + if (!_captureDevice.hasFlash) { + result([FlutterError errorWithCode:@"setFlashModeFailed" + message:@"Device does not have flash capabilities" + details:nil]); + return; + } + AVCaptureFlashMode avFlashMode = getAVCaptureFlashModeForFlashMode(mode); + if (![_capturePhotoOutput.supportedFlashModes + containsObject:[NSNumber numberWithInt:((int)avFlashMode)]]) { + result([FlutterError errorWithCode:@"setFlashModeFailed" + message:@"Device does not support this specific flash mode" + details:nil]); + return; + } + if (_captureDevice.torchMode != AVCaptureTorchModeOff) { + [_captureDevice lockForConfiguration:nil]; + [_captureDevice setTorchMode:AVCaptureTorchModeOff]; + [_captureDevice unlockForConfiguration]; + } + } + _flashMode = mode; + result(nil); +} + +- (void)setExposureModeWithResult:(FlutterResult)result mode:(NSString *)modeStr { + ExposureMode mode; + @try { + mode = getExposureModeForString(modeStr); + } @catch (NSError *e) { + result(getFlutterError(e)); + return; + } + _exposureMode = mode; + [self applyExposureMode]; + result(nil); +} + +- (void)applyExposureMode { + [_captureDevice lockForConfiguration:nil]; + switch (_exposureMode) { + case ExposureModeLocked: + [_captureDevice setExposureMode:AVCaptureExposureModeAutoExpose]; + break; + case ExposureModeAuto: + if ([_captureDevice isExposureModeSupported:AVCaptureExposureModeContinuousAutoExposure]) { + [_captureDevice setExposureMode:AVCaptureExposureModeContinuousAutoExposure]; + } else { + [_captureDevice setExposureMode:AVCaptureExposureModeAutoExpose]; + } + break; + } + [_captureDevice unlockForConfiguration]; +} + +- (void)setFocusModeWithResult:(FlutterResult)result mode:(NSString *)modeStr { + FocusMode mode; + @try { + mode = getFocusModeForString(modeStr); + } @catch (NSError *e) { + result(getFlutterError(e)); + return; + } + _focusMode = mode; + [self applyFocusMode]; + result(nil); +} + +- (void)applyFocusMode { + [self applyFocusMode:_focusMode onDevice:_captureDevice]; +} + +/** + * Applies FocusMode on the AVCaptureDevice. + * + * If the @c focusMode is set to FocusModeAuto the AVCaptureDevice is configured to use + * AVCaptureFocusModeContinuousModeAutoFocus when supported, otherwise it is set to + * AVCaptureFocusModeAutoFocus. If neither AVCaptureFocusModeContinuousModeAutoFocus nor + * AVCaptureFocusModeAutoFocus are supported focus mode will not be set. + * If @c focusMode is set to FocusModeLocked the AVCaptureDevice is configured to use + * AVCaptureFocusModeAutoFocus. If AVCaptureFocusModeAutoFocus is not supported focus mode will not + * be set. + * + * @param focusMode The focus mode that should be applied to the @captureDevice instance. + * @param captureDevice The AVCaptureDevice to which the @focusMode will be applied. + */ +- (void)applyFocusMode:(FocusMode)focusMode onDevice:(AVCaptureDevice *)captureDevice { + [captureDevice lockForConfiguration:nil]; + switch (focusMode) { + case FocusModeLocked: + if ([captureDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]) { + [captureDevice setFocusMode:AVCaptureFocusModeAutoFocus]; + } + break; + case FocusModeAuto: + if ([captureDevice isFocusModeSupported:AVCaptureFocusModeContinuousAutoFocus]) { + [captureDevice setFocusMode:AVCaptureFocusModeContinuousAutoFocus]; + } else if ([captureDevice isFocusModeSupported:AVCaptureFocusModeAutoFocus]) { + [captureDevice setFocusMode:AVCaptureFocusModeAutoFocus]; + } + break; + } + [captureDevice unlockForConfiguration]; +} + +- (void)pausePreviewWithResult:(FlutterResult)result { + _isPreviewPaused = true; + result(nil); +} + +- (void)resumePreviewWithResult:(FlutterResult)result { + _isPreviewPaused = false; + result(nil); +} + +- (CGPoint)getCGPointForCoordsWithOrientation:(UIDeviceOrientation)orientation + x:(double)x + y:(double)y { + double oldX = x, oldY = y; + switch (orientation) { + case UIDeviceOrientationPortrait: // 90 ccw + y = 1 - oldX; + x = oldY; + break; + case UIDeviceOrientationPortraitUpsideDown: // 90 cw + x = 1 - oldY; + y = oldX; + break; + case UIDeviceOrientationLandscapeRight: // 180 + x = 1 - x; + y = 1 - y; + break; + case UIDeviceOrientationLandscapeLeft: + default: + // No rotation required + break; + } + return CGPointMake(x, y); +} + +- (void)setExposurePointWithResult:(FlutterResult)result x:(double)x y:(double)y { + if (!_captureDevice.isExposurePointOfInterestSupported) { + result([FlutterError errorWithCode:@"setExposurePointFailed" + message:@"Device does not have exposure point capabilities" + details:nil]); + return; + } + UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation]; + [_captureDevice lockForConfiguration:nil]; + [_captureDevice setExposurePointOfInterest:[self getCGPointForCoordsWithOrientation:orientation + x:x + y:y]]; + [_captureDevice unlockForConfiguration]; + // Retrigger auto exposure + [self applyExposureMode]; + result(nil); +} + +- (void)setFocusPointWithResult:(FlutterResult)result x:(double)x y:(double)y { + if (!_captureDevice.isFocusPointOfInterestSupported) { + result([FlutterError errorWithCode:@"setFocusPointFailed" + message:@"Device does not have focus point capabilities" + details:nil]); + return; + } + UIDeviceOrientation orientation = [[UIDevice currentDevice] orientation]; + [_captureDevice lockForConfiguration:nil]; + + [_captureDevice setFocusPointOfInterest:[self getCGPointForCoordsWithOrientation:orientation + x:x + y:y]]; + [_captureDevice unlockForConfiguration]; + // Retrigger auto focus + [self applyFocusMode]; + + result(nil); +} + +- (void)setExposureOffsetWithResult:(FlutterResult)result offset:(double)offset { + [_captureDevice lockForConfiguration:nil]; + [_captureDevice setExposureTargetBias:offset completionHandler:nil]; + [_captureDevice unlockForConfiguration]; + result(@(offset)); +} + +- (void)startImageStreamWithMessenger:(NSObject *)messenger { + if (!_isStreamingImages) { + FlutterEventChannel *eventChannel = + [FlutterEventChannel eventChannelWithName:@"plugins.flutter.io/camera/imageStream" + binaryMessenger:messenger]; + + _imageStreamHandler = [[FLTImageStreamHandler alloc] init]; + [eventChannel setStreamHandler:_imageStreamHandler]; + + _isStreamingImages = YES; + } else { + [_methodChannel invokeMethod:errorMethod + arguments:@"Images from camera are already streaming!"]; + } +} + +- (void)stopImageStream { + if (_isStreamingImages) { + _isStreamingImages = NO; + _imageStreamHandler = nil; + } else { + [_methodChannel invokeMethod:errorMethod arguments:@"Images from camera are not streaming!"]; + } +} + +- (void)getMaxZoomLevelWithResult:(FlutterResult)result { + CGFloat maxZoomFactor = [self getMaxAvailableZoomFactor]; + + result([NSNumber numberWithFloat:maxZoomFactor]); +} + +- (void)getMinZoomLevelWithResult:(FlutterResult)result { + CGFloat minZoomFactor = [self getMinAvailableZoomFactor]; + + result([NSNumber numberWithFloat:minZoomFactor]); +} + +- (void)setZoomLevel:(CGFloat)zoom Result:(FlutterResult)result { + CGFloat maxAvailableZoomFactor = [self getMaxAvailableZoomFactor]; + CGFloat minAvailableZoomFactor = [self getMinAvailableZoomFactor]; + + if (maxAvailableZoomFactor < zoom || minAvailableZoomFactor > zoom) { + NSString *errorMessage = [NSString + stringWithFormat:@"Zoom level out of bounds (zoom level should be between %f and %f).", + minAvailableZoomFactor, maxAvailableZoomFactor]; + FlutterError *error = [FlutterError errorWithCode:@"ZOOM_ERROR" + message:errorMessage + details:nil]; + result(error); + return; + } + + NSError *error = nil; + if (![_captureDevice lockForConfiguration:&error]) { + result(getFlutterError(error)); + return; + } + _captureDevice.videoZoomFactor = zoom; + [_captureDevice unlockForConfiguration]; + + result(nil); +} + +- (CGFloat)getMinAvailableZoomFactor { + if (@available(iOS 11.0, *)) { + return _captureDevice.minAvailableVideoZoomFactor; + } else { + return 1.0; + } +} + +- (CGFloat)getMaxAvailableZoomFactor { + if (@available(iOS 11.0, *)) { + return _captureDevice.maxAvailableVideoZoomFactor; + } else { + return _captureDevice.activeFormat.videoMaxZoomFactor; + } +} + +- (BOOL)setupWriterForPath:(NSString *)path { + NSError *error = nil; + NSURL *outputURL; + if (path != nil) { + outputURL = [NSURL fileURLWithPath:path]; + } else { + return NO; + } + if (_enableAudio && !_isAudioSetup) { + [self setUpCaptureSessionForAudio]; + } + + _videoWriter = [[AVAssetWriter alloc] initWithURL:outputURL + fileType:AVFileTypeMPEG4 + error:&error]; + NSParameterAssert(_videoWriter); + if (error) { + [_methodChannel invokeMethod:errorMethod arguments:error.description]; + return NO; + } + + NSDictionary *videoSettings = [_captureVideoOutput + recommendedVideoSettingsForAssetWriterWithOutputFileType:AVFileTypeMPEG4]; + _videoWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo + outputSettings:videoSettings]; + + _videoAdaptor = [AVAssetWriterInputPixelBufferAdaptor + assetWriterInputPixelBufferAdaptorWithAssetWriterInput:_videoWriterInput + sourcePixelBufferAttributes:@{ + (NSString *)kCVPixelBufferPixelFormatTypeKey : @(videoFormat) + }]; + + NSParameterAssert(_videoWriterInput); + + _videoWriterInput.expectsMediaDataInRealTime = YES; + + // Add the audio input + if (_enableAudio) { + AudioChannelLayout acl; + bzero(&acl, sizeof(acl)); + acl.mChannelLayoutTag = kAudioChannelLayoutTag_Mono; + NSDictionary *audioOutputSettings = nil; + // Both type of audio inputs causes output video file to be corrupted. + audioOutputSettings = @{ + AVFormatIDKey : [NSNumber numberWithInt:kAudioFormatMPEG4AAC], + AVSampleRateKey : [NSNumber numberWithFloat:44100.0], + AVNumberOfChannelsKey : [NSNumber numberWithInt:1], + AVChannelLayoutKey : [NSData dataWithBytes:&acl length:sizeof(acl)], + }; + _audioWriterInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio + outputSettings:audioOutputSettings]; + _audioWriterInput.expectsMediaDataInRealTime = YES; + + [_videoWriter addInput:_audioWriterInput]; + [_audioOutput setSampleBufferDelegate:self queue:_dispatchQueue]; + } + + if (_flashMode == FlashModeTorch) { + [self.captureDevice lockForConfiguration:nil]; + [self.captureDevice setTorchMode:AVCaptureTorchModeOn]; + [self.captureDevice unlockForConfiguration]; + } + + [_videoWriter addInput:_videoWriterInput]; + + [_captureVideoOutput setSampleBufferDelegate:self queue:_dispatchQueue]; + + return YES; +} + +- (void)setUpCaptureSessionForAudio { + NSError *error = nil; + // Create a device input with the device and add it to the session. + // Setup the audio input. + AVCaptureDevice *audioDevice = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeAudio]; + AVCaptureDeviceInput *audioInput = [AVCaptureDeviceInput deviceInputWithDevice:audioDevice + error:&error]; + if (error) { + [_methodChannel invokeMethod:errorMethod arguments:error.description]; + } + // Setup the audio output. + _audioOutput = [[AVCaptureAudioDataOutput alloc] init]; + + if ([_captureSession canAddInput:audioInput]) { + [_captureSession addInput:audioInput]; + + if ([_captureSession canAddOutput:_audioOutput]) { + [_captureSession addOutput:_audioOutput]; + _isAudioSetup = YES; + } else { + [_methodChannel invokeMethod:errorMethod + arguments:@"Unable to add Audio input/output to session capture"]; + _isAudioSetup = NO; + } + } +} +@end + +@interface CameraPlugin () +@property(readonly, nonatomic) NSObject *registry; +@property(readonly, nonatomic) NSObject *messenger; +@property(readonly, nonatomic) FLTCam *camera; +@property(readonly, nonatomic) FlutterMethodChannel *deviceEventMethodChannel; +@end + +@implementation CameraPlugin { + dispatch_queue_t _dispatchQueue; +} ++ (void)registerWithRegistrar:(NSObject *)registrar { + FlutterMethodChannel *channel = + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/camera" + binaryMessenger:[registrar messenger]]; + CameraPlugin *instance = [[CameraPlugin alloc] initWithRegistry:[registrar textures] + messenger:[registrar messenger]]; + [registrar addMethodCallDelegate:instance channel:channel]; +} + +- (instancetype)initWithRegistry:(NSObject *)registry + messenger:(NSObject *)messenger { + self = [super init]; + NSAssert(self, @"super init cannot be nil"); + _registry = registry; + _messenger = messenger; + [self initDeviceEventMethodChannel]; + [self startOrientationListener]; + return self; +} + +- (void)initDeviceEventMethodChannel { + _deviceEventMethodChannel = + [FlutterMethodChannel methodChannelWithName:@"flutter.io/cameraPlugin/device" + binaryMessenger:_messenger]; +} + +- (void)startOrientationListener { + [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(orientationChanged:) + name:UIDeviceOrientationDidChangeNotification + object:[UIDevice currentDevice]]; +} + +- (void)orientationChanged:(NSNotification *)note { + UIDevice *device = note.object; + UIDeviceOrientation orientation = device.orientation; + + if (orientation == UIDeviceOrientationFaceUp || orientation == UIDeviceOrientationFaceDown) { + // Do not change when oriented flat. + return; + } + + if (_camera) { + [_camera setDeviceOrientation:orientation]; + } + + [self sendDeviceOrientation:orientation]; +} + +- (void)sendDeviceOrientation:(UIDeviceOrientation)orientation { + [_deviceEventMethodChannel + invokeMethod:@"orientation_changed" + arguments:@{@"orientation" : getStringForUIDeviceOrientation(orientation)}]; +} + +- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { + if (_dispatchQueue == nil) { + _dispatchQueue = dispatch_queue_create("io.flutter.camera.dispatchqueue", NULL); + } + + // Invoke the plugin on another dispatch queue to avoid blocking the UI. + dispatch_async(_dispatchQueue, ^{ + [self handleMethodCallAsync:call result:result]; + }); +} + +- (void)handleMethodCallAsync:(FlutterMethodCall *)call result:(FlutterResult)result { + if ([@"availableCameras" isEqualToString:call.method]) { + if (@available(iOS 10.0, *)) { + AVCaptureDeviceDiscoverySession *discoverySession = [AVCaptureDeviceDiscoverySession + discoverySessionWithDeviceTypes:@[ AVCaptureDeviceTypeBuiltInWideAngleCamera ] + mediaType:AVMediaTypeVideo + position:AVCaptureDevicePositionUnspecified]; + NSArray *devices = discoverySession.devices; + NSMutableArray *> *reply = + [[NSMutableArray alloc] initWithCapacity:devices.count]; + for (AVCaptureDevice *device in devices) { + NSString *lensFacing; + switch ([device position]) { + case AVCaptureDevicePositionBack: + lensFacing = @"back"; + break; + case AVCaptureDevicePositionFront: + lensFacing = @"front"; + break; + case AVCaptureDevicePositionUnspecified: + lensFacing = @"external"; + break; + } + [reply addObject:@{ + @"name" : [device uniqueID], + @"lensFacing" : lensFacing, + @"sensorOrientation" : @90, + }]; + } + result(reply); + } else { + result(FlutterMethodNotImplemented); + } + } else if ([@"create" isEqualToString:call.method]) { + NSString *cameraName = call.arguments[@"cameraName"]; + NSString *resolutionPreset = call.arguments[@"resolutionPreset"]; + NSNumber *enableAudio = call.arguments[@"enableAudio"]; + NSError *error; + FLTCam *cam = [[FLTCam alloc] initWithCameraName:cameraName + resolutionPreset:resolutionPreset + enableAudio:[enableAudio boolValue] + orientation:[[UIDevice currentDevice] orientation] + dispatchQueue:_dispatchQueue + error:&error]; + + if (error) { + result(getFlutterError(error)); + } else { + if (_camera) { + [_camera close]; + } + int64_t textureId = [_registry registerTexture:cam]; + _camera = cam; + + result(@{ + @"cameraId" : @(textureId), + }); + } + } else if ([@"startImageStream" isEqualToString:call.method]) { + [_camera startImageStreamWithMessenger:_messenger]; + result(nil); + } else if ([@"stopImageStream" isEqualToString:call.method]) { + [_camera stopImageStream]; + result(nil); + } else { + NSDictionary *argsMap = call.arguments; + NSUInteger cameraId = ((NSNumber *)argsMap[@"cameraId"]).unsignedIntegerValue; + if ([@"initialize" isEqualToString:call.method]) { + NSString *videoFormatValue = ((NSString *)argsMap[@"imageFormatGroup"]); + videoFormat = getVideoFormatFromString(videoFormatValue); + + __weak CameraPlugin *weakSelf = self; + _camera.onFrameAvailable = ^{ + if (![weakSelf.camera isPreviewPaused]) { + [weakSelf.registry textureFrameAvailable:cameraId]; + } + }; + FlutterMethodChannel *methodChannel = [FlutterMethodChannel + methodChannelWithName:[NSString stringWithFormat:@"flutter.io/cameraPlugin/camera%lu", + (unsigned long)cameraId] + binaryMessenger:_messenger]; + _camera.methodChannel = methodChannel; + [methodChannel + invokeMethod:@"initialized" + arguments:@{ + @"previewWidth" : @(_camera.previewSize.width), + @"previewHeight" : @(_camera.previewSize.height), + @"exposureMode" : getStringForExposureMode([_camera exposureMode]), + @"focusMode" : getStringForFocusMode([_camera focusMode]), + @"exposurePointSupported" : + @([_camera.captureDevice isExposurePointOfInterestSupported]), + @"focusPointSupported" : @([_camera.captureDevice isFocusPointOfInterestSupported]), + }]; + [self sendDeviceOrientation:[UIDevice currentDevice].orientation]; + [_camera start]; + result(nil); + } else if ([@"takePicture" isEqualToString:call.method]) { + if (@available(iOS 10.0, *)) { + [_camera captureToFile:result]; + } else { + result(FlutterMethodNotImplemented); + } + } else if ([@"dispose" isEqualToString:call.method]) { + [_registry unregisterTexture:cameraId]; + [_camera close]; + _dispatchQueue = nil; + result(nil); + } else if ([@"prepareForVideoRecording" isEqualToString:call.method]) { + [_camera setUpCaptureSessionForAudio]; + result(nil); + } else if ([@"startVideoRecording" isEqualToString:call.method]) { + [_camera startVideoRecordingWithResult:result]; + } else if ([@"stopVideoRecording" isEqualToString:call.method]) { + [_camera stopVideoRecordingWithResult:result]; + } else if ([@"pauseVideoRecording" isEqualToString:call.method]) { + [_camera pauseVideoRecordingWithResult:result]; + } else if ([@"resumeVideoRecording" isEqualToString:call.method]) { + [_camera resumeVideoRecordingWithResult:result]; + } else if ([@"getMaxZoomLevel" isEqualToString:call.method]) { + [_camera getMaxZoomLevelWithResult:result]; + } else if ([@"getMinZoomLevel" isEqualToString:call.method]) { + [_camera getMinZoomLevelWithResult:result]; + } else if ([@"setZoomLevel" isEqualToString:call.method]) { + CGFloat zoom = ((NSNumber *)argsMap[@"zoom"]).floatValue; + [_camera setZoomLevel:zoom Result:result]; + } else if ([@"setFlashMode" isEqualToString:call.method]) { + [_camera setFlashModeWithResult:result mode:call.arguments[@"mode"]]; + } else if ([@"setExposureMode" isEqualToString:call.method]) { + [_camera setExposureModeWithResult:result mode:call.arguments[@"mode"]]; + } else if ([@"setExposurePoint" isEqualToString:call.method]) { + BOOL reset = ((NSNumber *)call.arguments[@"reset"]).boolValue; + double x = 0.5; + double y = 0.5; + if (!reset) { + x = ((NSNumber *)call.arguments[@"x"]).doubleValue; + y = ((NSNumber *)call.arguments[@"y"]).doubleValue; + } + [_camera setExposurePointWithResult:result x:x y:y]; + } else if ([@"getMinExposureOffset" isEqualToString:call.method]) { + result(@(_camera.captureDevice.minExposureTargetBias)); + } else if ([@"getMaxExposureOffset" isEqualToString:call.method]) { + result(@(_camera.captureDevice.maxExposureTargetBias)); + } else if ([@"getExposureOffsetStepSize" isEqualToString:call.method]) { + result(@(0.0)); + } else if ([@"setExposureOffset" isEqualToString:call.method]) { + [_camera setExposureOffsetWithResult:result + offset:((NSNumber *)call.arguments[@"offset"]).doubleValue]; + } else if ([@"lockCaptureOrientation" isEqualToString:call.method]) { + [_camera lockCaptureOrientationWithResult:result orientation:call.arguments[@"orientation"]]; + } else if ([@"unlockCaptureOrientation" isEqualToString:call.method]) { + [_camera unlockCaptureOrientationWithResult:result]; + } else if ([@"setFocusMode" isEqualToString:call.method]) { + [_camera setFocusModeWithResult:result mode:call.arguments[@"mode"]]; + } else if ([@"setFocusPoint" isEqualToString:call.method]) { + BOOL reset = ((NSNumber *)call.arguments[@"reset"]).boolValue; + double x = 0.5; + double y = 0.5; + if (!reset) { + x = ((NSNumber *)call.arguments[@"x"]).doubleValue; + y = ((NSNumber *)call.arguments[@"y"]).doubleValue; + } + [_camera setFocusPointWithResult:result x:x y:y]; + } else if ([@"pausePreview" isEqualToString:call.method]) { + [_camera pausePreviewWithResult:result]; + } else if ([@"resumePreview" isEqualToString:call.method]) { + [_camera resumePreviewWithResult:result]; + } else { + result(FlutterMethodNotImplemented); + } + } +} + +@end diff --git a/packages/camera/camera/ios/camera.podspec b/packages/camera/camera/ios/camera.podspec new file mode 100644 index 000000000000..4a142bd4589a --- /dev/null +++ b/packages/camera/camera/ios/camera.podspec @@ -0,0 +1,22 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'camera' + s.version = '0.0.1' + s.summary = 'Flutter Camera' + s.description = <<-DESC +A Flutter plugin to use the camera from your Flutter app. + DESC + s.homepage = 'https://github.com/flutter/plugins' + s.license = { :type => 'BSD', :file => '../LICENSE' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/camera' } + s.documentation_url = 'https://pub.dev/packages/camera' + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + + s.platform = :ios, '9.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } +end diff --git a/packages/camera/camera/lib/camera.dart b/packages/camera/camera/lib/camera.dart 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..8cf1e90e36c1 --- /dev/null +++ b/packages/camera/camera/lib/src/camera_controller.dart @@ -0,0 +1,837 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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, + this.isPreviewPaused = false, + this.previewPauseOrientation, + }) : _isRecordingPaused = isRecordingPaused; + + /// Creates a new camera controller state for an uninitialized controller. + const CameraValue.uninitialized() + : this( + isInitialized: false, + isRecordingVideo: false, + isTakingPicture: false, + isStreamingImages: false, + isRecordingPaused: false, + flashMode: FlashMode.auto, + exposureMode: ExposureMode.auto, + exposurePointSupported: false, + focusMode: FocusMode.auto, + focusPointSupported: false, + deviceOrientation: DeviceOrientation.portraitUp, + isPreviewPaused: false, + ); + + /// True after [CameraController.initialize] has completed successfully. + final bool isInitialized; + + /// True when a picture capture request has been sent but as not yet returned. + final bool isTakingPicture; + + /// True when the camera is recording (not the same as previewing). + final bool isRecordingVideo; + + /// True when images from the camera are being streamed. + final bool isStreamingImages; + + final bool _isRecordingPaused; + + /// True when the preview widget has been paused manually. + final bool isPreviewPaused; + + /// Set to the orientation the preview was paused in, if it is currently paused. + final DeviceOrientation? previewPauseOrientation; + + /// True when camera [isRecordingVideo] and recording is paused. + bool get isRecordingPaused => isRecordingVideo && _isRecordingPaused; + + /// Description of an error state. + /// + /// This is null while the controller is not in an error state. + /// When [hasError] is true this contains the error description. + final String? errorDescription; + + /// The size of the preview in pixels. + /// + /// Is `null` until [isInitialized] is `true`. + final Size? previewSize; + + /// Convenience getter for `previewSize.width / previewSize.height`. + /// + /// Can only be called when [initialize] is done. + double get aspectRatio => previewSize!.width / previewSize!.height; + + /// Whether the controller is in an error state. + /// + /// When true [errorDescription] describes the error. + bool get hasError => errorDescription != null; + + /// The flash mode the camera is currently set to. + final FlashMode flashMode; + + /// The exposure mode the camera is currently set to. + final ExposureMode exposureMode; + + /// The focus mode the camera is currently set to. + final FocusMode focusMode; + + /// Whether setting the exposure point is supported. + final bool exposurePointSupported; + + /// Whether setting the focus point is supported. + final bool focusPointSupported; + + /// The current device UI orientation. + final DeviceOrientation deviceOrientation; + + /// The currently locked capture orientation. + final DeviceOrientation? lockedCaptureOrientation; + + /// Whether the capture orientation is currently locked. + bool get isCaptureOrientationLocked => lockedCaptureOrientation != null; + + /// The orientation of the currently running video recording. + final DeviceOrientation? recordingOrientation; + + /// Creates a modified copy of the object. + /// + /// Explicitly specified fields get the specified value, all other fields get + /// the same value of the current object. + CameraValue copyWith({ + bool? isInitialized, + bool? isRecordingVideo, + bool? isTakingPicture, + bool? isStreamingImages, + String? errorDescription, + Size? previewSize, + bool? isRecordingPaused, + FlashMode? flashMode, + ExposureMode? exposureMode, + FocusMode? focusMode, + bool? exposurePointSupported, + bool? focusPointSupported, + DeviceOrientation? deviceOrientation, + Optional? lockedCaptureOrientation, + Optional? recordingOrientation, + bool? isPreviewPaused, + Optional? previewPauseOrientation, + }) { + return CameraValue( + isInitialized: isInitialized ?? this.isInitialized, + errorDescription: errorDescription, + previewSize: previewSize ?? this.previewSize, + isRecordingVideo: isRecordingVideo ?? this.isRecordingVideo, + isTakingPicture: isTakingPicture ?? this.isTakingPicture, + isStreamingImages: isStreamingImages ?? this.isStreamingImages, + isRecordingPaused: isRecordingPaused ?? _isRecordingPaused, + flashMode: flashMode ?? this.flashMode, + exposureMode: exposureMode ?? this.exposureMode, + focusMode: focusMode ?? this.focusMode, + exposurePointSupported: + exposurePointSupported ?? this.exposurePointSupported, + focusPointSupported: focusPointSupported ?? this.focusPointSupported, + deviceOrientation: deviceOrientation ?? this.deviceOrientation, + lockedCaptureOrientation: lockedCaptureOrientation == null + ? this.lockedCaptureOrientation + : lockedCaptureOrientation.orNull, + recordingOrientation: recordingOrientation == null + ? this.recordingOrientation + : recordingOrientation.orNull, + isPreviewPaused: isPreviewPaused ?? this.isPreviewPaused, + previewPauseOrientation: previewPauseOrientation == null + ? this.previewPauseOrientation + : previewPauseOrientation.orNull, + ); + } + + @override + String toString() { + return '$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, ' + 'isPreviewPaused: $isPreviewPaused, ' + 'previewPausedOrientation: $previewPauseOrientation)'; + } +} + +/// Controls a device camera. +/// +/// Use [availableCameras] to get a list of available cameras. +/// +/// Before using a [CameraController] a call to [initialize] must complete. +/// +/// To show the camera preview on the screen use a [CameraPreview] widget. +class CameraController extends ValueNotifier { + /// Creates a new camera controller in an uninitialized state. + CameraController( + this.description, + this.resolutionPreset, { + this.enableAudio = true, + this.imageFormatGroup, + }) : super(const CameraValue.uninitialized()); + + /// The properties of the camera device controlled by this controller. + final CameraDescription description; + + /// The resolution this controller is targeting. + /// + /// This resolution preset is not guaranteed to be available on the device, + /// if unavailable a lower resolution will be used. + /// + /// See also: [ResolutionPreset]. + final ResolutionPreset resolutionPreset; + + /// Whether to include audio when recording a video. + final bool enableAudio; + + /// The [ImageFormatGroup] describes the output of the raw image format. + /// + /// When null the imageFormat will fallback to the platforms default. + final ImageFormatGroup? imageFormatGroup; + + /// The id of a camera that hasn't been initialized. + @visibleForTesting + static const int kUninitializedCameraId = -1; + int _cameraId = kUninitializedCameraId; + + bool _isDisposed = false; + StreamSubscription? _imageStreamSubscription; + FutureOr? _initCalled; + StreamSubscription? _deviceOrientationSubscription; + + /// Checks whether [CameraController.dispose] has completed successfully. + /// + /// This is a no-op when asserts are disabled. + void debugCheckIsDisposed() { + assert(_isDisposed); + } + + /// The camera identifier with which the controller is associated. + int get cameraId => _cameraId; + + /// Initializes the camera on the device. + /// + /// Throws a [CameraException] if the initialization fails. + Future initialize() async { + if (_isDisposed) { + throw CameraException( + 'Disposed CameraController', + 'initialize was called on a disposed CameraController', + ); + } + try { + 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 and Web. + /// + /// Throws a [CameraException] if the prepare fails. + Future prepareForVideoRecording() async { + await CameraPlatform.instance.prepareForVideoRecording(); + } + + /// Pauses the current camera preview + Future pausePreview() async { + if (value.isPreviewPaused) { + return; + } + try { + await CameraPlatform.instance.pausePreview(_cameraId); + value = value.copyWith( + isPreviewPaused: true, + previewPauseOrientation: Optional.of(this.value.deviceOrientation)); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Resumes the current camera preview + Future resumePreview() async { + if (!value.isPreviewPaused) { + return; + } + try { + await CameraPlatform.instance.resumePreview(_cameraId); + value = value.copyWith( + isPreviewPaused: false, previewPauseOrientation: Optional.absent()); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + /// Captures an image and returns the file where it was saved. + /// + /// Throws a [CameraException] if the capture fails. + Future takePicture() async { + _throwIfNotInitialized("takePicture"); + if (value.isTakingPicture) { + throw CameraException( + 'Previous capture has not returned yet.', + 'takePicture was called before the previous capture returned.', + ); + } + try { + value = value.copyWith(isTakingPicture: true); + 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.', + ); + } + } + + @override + void removeListener(VoidCallback listener) { + // Prevent ValueListenableBuilder in CameraPreview widget from causing an + // exception to be thrown by attempting to remove its own listener after + // the controller has already been disposed. + if (!_isDisposed) { + super.removeListener(listener); + } + } +} diff --git a/packages/camera/camera/lib/src/camera_image.dart b/packages/camera/camera/lib/src/camera_image.dart new file mode 100644 index 000000000000..43fa763bed48 --- /dev/null +++ b/packages/camera/camera/lib/src/camera_image.dart @@ -0,0 +1,142 @@ +// Copyright 2013 The Flutter Authors. All 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'], + lensAperture = data['lensAperture'], + sensorExposureTime = data['sensorExposureTime'], + sensorSensitivity = data['sensorSensitivity'], + 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; + + /// The aperture settings for this image. + /// + /// Represented as an f-stop value. + final double? lensAperture; + + /// The sensor exposure time for this image in nanoseconds. + final int? sensorExposureTime; + + /// The sensor sensitivity in standard ISO arithmetic units. + final double? sensorSensitivity; +} diff --git a/packages/camera/camera/lib/src/camera_preview.dart b/packages/camera/camera/lib/src/camera_preview.dart new file mode 100644 index 000000000000..5faa69f3cb9d --- /dev/null +++ b/packages/camera/camera/lib/src/camera_preview.dart @@ -0,0 +1,78 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package: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 + ? ValueListenableBuilder( + valueListenable: controller, + builder: (context, value, child) { + return AspectRatio( + aspectRatio: _isLandscape() + ? controller.value.aspectRatio + : (1 / controller.value.aspectRatio), + child: Stack( + fit: StackFit.expand, + children: [ + _wrapInRotatedBox(child: controller.buildPreview()), + child ?? Container(), + ], + ), + ); + }, + child: child, + ) + : Container(); + } + + Widget _wrapInRotatedBox({required Widget child}) { + if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) { + return child; + } + + return RotatedBox( + quarterTurns: _getQuarterTurns(), + child: child, + ); + } + + bool _isLandscape() { + return [DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight] + .contains(_getApplicableOrientation()); + } + + int _getQuarterTurns() { + Map turns = { + DeviceOrientation.portraitUp: 0, + DeviceOrientation.landscapeRight: 1, + DeviceOrientation.portraitDown: 2, + DeviceOrientation.landscapeLeft: 3, + }; + return turns[_getApplicableOrientation()]!; + } + + DeviceOrientation _getApplicableOrientation() { + return controller.value.isRecordingVideo + ? controller.value.recordingOrientation! + : (controller.value.previewPauseOrientation ?? + controller.value.lockedCaptureOrientation ?? + controller.value.deviceOrientation); + } +} diff --git a/packages/camera/camera/pubspec.yaml b/packages/camera/camera/pubspec.yaml new file mode 100644 index 000000000000..21892b213781 --- /dev/null +++ b/packages/camera/camera/pubspec.yaml @@ -0,0 +1,40 @@ +name: camera +description: A Flutter plugin for controlling the camera. Supports previewing + the camera feed, capturing images and video, and streaming image buffers to + Dart. +repository: https://github.com/flutter/plugins/tree/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.9.4+1 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" + +flutter: + plugin: + platforms: + android: + package: io.flutter.plugins.camera + pluginClass: CameraPlugin + ios: + pluginClass: CameraPlugin + web: + default_package: camera_web + +dependencies: + camera_platform_interface: ^2.1.0 + camera_web: ^0.2.1 + flutter: + sdk: flutter + pedantic: ^1.10.0 + quiver: ^3.0.0 + flutter_plugin_android_lifecycle: ^2.0.2 + +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..85d613f41485 --- /dev/null +++ b/packages/camera/camera/test/camera_image_test.dart @@ -0,0 +1,129 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '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, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'bytesPerPixel': 1, + 'bytesPerRow': 4, + 'height': 1, + 'width': 4 + } + ] + }); + expect(cameraImage.height, 1); + expect(cameraImage.width, 4); + expect(cameraImage.format.group, ImageFormatGroup.yuv420); + expect(cameraImage.planes.length, 1); + }); + + test('$CameraImage has ImageFormatGroup.yuv420 for iOS', () { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + + CameraImage cameraImage = CameraImage.fromPlatformData({ + 'format': 875704438, + 'height': 1, + 'width': 4, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + '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, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'bytesPerPixel': 1, + 'bytesPerRow': 4, + 'height': 1, + 'width': 4 + } + ] + }); + expect(cameraImage.format.group, ImageFormatGroup.yuv420); + }); + + test('$CameraImage has ImageFormatGroup.bgra8888 for iOS', () { + debugDefaultTargetPlatformOverride = TargetPlatform.iOS; + + CameraImage cameraImage = CameraImage.fromPlatformData({ + 'format': 1111970369, + 'height': 1, + 'width': 4, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + '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, + 'lensAperture': 1.8, + 'sensorExposureTime': 9991324, + 'sensorSensitivity': 92.0, + 'planes': [ + { + 'bytes': Uint8List.fromList([1, 2, 3, 4]), + 'bytesPerPixel': 1, + 'bytesPerRow': 4, + 'height': 1, + 'width': 4 + } + ] + }); + expect(cameraImage.format.group, ImageFormatGroup.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..32718f4d5169 --- /dev/null +++ b/packages/camera/camera/test/camera_preview_test.dart @@ -0,0 +1,245 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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 {} + + @override + Future pausePreview() async {} + + @override + Future resumePreview() 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, 3); + + 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, 1); + + 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; + }); + }, skip: kIsWeb); + + 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..6904e68ef89f --- /dev/null +++ b/packages/camera/camera/test/camera_test.dart @@ -0,0 +1,1529 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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('pausePreview() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + cameraController.value = cameraController.value + .copyWith(deviceOrientation: DeviceOrientation.portraitUp); + + await cameraController.pausePreview(); + + verify(CameraPlatform.instance.pausePreview(cameraController.cameraId)) + .called(1); + expect(cameraController.value.isPreviewPaused, equals(true)); + expect(cameraController.value.previewPauseOrientation, + DeviceOrientation.portraitUp); + }); + + test('pausePreview() does not call $CameraPlatform when already paused', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + cameraController.value = + cameraController.value.copyWith(isPreviewPaused: true); + + await cameraController.pausePreview(); + + verifyNever( + CameraPlatform.instance.pausePreview(cameraController.cameraId)); + expect(cameraController.value.isPreviewPaused, equals(true)); + }); + + test('pausePreview() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + when(CameraPlatform.instance.pausePreview(cameraController.cameraId)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.pausePreview(), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + test('resumePreview() calls $CameraPlatform', () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + cameraController.value = + cameraController.value.copyWith(isPreviewPaused: true); + + await cameraController.resumePreview(); + + verify(CameraPlatform.instance.resumePreview(cameraController.cameraId)) + .called(1); + expect(cameraController.value.isPreviewPaused, equals(false)); + }); + + test('resumePreview() does not call $CameraPlatform when not paused', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + cameraController.value = + cameraController.value.copyWith(isPreviewPaused: false); + + await cameraController.resumePreview(); + + verifyNever( + CameraPlatform.instance.resumePreview(cameraController.cameraId)); + expect(cameraController.value.isPreviewPaused, equals(false)); + }); + + test('resumePreview() throws $CameraException on $PlatformException', + () async { + CameraController cameraController = CameraController( + CameraDescription( + name: 'cam', + lensDirection: CameraLensDirection.back, + sensorOrientation: 90), + ResolutionPreset.max); + await cameraController.initialize(); + cameraController.value = + cameraController.value.copyWith(isPreviewPaused: true); + when(CameraPlatform.instance.resumePreview(cameraController.cameraId)) + .thenThrow( + PlatformException( + code: 'TEST_ERROR', + message: 'This is a test error message', + details: null, + ), + ); + + expect( + cameraController.resumePreview(), + throwsA(isA().having( + (error) => error.description, + 'TEST_ERROR', + 'This is a test error message', + ))); + }); + + 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 pausePreview(int? cameraId) async => + super.noSuchMethod(Invocation.method(#pausePreview, [cameraId])); + + @override + Future resumePreview(int? cameraId) async => + super.noSuchMethod(Invocation.method(#resumePreview, [cameraId])); + + @override + Future getMaxZoomLevel(int? cameraId) async => super.noSuchMethod( + Invocation.method(#getMaxZoomLevel, [cameraId]), + returnValue: 1.0, + ); + + @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..4718d8943c34 --- /dev/null +++ b/packages/camera/camera/test/camera_value_test.dart @@ -0,0 +1,150 @@ +// Copyright 2013 The Flutter Authors. All 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, + isPreviewPaused: false, + previewPauseOrientation: DeviceOrientation.portraitUp, + ); + + 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); + expect(cameraValue.isPreviewPaused, false); + expect(cameraValue.previewPauseOrientation, 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); + expect(cameraValue.isPreviewPaused, isFalse); + expect(cameraValue.previewPauseOrientation, 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); + expect(cameraValue.isPreviewPaused, isFalse); + expect(cameraValue.previewPauseOrientation, 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, + isPreviewPaused: true, + previewPauseOrientation: 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, isPreviewPaused: true, previewPausedOrientation: 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..195e142fe10f --- /dev/null +++ b/packages/camera/camera_platform_interface/CHANGELOG.md @@ -0,0 +1,60 @@ +## 2.1.1 + +* Add web-relevant docs to platform interface code. + +## 2.1.0 + +* Introduces interface methods for pausing and resuming the camera preview. + +## 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..ac1c66e4df82 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/events/device_event.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/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 orientation of the device UI changes. +class DeviceOrientationChangedEvent extends DeviceEvent { + /// The new orientation of the device + final DeviceOrientation orientation; + + /// Build a new orientation changed event. + DeviceOrientationChangedEvent(this.orientation); + + /// 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..f932f253f491 --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/method_channel/method_channel_camera.dart @@ -0,0 +1,523 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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 + Future pausePreview(int cameraId) async { + await _channel.invokeMethod( + 'pausePreview', + {'cameraId': cameraId}, + ); + } + + @override + Future resumePreview(int cameraId) async { + await _channel.invokeMethod( + 'resumePreview', + {'cameraId': cameraId}, + ); + } + + @override + Widget buildPreview(int cameraId) { + return Texture(textureId: cameraId); + } + + /// Returns the flash mode as a String. + String _serializeFlashMode(FlashMode flashMode) { + switch (flashMode) { + case FlashMode.off: + return 'off'; + case FlashMode.auto: + return 'auto'; + case FlashMode.always: + return 'always'; + case FlashMode.torch: + return 'torch'; + default: + throw ArgumentError('Unknown FlashMode value'); + } + } + + /// Returns the resolution preset as a String. + String _serializeResolutionPreset(ResolutionPreset resolutionPreset) { + switch (resolutionPreset) { + case ResolutionPreset.max: + return 'max'; + case ResolutionPreset.ultraHigh: + return 'ultraHigh'; + case ResolutionPreset.veryHigh: + return 'veryHigh'; + case ResolutionPreset.high: + return 'high'; + case ResolutionPreset.medium: + return 'medium'; + case ResolutionPreset.low: + return 'low'; + default: + throw ArgumentError('Unknown ResolutionPreset value'); + } + } + + /// Converts messages received from the native platform into device events. + /// + /// 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..aafeef890f1b --- /dev/null +++ b/packages/camera/camera_platform_interface/lib/src/platform_interface/camera_platform.dart @@ -0,0 +1,259 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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. + /// On Web this parameter is currently not supported. + 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. + /// On Web this returns an empty stream. + 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 ui orientation changed. + /// + /// Implementations for this: + /// - Should support all 4 orientations. + 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. + /// On Web [FlashMode.auto] corresponds to [FlashMode.always]. + 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 the minimum and the maximum supported + /// zoom level returned by `getMinZoomLevel` and `getMaxZoomLevel`. Throws a `CameraException` + /// when an illegal zoom level is supplied. + Future setZoomLevel(int cameraId, double zoom) { + throw UnimplementedError('setZoomLevel() is not implemented.'); + } + + /// Pause the active preview on the current frame for the selected camera. + Future pausePreview(int cameraId) { + throw UnimplementedError('pausePreview() is not implemented.'); + } + + /// Resume the paused preview for the selected camera. + Future resumePreview(int cameraId) { + throw UnimplementedError('pausePreview() 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..fcb6b83bbf14 --- /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 and Web + low, + + /// 480p (640x480 on iOS, 720x480 on Android and Web) + medium, + + /// 720p (1280x720) + high, + + /// 1080p (1920x1080) + veryHigh, + + /// 2160p (3840x2160 on Android and iOS, 4096x2160 on Web) + 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..41c6a9705482 --- /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.1.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..750c27200692 --- /dev/null +++ b/packages/camera/camera_platform_interface/test/camera_platform_interface_test.dart @@ -0,0 +1,445 @@ +// Copyright 2013 The Flutter Authors. All 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, + ); + }); + + test( + 'Default implementation of pausePreview() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.pausePreview(1), + throwsUnimplementedError, + ); + }); + + test( + 'Default implementation of resumePreview() should throw unimplemented error', + () { + // Arrange + final cameraPlatform = ExtendsCameraPlatform(); + + // Act & Assert + expect( + () => cameraPlatform.resumePreview(1), + 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..ec71aa173fff --- /dev/null +++ b/packages/camera/camera_platform_interface/test/method_channel/method_channel_camera_test.dart @@ -0,0 +1,960 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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}), + ]); + }); + + test('Should pause the camera preview', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'pausePreview': null}, + ); + + // Act + await camera.pausePreview(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('pausePreview', arguments: {'cameraId': cameraId}), + ]); + }); + + test('Should resume the camera preview', () async { + // Arrange + MethodChannelMock channel = MethodChannelMock( + channelName: 'plugins.flutter.io/camera', + methods: {'resumePreview': null}, + ); + + // Act + await camera.resumePreview(cameraId); + + // Assert + expect(channel.log, [ + isMethodCall('resumePreview', 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/camera_web/AUTHORS b/packages/camera/camera_web/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/camera/camera_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/camera/camera_web/CHANGELOG.md b/packages/camera/camera_web/CHANGELOG.md new file mode 100644 index 000000000000..dd9225f48ff4 --- /dev/null +++ b/packages/camera/camera_web/CHANGELOG.md @@ -0,0 +1,12 @@ +## 0.2.1+1 + +* Update usage documentation. + +## 0.2.1 + +* Add video recording functionality. +* Fix cameraNotReadable error that prevented access to the camera on some Android devices. + +## 0.2.0 + +* Initial release, adapted from the Flutter [I/O Photobooth](https://photobooth.flutter.dev/) project. diff --git a/packages/camera/camera_web/LICENSE b/packages/camera/camera_web/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/camera/camera_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/camera/camera_web/README.md b/packages/camera/camera_web/README.md new file mode 100644 index 000000000000..04bf665c1039 --- /dev/null +++ b/packages/camera/camera_web/README.md @@ -0,0 +1,112 @@ +# Camera Web Plugin + +The web implementation of [`camera`][camera]. + +*Note*: This plugin is under development. See [missing implementation](#missing-implementation). + +## Usage + +### Depend on the package + +This package is [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin), +which means you can simply use `camera` +normally. This package will be automatically included in your app when you do. + +## Example + +Find the example in the [`camera` package](https://pub.dev/packages/camera#example). + +## Limitations on the web platform + +### Camera devices + +The camera devices are accessed with [Stream Web API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Streams_API) +with the following [browser support](https://caniuse.com/stream): + +![Data on support for the Stream feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/stream.png) + +Accessing camera devices requires a [secure browsing context](https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts). +Broadly speaking, this means that you need to serve your web application over HTTPS +(or `localhost` for local development). For insecure contexts +`CameraPlatform.availableCameras` might throw a `CameraException` with the +`permissionDenied` error code. + +### Device orientation + +The device orientation implementation is backed by [`Screen Orientation Web API`](https://www.w3.org/TR/screen-orientation/) +with the following [browser support](https://caniuse.com/screen-orientation): + +![Data on support for the Screen Orientation feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/screen-orientation.png) + +For the browsers that do not support the device orientation: + +- `CameraPlatform.onDeviceOrientationChanged` returns an empty stream. +- `CameraPlatform.lockCaptureOrientation` and `CameraPlatform.unlockCaptureOrientation` +throw a `PlatformException` with the `orientationNotSupported` error code. + +### Flash mode and zoom level + +The flash mode and zoom level implementation is backed by [Image Capture Web API](https://w3c.github.io/mediacapture-image/) +with the following [browser support](https://caniuse.com/mdn-api_imagecapture): + +![Data on support for the Image Capture feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/static/v1/mdn-api__ImageCapture-1628778966589.png) + +For the browsers that do not support the flash mode: + +- `CameraPlatform.setFlashMode` throws a `PlatformException` with the +`torchModeNotSupported` error code. + +For the browsers that do not support the zoom level: + +- `CameraPlatform.getMaxZoomLevel`, `CameraPlatform.getMinZoomLevel` and +`CameraPlatform.setZoomLevel` throw a `PlatformException` with the +`zoomLevelNotSupported` error code. + +### Taking a picture + +The image capturing implementation is backed by [`URL.createObjectUrl` Web API](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL) +with the following [browser support](https://caniuse.com/bloburls): + +![Data on support for the Blob URLs feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/bloburls.png) + +The web platform does not support `dart:io`. Attempts to display a captured image +using `Image.file` will throw an error. The capture image contains a network-accessible +URL pointing to a location within the browser (blob) and can be displayed using +`Image.network` or `Image.memory` after loading the image bytes to memory. + +See the example below: + +```dart +if (kIsWeb) { + Image.network(capturedImage.path); +} else { + Image.file(File(capturedImage.path)); +} +``` + +### Video recording + +The video recording implementation is backed by [MediaRecorder Web API](https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder) with the following [browser support](https://caniuse.com/mdn-api_mediarecorder): + +![Data on support for the MediaRecorder feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/mediarecorder.png). + +A video is recorded in one of the following video MIME types: +- video/webm (e.g. on Chrome or Firefox) +- video/mp4 (e.g. on Safari) + +Pausing, resuming or stopping the video recording throws a `PlatformException` with the `videoRecordingNotStarted` error code if the video recording was not started. + +For the browsers that do not support the video recording: +- `CameraPlatform.startVideoRecording` throws a `PlatformException` with the `notSupported` error code. + +## Missing implementation + +The web implementation of [`camera`][camera] is missing the following features: +- Exposure mode, point and offset +- Focus mode and point +- Sensor orientation +- Image format group +- Streaming of frames + + +[camera]: https://pub.dev/packages/camera diff --git a/packages/camera/camera_web/example/README.md b/packages/camera/camera_web/example/README.md new file mode 100644 index 000000000000..8a6e74b107ea --- /dev/null +++ b/packages/camera/camera_web/example/README.md @@ -0,0 +1,9 @@ +# Testing + +This package uses `package:integration_test` to run its tests in a web browser. + +See [Plugin Tests > Web Tests](https://github.com/flutter/flutter/wiki/Plugin-Tests#web-tests) +in the Flutter wiki for instructions to setup and run the tests in this package. + +Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) +for more info. \ No newline at end of file diff --git a/packages/camera/camera_web/example/integration_test/camera_error_code_test.dart b/packages/camera/camera_web/example/integration_test/camera_error_code_test.dart new file mode 100644 index 000000000000..a298b57dfd7f --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/camera_error_code_test.dart @@ -0,0 +1,178 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:html'; + +import 'package:camera_web/src/types/types.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'helpers/helpers.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraErrorCode', () { + group('toString returns a correct type for', () { + testWidgets('notSupported', (tester) async { + expect( + CameraErrorCode.notSupported.toString(), + equals('cameraNotSupported'), + ); + }); + + testWidgets('notFound', (tester) async { + expect( + CameraErrorCode.notFound.toString(), + equals('cameraNotFound'), + ); + }); + + testWidgets('notReadable', (tester) async { + expect( + CameraErrorCode.notReadable.toString(), + equals('cameraNotReadable'), + ); + }); + + testWidgets('overconstrained', (tester) async { + expect( + CameraErrorCode.overconstrained.toString(), + equals('cameraOverconstrained'), + ); + }); + + testWidgets('permissionDenied', (tester) async { + expect( + CameraErrorCode.permissionDenied.toString(), + equals('cameraPermission'), + ); + }); + + testWidgets('type', (tester) async { + expect( + CameraErrorCode.type.toString(), + equals('cameraType'), + ); + }); + + testWidgets('abort', (tester) async { + expect( + CameraErrorCode.abort.toString(), + equals('cameraAbort'), + ); + }); + + testWidgets('security', (tester) async { + expect( + CameraErrorCode.security.toString(), + equals('cameraSecurity'), + ); + }); + + testWidgets('missingMetadata', (tester) async { + expect( + CameraErrorCode.missingMetadata.toString(), + equals('cameraMissingMetadata'), + ); + }); + + testWidgets('orientationNotSupported', (tester) async { + expect( + CameraErrorCode.orientationNotSupported.toString(), + equals('orientationNotSupported'), + ); + }); + + testWidgets('torchModeNotSupported', (tester) async { + expect( + CameraErrorCode.torchModeNotSupported.toString(), + equals('torchModeNotSupported'), + ); + }); + + testWidgets('zoomLevelNotSupported', (tester) async { + expect( + CameraErrorCode.zoomLevelNotSupported.toString(), + equals('zoomLevelNotSupported'), + ); + }); + + testWidgets('zoomLevelInvalid', (tester) async { + expect( + CameraErrorCode.zoomLevelInvalid.toString(), + equals('zoomLevelInvalid'), + ); + }); + + testWidgets('notStarted', (tester) async { + expect( + CameraErrorCode.notStarted.toString(), + equals('cameraNotStarted'), + ); + }); + + testWidgets('videoRecordingNotStarted', (tester) async { + expect( + CameraErrorCode.videoRecordingNotStarted.toString(), + equals('videoRecordingNotStarted'), + ); + }); + + testWidgets('unknown', (tester) async { + expect( + CameraErrorCode.unknown.toString(), + equals('cameraUnknown'), + ); + }); + + group('fromMediaError', () { + testWidgets('with aborted error code', (tester) async { + expect( + CameraErrorCode.fromMediaError( + FakeMediaError(MediaError.MEDIA_ERR_ABORTED), + ).toString(), + equals('mediaErrorAborted'), + ); + }); + + testWidgets('with network error code', (tester) async { + expect( + CameraErrorCode.fromMediaError( + FakeMediaError(MediaError.MEDIA_ERR_NETWORK), + ).toString(), + equals('mediaErrorNetwork'), + ); + }); + + testWidgets('with decode error code', (tester) async { + expect( + CameraErrorCode.fromMediaError( + FakeMediaError(MediaError.MEDIA_ERR_DECODE), + ).toString(), + equals('mediaErrorDecode'), + ); + }); + + testWidgets('with source not supported error code', (tester) async { + expect( + CameraErrorCode.fromMediaError( + FakeMediaError(MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED), + ).toString(), + equals('mediaErrorSourceNotSupported'), + ); + }); + + testWidgets('with unknown error code', (tester) async { + expect( + CameraErrorCode.fromMediaError( + FakeMediaError(5), + ).toString(), + equals('mediaErrorUnknown'), + ); + }); + }); + }); + }); +} diff --git a/packages/camera/camera_web/example/integration_test/camera_metadata_test.dart b/packages/camera/camera_web/example/integration_test/camera_metadata_test.dart new file mode 100644 index 000000000000..36ecb3e47f31 --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/camera_metadata_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_web/src/types/types.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraMetadata', () { + testWidgets('supports value equality', (tester) async { + expect( + CameraMetadata( + deviceId: 'deviceId', + facingMode: 'environment', + ), + equals( + CameraMetadata( + deviceId: 'deviceId', + facingMode: 'environment', + ), + ), + ); + }); + }); +} diff --git a/packages/camera/camera_web/example/integration_test/camera_options_test.dart b/packages/camera/camera_web/example/integration_test/camera_options_test.dart new file mode 100644 index 000000000000..a74ba3088394 --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/camera_options_test.dart @@ -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 'package:camera_web/src/types/types.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraOptions', () { + testWidgets('serializes correctly', (tester) async { + final cameraOptions = CameraOptions( + audio: AudioConstraints(enabled: true), + video: VideoConstraints( + facingMode: FacingModeConstraint.exact(CameraType.user), + ), + ); + + expect( + cameraOptions.toJson(), + equals({ + 'audio': cameraOptions.audio.toJson(), + 'video': cameraOptions.video.toJson(), + }), + ); + }); + + testWidgets('supports value equality', (tester) async { + expect( + CameraOptions( + audio: AudioConstraints(enabled: false), + video: VideoConstraints( + facingMode: FacingModeConstraint(CameraType.environment), + width: VideoSizeConstraint(minimum: 10, ideal: 15, maximum: 20), + height: VideoSizeConstraint(minimum: 15, ideal: 20, maximum: 25), + deviceId: 'deviceId', + ), + ), + equals( + CameraOptions( + audio: AudioConstraints(enabled: false), + video: VideoConstraints( + facingMode: FacingModeConstraint(CameraType.environment), + width: VideoSizeConstraint(minimum: 10, ideal: 15, maximum: 20), + height: VideoSizeConstraint(minimum: 15, ideal: 20, maximum: 25), + deviceId: 'deviceId', + ), + ), + ), + ); + }); + }); + + group('AudioConstraints', () { + testWidgets('serializes correctly', (tester) async { + expect( + AudioConstraints(enabled: true).toJson(), + equals(true), + ); + }); + + testWidgets('supports value equality', (tester) async { + expect( + AudioConstraints(enabled: true), + equals(AudioConstraints(enabled: true)), + ); + }); + }); + + group('VideoConstraints', () { + testWidgets('serializes correctly', (tester) async { + final videoConstraints = VideoConstraints( + facingMode: FacingModeConstraint.exact(CameraType.user), + width: VideoSizeConstraint(ideal: 100, maximum: 100), + height: VideoSizeConstraint(ideal: 50, maximum: 50), + deviceId: 'deviceId', + ); + + expect( + videoConstraints.toJson(), + equals({ + 'facingMode': videoConstraints.facingMode!.toJson(), + 'width': videoConstraints.width!.toJson(), + 'height': videoConstraints.height!.toJson(), + 'deviceId': { + 'exact': 'deviceId', + } + }), + ); + }); + + testWidgets('supports value equality', (tester) async { + expect( + VideoConstraints( + facingMode: FacingModeConstraint.exact(CameraType.environment), + width: VideoSizeConstraint(minimum: 90, ideal: 100, maximum: 100), + height: VideoSizeConstraint(minimum: 40, ideal: 50, maximum: 50), + deviceId: 'deviceId', + ), + equals( + VideoConstraints( + facingMode: FacingModeConstraint.exact(CameraType.environment), + width: VideoSizeConstraint(minimum: 90, ideal: 100, maximum: 100), + height: VideoSizeConstraint(minimum: 40, ideal: 50, maximum: 50), + deviceId: 'deviceId', + ), + ), + ); + }); + }); + + group('FacingModeConstraint', () { + group('ideal', () { + testWidgets( + 'serializes correctly ' + 'for environment camera type', (tester) async { + expect( + FacingModeConstraint(CameraType.environment).toJson(), + equals({'ideal': 'environment'}), + ); + }); + + testWidgets( + 'serializes correctly ' + 'for user camera type', (tester) async { + expect( + FacingModeConstraint(CameraType.user).toJson(), + equals({'ideal': 'user'}), + ); + }); + + testWidgets('supports value equality', (tester) async { + expect( + FacingModeConstraint(CameraType.user), + equals(FacingModeConstraint(CameraType.user)), + ); + }); + }); + + group('exact', () { + testWidgets( + 'serializes correctly ' + 'for environment camera type', (tester) async { + expect( + FacingModeConstraint.exact(CameraType.environment).toJson(), + equals({'exact': 'environment'}), + ); + }); + + testWidgets( + 'serializes correctly ' + 'for user camera type', (tester) async { + expect( + FacingModeConstraint.exact(CameraType.user).toJson(), + equals({'exact': 'user'}), + ); + }); + + testWidgets('supports value equality', (tester) async { + expect( + FacingModeConstraint.exact(CameraType.environment), + equals(FacingModeConstraint.exact(CameraType.environment)), + ); + }); + }); + }); + + group('VideoSizeConstraint ', () { + testWidgets('serializes correctly', (tester) async { + expect( + VideoSizeConstraint( + minimum: 200, + ideal: 400, + maximum: 400, + ).toJson(), + equals({ + 'min': 200, + 'ideal': 400, + 'max': 400, + }), + ); + }); + + testWidgets('supports value equality', (tester) async { + expect( + VideoSizeConstraint( + minimum: 100, + ideal: 200, + maximum: 300, + ), + equals( + VideoSizeConstraint( + minimum: 100, + ideal: 200, + maximum: 300, + ), + ), + ); + }); + }); +} diff --git a/packages/camera/camera_web/example/integration_test/camera_service_test.dart b/packages/camera/camera_web/example/integration_test/camera_service_test.dart new file mode 100644 index 000000000000..346ab26237ea --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/camera_service_test.dart @@ -0,0 +1,869 @@ +// Copyright 2013 The Flutter Authors. All 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:ui'; +import 'dart:js_util' as js_util; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/src/camera.dart'; +import 'package:camera_web/src/camera_service.dart'; +import 'package:camera_web/src/shims/dart_js_util.dart'; +import 'package:camera_web/src/types/types.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import 'helpers/helpers.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraService', () { + const cameraId = 0; + + late Window window; + late Navigator navigator; + late MediaDevices mediaDevices; + late CameraService cameraService; + late JsUtil jsUtil; + + setUp(() async { + window = MockWindow(); + navigator = MockNavigator(); + mediaDevices = MockMediaDevices(); + jsUtil = MockJsUtil(); + + when(() => window.navigator).thenReturn(navigator); + when(() => navigator.mediaDevices).thenReturn(mediaDevices); + + // Mock JsUtil to return the real getProperty from dart:js_util. + when(() => jsUtil.getProperty(any(), any())).thenAnswer( + (invocation) => js_util.getProperty( + invocation.positionalArguments[0], + invocation.positionalArguments[1], + ), + ); + + cameraService = CameraService()..window = window; + }); + + group('getMediaStreamForOptions', () { + testWidgets( + 'calls MediaDevices.getUserMedia ' + 'with provided options', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenAnswer((_) async => FakeMediaStream([])); + + final options = CameraOptions( + video: VideoConstraints( + facingMode: FacingModeConstraint.exact(CameraType.user), + width: VideoSizeConstraint(ideal: 200), + ), + ); + + await cameraService.getMediaStreamForOptions(options); + + verify( + () => mediaDevices.getUserMedia(options.toJson()), + ).called(1); + }); + + testWidgets( + 'throws PlatformException ' + 'with notSupported error ' + 'when there are no media devices', (tester) async { + when(() => navigator.mediaDevices).thenReturn(null); + + expect( + () => cameraService.getMediaStreamForOptions(CameraOptions()), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notSupported.toString(), + ), + ), + ); + }); + + group('throws CameraWebException', () { + testWidgets( + 'with notFound error ' + 'when MediaDevices.getUserMedia throws DomException ' + 'with NotFoundError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('NotFoundError')); + + expect( + () => cameraService.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having((e) => e.code, 'code', CameraErrorCode.notFound), + ), + ); + }); + + testWidgets( + 'with notFound error ' + 'when MediaDevices.getUserMedia throws DomException ' + 'with DevicesNotFoundError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('DevicesNotFoundError')); + + expect( + () => cameraService.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having((e) => e.code, 'code', CameraErrorCode.notFound), + ), + ); + }); + + testWidgets( + 'with notReadable error ' + 'when MediaDevices.getUserMedia throws DomException ' + 'with NotReadableError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('NotReadableError')); + + expect( + () => cameraService.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having((e) => e.code, 'code', CameraErrorCode.notReadable), + ), + ); + }); + + testWidgets( + 'with notReadable error ' + 'when MediaDevices.getUserMedia throws DomException ' + 'with TrackStartError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('TrackStartError')); + + expect( + () => cameraService.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having((e) => e.code, 'code', CameraErrorCode.notReadable), + ), + ); + }); + + testWidgets( + 'with overconstrained error ' + 'when MediaDevices.getUserMedia throws DomException ' + 'with OverconstrainedError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('OverconstrainedError')); + + expect( + () => cameraService.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having( + (e) => e.code, 'code', CameraErrorCode.overconstrained), + ), + ); + }); + + testWidgets( + 'with overconstrained error ' + 'when MediaDevices.getUserMedia throws DomException ' + 'with ConstraintNotSatisfiedError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('ConstraintNotSatisfiedError')); + + expect( + () => cameraService.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having( + (e) => e.code, 'code', CameraErrorCode.overconstrained), + ), + ); + }); + + testWidgets( + 'with permissionDenied error ' + 'when MediaDevices.getUserMedia throws DomException ' + 'with NotAllowedError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('NotAllowedError')); + + expect( + () => cameraService.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having( + (e) => e.code, 'code', CameraErrorCode.permissionDenied), + ), + ); + }); + + testWidgets( + 'with permissionDenied error ' + 'when MediaDevices.getUserMedia throws DomException ' + 'with PermissionDeniedError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('PermissionDeniedError')); + + expect( + () => cameraService.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having( + (e) => e.code, 'code', CameraErrorCode.permissionDenied), + ), + ); + }); + + testWidgets( + 'with type error ' + 'when MediaDevices.getUserMedia throws DomException ' + 'with TypeError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('TypeError')); + + expect( + () => cameraService.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having((e) => e.code, 'code', CameraErrorCode.type), + ), + ); + }); + + testWidgets( + 'with abort error ' + 'when MediaDevices.getUserMedia throws DomException ' + 'with AbortError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('AbortError')); + + expect( + () => cameraService.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having((e) => e.code, 'code', CameraErrorCode.abort), + ), + ); + }); + + testWidgets( + 'with security error ' + 'when MediaDevices.getUserMedia throws DomException ' + 'with SecurityError', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('SecurityError')); + + expect( + () => cameraService.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having((e) => e.code, 'code', CameraErrorCode.security), + ), + ); + }); + + testWidgets( + 'with unknown error ' + 'when MediaDevices.getUserMedia throws DomException ' + 'with an unknown error', (tester) async { + when(() => mediaDevices.getUserMedia(any())) + .thenThrow(FakeDomException('Unknown')); + + expect( + () => cameraService.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having((e) => e.code, 'code', CameraErrorCode.unknown), + ), + ); + }); + + testWidgets( + 'with unknown error ' + 'when MediaDevices.getUserMedia throws an unknown exception', + (tester) async { + when(() => mediaDevices.getUserMedia(any())).thenThrow(Exception()); + + expect( + () => cameraService.getMediaStreamForOptions( + CameraOptions(), + cameraId: cameraId, + ), + throwsA( + isA() + .having((e) => e.cameraId, 'cameraId', cameraId) + .having((e) => e.code, 'code', CameraErrorCode.unknown), + ), + ); + }); + }); + }); + + group('getZoomLevelCapabilityForCamera', () { + late Camera camera; + late List videoTracks; + + setUp(() { + camera = MockCamera(); + videoTracks = [MockMediaStreamTrack(), MockMediaStreamTrack()]; + + when(() => camera.textureId).thenReturn(0); + when(() => camera.stream).thenReturn(FakeMediaStream(videoTracks)); + + cameraService.jsUtil = jsUtil; + }); + + testWidgets( + 'returns the zoom level capability ' + 'based on the first video track', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'zoom': true, + }); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'zoom': js_util.jsify({ + 'min': 100, + 'max': 400, + 'step': 2, + }), + }); + + final zoomLevelCapability = + cameraService.getZoomLevelCapabilityForCamera(camera); + + expect(zoomLevelCapability.minimum, equals(100.0)); + expect(zoomLevelCapability.maximum, equals(400.0)); + expect(zoomLevelCapability.videoTrack, equals(videoTracks.first)); + }); + + group('throws CameraWebException', () { + testWidgets( + 'with zoomLevelNotSupported error ' + 'when there are no media devices', (tester) async { + when(() => navigator.mediaDevices).thenReturn(null); + + expect( + () => cameraService.getZoomLevelCapabilityForCamera(camera), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + camera.textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.zoomLevelNotSupported, + ), + ), + ); + }); + + testWidgets( + 'with zoomLevelNotSupported error ' + 'when the zoom level is not supported ' + 'in the browser', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'zoom': false, + }); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'zoom': { + 'min': 100, + 'max': 400, + 'step': 2, + }, + }); + + expect( + () => cameraService.getZoomLevelCapabilityForCamera(camera), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + camera.textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.zoomLevelNotSupported, + ), + ), + ); + }); + + testWidgets( + 'with zoomLevelNotSupported error ' + 'when the zoom level is not supported ' + 'by the camera', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'zoom': true, + }); + + when(videoTracks.first.getCapabilities).thenReturn({}); + + expect( + () => cameraService.getZoomLevelCapabilityForCamera(camera), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + camera.textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.zoomLevelNotSupported, + ), + ), + ); + }); + + testWidgets( + 'with notStarted error ' + 'when the camera stream has not been initialized', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'zoom': true, + }); + + // Create a camera stream with no video tracks. + when(() => camera.stream).thenReturn(FakeMediaStream([])); + + expect( + () => cameraService.getZoomLevelCapabilityForCamera(camera), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + camera.textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.notStarted, + ), + ), + ); + }); + }); + }); + + group('getFacingModeForVideoTrack', () { + setUp(() { + cameraService.jsUtil = jsUtil; + }); + + testWidgets( + 'throws PlatformException ' + 'with notSupported error ' + 'when there are no media devices', (tester) async { + when(() => navigator.mediaDevices).thenReturn(null); + + expect( + () => + cameraService.getFacingModeForVideoTrack(MockMediaStreamTrack()), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notSupported.toString(), + ), + ), + ); + }); + + testWidgets( + 'returns null ' + 'when the facing mode is not supported', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'facingMode': false, + }); + + final facingMode = + cameraService.getFacingModeForVideoTrack(MockMediaStreamTrack()); + + expect(facingMode, isNull); + }); + + group('when the facing mode is supported', () { + late MediaStreamTrack videoTrack; + + setUp(() { + videoTrack = MockMediaStreamTrack(); + + when(() => jsUtil.hasProperty(videoTrack, 'getCapabilities')) + .thenReturn(true); + + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'facingMode': true, + }); + }); + + testWidgets( + 'returns an appropriate facing mode ' + 'based on the video track settings', (tester) async { + when(videoTrack.getSettings).thenReturn({'facingMode': 'user'}); + + final facingMode = + cameraService.getFacingModeForVideoTrack(videoTrack); + + expect(facingMode, equals('user')); + }); + + testWidgets( + 'returns an appropriate facing mode ' + 'based on the video track capabilities ' + 'when the facing mode setting is empty', (tester) async { + when(videoTrack.getSettings).thenReturn({}); + when(videoTrack.getCapabilities).thenReturn({ + 'facingMode': ['environment', 'left'] + }); + + when(() => jsUtil.hasProperty(videoTrack, 'getCapabilities')) + .thenReturn(true); + + final facingMode = + cameraService.getFacingModeForVideoTrack(videoTrack); + + expect(facingMode, equals('environment')); + }); + + testWidgets( + 'returns null ' + 'when the facing mode setting ' + 'and capabilities are empty', (tester) async { + when(videoTrack.getSettings).thenReturn({}); + when(videoTrack.getCapabilities).thenReturn({'facingMode': []}); + + final facingMode = + cameraService.getFacingModeForVideoTrack(videoTrack); + + expect(facingMode, isNull); + }); + + testWidgets( + 'returns null ' + 'when the facing mode setting is empty and ' + 'the video track capabilities are not supported', (tester) async { + when(videoTrack.getSettings).thenReturn({}); + + when(() => jsUtil.hasProperty(videoTrack, 'getCapabilities')) + .thenReturn(false); + + final facingMode = + cameraService.getFacingModeForVideoTrack(videoTrack); + + expect(facingMode, isNull); + }); + }); + }); + + group('mapFacingModeToLensDirection', () { + testWidgets( + 'returns front ' + 'when the facing mode is user', (tester) async { + expect( + cameraService.mapFacingModeToLensDirection('user'), + equals(CameraLensDirection.front), + ); + }); + + testWidgets( + 'returns back ' + 'when the facing mode is environment', (tester) async { + expect( + cameraService.mapFacingModeToLensDirection('environment'), + equals(CameraLensDirection.back), + ); + }); + + testWidgets( + 'returns external ' + 'when the facing mode is left', (tester) async { + expect( + cameraService.mapFacingModeToLensDirection('left'), + equals(CameraLensDirection.external), + ); + }); + + testWidgets( + 'returns external ' + 'when the facing mode is right', (tester) async { + expect( + cameraService.mapFacingModeToLensDirection('right'), + equals(CameraLensDirection.external), + ); + }); + }); + + group('mapFacingModeToCameraType', () { + testWidgets( + 'returns user ' + 'when the facing mode is user', (tester) async { + expect( + cameraService.mapFacingModeToCameraType('user'), + equals(CameraType.user), + ); + }); + + testWidgets( + 'returns environment ' + 'when the facing mode is environment', (tester) async { + expect( + cameraService.mapFacingModeToCameraType('environment'), + equals(CameraType.environment), + ); + }); + + testWidgets( + 'returns user ' + 'when the facing mode is left', (tester) async { + expect( + cameraService.mapFacingModeToCameraType('left'), + equals(CameraType.user), + ); + }); + + testWidgets( + 'returns user ' + 'when the facing mode is right', (tester) async { + expect( + cameraService.mapFacingModeToCameraType('right'), + equals(CameraType.user), + ); + }); + }); + + group('mapResolutionPresetToSize', () { + testWidgets( + 'returns 4096x2160 ' + 'when the resolution preset is max', (tester) async { + expect( + cameraService.mapResolutionPresetToSize(ResolutionPreset.max), + equals(Size(4096, 2160)), + ); + }); + + testWidgets( + 'returns 4096x2160 ' + 'when the resolution preset is ultraHigh', (tester) async { + expect( + cameraService.mapResolutionPresetToSize(ResolutionPreset.ultraHigh), + equals(Size(4096, 2160)), + ); + }); + + testWidgets( + 'returns 1920x1080 ' + 'when the resolution preset is veryHigh', (tester) async { + expect( + cameraService.mapResolutionPresetToSize(ResolutionPreset.veryHigh), + equals(Size(1920, 1080)), + ); + }); + + testWidgets( + 'returns 1280x720 ' + 'when the resolution preset is high', (tester) async { + expect( + cameraService.mapResolutionPresetToSize(ResolutionPreset.high), + equals(Size(1280, 720)), + ); + }); + + testWidgets( + 'returns 720x480 ' + 'when the resolution preset is medium', (tester) async { + expect( + cameraService.mapResolutionPresetToSize(ResolutionPreset.medium), + equals(Size(720, 480)), + ); + }); + + testWidgets( + 'returns 320x240 ' + 'when the resolution preset is low', (tester) async { + expect( + cameraService.mapResolutionPresetToSize(ResolutionPreset.low), + equals(Size(320, 240)), + ); + }); + }); + + group('mapDeviceOrientationToOrientationType', () { + testWidgets( + 'returns portraitPrimary ' + 'when the device orientation is portraitUp', (tester) async { + expect( + cameraService.mapDeviceOrientationToOrientationType( + DeviceOrientation.portraitUp, + ), + equals(OrientationType.portraitPrimary), + ); + }); + + testWidgets( + 'returns landscapePrimary ' + 'when the device orientation is landscapeLeft', (tester) async { + expect( + cameraService.mapDeviceOrientationToOrientationType( + DeviceOrientation.landscapeLeft, + ), + equals(OrientationType.landscapePrimary), + ); + }); + + testWidgets( + 'returns portraitSecondary ' + 'when the device orientation is portraitDown', (tester) async { + expect( + cameraService.mapDeviceOrientationToOrientationType( + DeviceOrientation.portraitDown, + ), + equals(OrientationType.portraitSecondary), + ); + }); + + testWidgets( + 'returns landscapeSecondary ' + 'when the device orientation is landscapeRight', (tester) async { + expect( + cameraService.mapDeviceOrientationToOrientationType( + DeviceOrientation.landscapeRight, + ), + equals(OrientationType.landscapeSecondary), + ); + }); + }); + + group('mapOrientationTypeToDeviceOrientation', () { + testWidgets( + 'returns portraitUp ' + 'when the orientation type is portraitPrimary', (tester) async { + expect( + cameraService.mapOrientationTypeToDeviceOrientation( + OrientationType.portraitPrimary, + ), + equals(DeviceOrientation.portraitUp), + ); + }); + + testWidgets( + 'returns landscapeLeft ' + 'when the orientation type is landscapePrimary', (tester) async { + expect( + cameraService.mapOrientationTypeToDeviceOrientation( + OrientationType.landscapePrimary, + ), + equals(DeviceOrientation.landscapeLeft), + ); + }); + + testWidgets( + 'returns portraitDown ' + 'when the orientation type is portraitSecondary', (tester) async { + expect( + cameraService.mapOrientationTypeToDeviceOrientation( + OrientationType.portraitSecondary, + ), + equals(DeviceOrientation.portraitDown), + ); + }); + + testWidgets( + 'returns portraitDown ' + 'when the orientation type is portraitSecondary', (tester) async { + expect( + cameraService.mapOrientationTypeToDeviceOrientation( + OrientationType.portraitSecondary, + ), + equals(DeviceOrientation.portraitDown), + ); + }); + + testWidgets( + 'returns landscapeRight ' + 'when the orientation type is landscapeSecondary', (tester) async { + expect( + cameraService.mapOrientationTypeToDeviceOrientation( + OrientationType.landscapeSecondary, + ), + equals(DeviceOrientation.landscapeRight), + ); + }); + + testWidgets( + 'returns portraitUp ' + 'for an unknown orientation type', (tester) async { + expect( + cameraService.mapOrientationTypeToDeviceOrientation( + 'unknown', + ), + equals(DeviceOrientation.portraitUp), + ); + }); + }); + }); +} + +class JSNoSuchMethodError implements Exception {} diff --git a/packages/camera/camera_web/example/integration_test/camera_test.dart b/packages/camera/camera_web/example/integration_test/camera_test.dart new file mode 100644 index 000000000000..3a25e33c5398 --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/camera_test.dart @@ -0,0 +1,1678 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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'; + +import 'package:async/async.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/src/camera.dart'; +import 'package:camera_web/src/camera_service.dart'; +import 'package:camera_web/src/types/types.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import 'helpers/helpers.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('Camera', () { + const textureId = 1; + + late Window window; + late Navigator navigator; + late MediaDevices mediaDevices; + + late MediaStream mediaStream; + late CameraService cameraService; + + setUp(() { + window = MockWindow(); + navigator = MockNavigator(); + mediaDevices = MockMediaDevices(); + + when(() => window.navigator).thenReturn(navigator); + when(() => navigator.mediaDevices).thenReturn(mediaDevices); + + cameraService = MockCameraService(); + + final videoElement = getVideoElementWithBlankStream(Size(10, 10)); + mediaStream = videoElement.captureStream(); + + when( + () => cameraService.getMediaStreamForOptions( + any(), + cameraId: any(named: 'cameraId'), + ), + ).thenAnswer((_) => Future.value(mediaStream)); + }); + + setUpAll(() { + registerFallbackValue(MockCameraOptions()); + }); + + group('initialize', () { + testWidgets( + 'calls CameraService.getMediaStreamForOptions ' + 'with provided options', (tester) async { + final options = CameraOptions( + video: VideoConstraints( + facingMode: FacingModeConstraint.exact(CameraType.user), + width: VideoSizeConstraint(ideal: 200), + ), + ); + + final camera = Camera( + textureId: textureId, + options: options, + cameraService: cameraService, + ); + + await camera.initialize(); + + verify( + () => cameraService.getMediaStreamForOptions( + options, + cameraId: textureId, + ), + ).called(1); + }); + + testWidgets( + 'creates a video element ' + 'with correct properties', (tester) async { + const audioConstraints = AudioConstraints(enabled: true); + final videoConstraints = VideoConstraints( + facingMode: FacingModeConstraint( + CameraType.user, + ), + ); + + final camera = Camera( + textureId: textureId, + options: CameraOptions( + audio: audioConstraints, + video: videoConstraints, + ), + cameraService: cameraService, + ); + + await camera.initialize(); + + expect(camera.videoElement, isNotNull); + expect(camera.videoElement.autoplay, isFalse); + expect(camera.videoElement.muted, isTrue); + expect(camera.videoElement.srcObject, mediaStream); + expect(camera.videoElement.attributes.keys, contains('playsinline')); + + expect( + camera.videoElement.style.transformOrigin, equals('center center')); + expect(camera.videoElement.style.pointerEvents, equals('none')); + expect(camera.videoElement.style.width, equals('100%')); + expect(camera.videoElement.style.height, equals('100%')); + expect(camera.videoElement.style.objectFit, equals('cover')); + }); + + testWidgets( + 'flips the video element horizontally ' + 'for a back camera', (tester) async { + final videoConstraints = VideoConstraints( + facingMode: FacingModeConstraint( + CameraType.environment, + ), + ); + + final camera = Camera( + textureId: textureId, + options: CameraOptions( + video: videoConstraints, + ), + cameraService: cameraService, + ); + + await camera.initialize(); + + expect(camera.videoElement.style.transform, equals('scaleX(-1)')); + }); + + testWidgets( + 'creates a wrapping div element ' + 'with correct properties', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + await camera.initialize(); + + expect(camera.divElement, isNotNull); + expect(camera.divElement.style.objectFit, equals('cover')); + expect(camera.divElement.children, contains(camera.videoElement)); + }); + + testWidgets('initializes the camera stream', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + await camera.initialize(); + + expect(camera.stream, mediaStream); + }); + + testWidgets( + 'throws an exception ' + 'when CameraService.getMediaStreamForOptions throws', (tester) async { + final exception = Exception('A media stream exception occured.'); + + when(() => cameraService.getMediaStreamForOptions(any(), + cameraId: any(named: 'cameraId'))).thenThrow(exception); + + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + expect( + camera.initialize, + throwsA(exception), + ); + }); + }); + + group('play', () { + testWidgets('starts playing the video element', (tester) async { + var startedPlaying = false; + + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + await camera.initialize(); + + final cameraPlaySubscription = + camera.videoElement.onPlay.listen((event) => startedPlaying = true); + + await camera.play(); + + expect(startedPlaying, isTrue); + + await cameraPlaySubscription.cancel(); + }); + + testWidgets( + 'initializes the camera stream ' + 'from CameraService.getMediaStreamForOptions ' + 'if it does not exist', (tester) async { + final options = CameraOptions( + video: VideoConstraints( + width: VideoSizeConstraint(ideal: 100), + ), + ); + + final camera = Camera( + textureId: textureId, + options: options, + cameraService: cameraService, + ); + + await camera.initialize(); + + /// Remove the video element's source + /// by stopping the camera. + camera.stop(); + + await camera.play(); + + // Should be called twice: for initialize and play. + verify( + () => cameraService.getMediaStreamForOptions( + options, + cameraId: textureId, + ), + ).called(2); + + expect(camera.videoElement.srcObject, mediaStream); + expect(camera.stream, mediaStream); + }); + }); + + group('pause', () { + testWidgets('pauses the camera stream', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + await camera.initialize(); + await camera.play(); + + expect(camera.videoElement.paused, isFalse); + + camera.pause(); + + expect(camera.videoElement.paused, isTrue); + }); + }); + + group('stop', () { + testWidgets('resets the camera stream', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + await camera.initialize(); + await camera.play(); + + camera.stop(); + + expect(camera.videoElement.srcObject, isNull); + expect(camera.stream, isNull); + }); + }); + + group('takePicture', () { + testWidgets('returns a captured picture', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + await camera.initialize(); + await camera.play(); + + final pictureFile = await camera.takePicture(); + + expect(pictureFile, isNotNull); + }); + + group( + 'enables the torch mode ' + 'when taking a picture', () { + late List videoTracks; + late MediaStream videoStream; + late VideoElement videoElement; + + setUp(() { + videoTracks = [MockMediaStreamTrack(), MockMediaStreamTrack()]; + videoStream = FakeMediaStream(videoTracks); + + videoElement = getVideoElementWithBlankStream(Size(100, 100)) + ..muted = true; + + when(() => videoTracks.first.applyConstraints(any())) + .thenAnswer((_) async => {}); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'torch': true, + }); + }); + + testWidgets('if the flash mode is auto', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ) + ..window = window + ..stream = videoStream + ..videoElement = videoElement + ..flashMode = FlashMode.auto; + + await camera.play(); + + final _ = await camera.takePicture(); + + verify( + () => videoTracks.first.applyConstraints({ + "advanced": [ + { + "torch": true, + } + ] + }), + ).called(1); + + verify( + () => videoTracks.first.applyConstraints({ + "advanced": [ + { + "torch": false, + } + ] + }), + ).called(1); + }); + + testWidgets('if the flash mode is always', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ) + ..window = window + ..stream = videoStream + ..videoElement = videoElement + ..flashMode = FlashMode.always; + + await camera.play(); + + final _ = await camera.takePicture(); + + verify( + () => videoTracks.first.applyConstraints({ + "advanced": [ + { + "torch": true, + } + ] + }), + ).called(1); + + verify( + () => videoTracks.first.applyConstraints({ + "advanced": [ + { + "torch": false, + } + ] + }), + ).called(1); + }); + }); + }); + + group('getVideoSize', () { + testWidgets( + 'returns a size ' + 'based on the first video track settings', (tester) async { + const videoSize = Size(1280, 720); + + final videoElement = getVideoElementWithBlankStream(videoSize); + mediaStream = videoElement.captureStream(); + + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + await camera.initialize(); + + expect( + camera.getVideoSize(), + equals(videoSize), + ); + }); + + testWidgets( + 'returns Size.zero ' + 'if the camera is missing video tracks', (tester) async { + // Create a video stream with no video tracks. + final videoElement = VideoElement(); + mediaStream = videoElement.captureStream(); + + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + await camera.initialize(); + + expect( + camera.getVideoSize(), + equals(Size.zero), + ); + }); + }); + + group('setFlashMode', () { + late List videoTracks; + late MediaStream videoStream; + + setUp(() { + videoTracks = [MockMediaStreamTrack(), MockMediaStreamTrack()]; + videoStream = FakeMediaStream(videoTracks); + + when(() => videoTracks.first.applyConstraints(any())) + .thenAnswer((_) async => {}); + + when(videoTracks.first.getCapabilities).thenReturn({}); + }); + + testWidgets('sets the camera flash mode', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'torch': true, + }); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'torch': true, + }); + + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ) + ..window = window + ..stream = videoStream; + + const flashMode = FlashMode.always; + + camera.setFlashMode(flashMode); + + expect( + camera.flashMode, + equals(flashMode), + ); + }); + + testWidgets( + 'enables the torch mode ' + 'if the flash mode is torch', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'torch': true, + }); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'torch': true, + }); + + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ) + ..window = window + ..stream = videoStream; + + camera.setFlashMode(FlashMode.torch); + + verify( + () => videoTracks.first.applyConstraints({ + "advanced": [ + { + "torch": true, + } + ] + }), + ).called(1); + }); + + testWidgets( + 'disables the torch mode ' + 'if the flash mode is not torch', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'torch': true, + }); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'torch': true, + }); + + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ) + ..window = window + ..stream = videoStream; + + camera.setFlashMode(FlashMode.auto); + + verify( + () => videoTracks.first.applyConstraints({ + "advanced": [ + { + "torch": false, + } + ] + }), + ).called(1); + }); + + group('throws a CameraWebException', () { + testWidgets( + 'with torchModeNotSupported error ' + 'when there are no media devices', (tester) async { + when(() => navigator.mediaDevices).thenReturn(null); + + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ) + ..window = window + ..stream = videoStream; + + expect( + () => camera.setFlashMode(FlashMode.always), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.torchModeNotSupported, + ), + ), + ); + }); + + testWidgets( + 'with torchModeNotSupported error ' + 'when the torch mode is not supported ' + 'in the browser', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'torch': false, + }); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'torch': true, + }); + + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ) + ..window = window + ..stream = videoStream; + + expect( + () => camera.setFlashMode(FlashMode.always), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.torchModeNotSupported, + ), + ), + ); + }); + + testWidgets( + 'with torchModeNotSupported error ' + 'when the torch mode is not supported ' + 'by the camera', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'torch': true, + }); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'torch': false, + }); + + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ) + ..window = window + ..stream = videoStream; + + expect( + () => camera.setFlashMode(FlashMode.always), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.torchModeNotSupported, + ), + ), + ); + }); + + testWidgets( + 'with notStarted error ' + 'when the camera stream has not been initialized', (tester) async { + when(mediaDevices.getSupportedConstraints).thenReturn({ + 'torch': true, + }); + + when(videoTracks.first.getCapabilities).thenReturn({ + 'torch': true, + }); + + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + )..window = window; + + expect( + () => camera.setFlashMode(FlashMode.always), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.notStarted, + ), + ), + ); + }); + }); + }); + + group('zoomLevel', () { + group('getMaxZoomLevel', () { + testWidgets( + 'returns maximum ' + 'from CameraService.getZoomLevelCapabilityForCamera', + (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + final zoomLevelCapability = ZoomLevelCapability( + minimum: 50.0, + maximum: 100.0, + videoTrack: MockMediaStreamTrack(), + ); + + when(() => cameraService.getZoomLevelCapabilityForCamera(camera)) + .thenReturn(zoomLevelCapability); + + final maximumZoomLevel = camera.getMaxZoomLevel(); + + verify(() => cameraService.getZoomLevelCapabilityForCamera(camera)) + .called(1); + + expect( + maximumZoomLevel, + equals(zoomLevelCapability.maximum), + ); + }); + }); + + group('getMinZoomLevel', () { + testWidgets( + 'returns minimum ' + 'from CameraService.getZoomLevelCapabilityForCamera', + (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + final zoomLevelCapability = ZoomLevelCapability( + minimum: 50.0, + maximum: 100.0, + videoTrack: MockMediaStreamTrack(), + ); + + when(() => cameraService.getZoomLevelCapabilityForCamera(camera)) + .thenReturn(zoomLevelCapability); + + final minimumZoomLevel = camera.getMinZoomLevel(); + + verify(() => cameraService.getZoomLevelCapabilityForCamera(camera)) + .called(1); + + expect( + minimumZoomLevel, + equals(zoomLevelCapability.minimum), + ); + }); + }); + + group('setZoomLevel', () { + testWidgets( + 'applies zoom on the video track ' + 'from CameraService.getZoomLevelCapabilityForCamera', + (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + final videoTrack = MockMediaStreamTrack(); + + final zoomLevelCapability = ZoomLevelCapability( + minimum: 50.0, + maximum: 100.0, + videoTrack: videoTrack, + ); + + when(() => videoTrack.applyConstraints(any())) + .thenAnswer((_) async {}); + + when(() => cameraService.getZoomLevelCapabilityForCamera(camera)) + .thenReturn(zoomLevelCapability); + + const zoom = 75.0; + + camera.setZoomLevel(zoom); + + verify( + () => videoTrack.applyConstraints({ + "advanced": [ + { + ZoomLevelCapability.constraintName: zoom, + } + ] + }), + ).called(1); + }); + + group('throws a CameraWebException', () { + testWidgets( + 'with zoomLevelInvalid error ' + 'when the provided zoom level is below minimum', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + final zoomLevelCapability = ZoomLevelCapability( + minimum: 50.0, + maximum: 100.0, + videoTrack: MockMediaStreamTrack(), + ); + + when(() => cameraService.getZoomLevelCapabilityForCamera(camera)) + .thenReturn(zoomLevelCapability); + + expect( + () => camera.setZoomLevel(45.0), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.zoomLevelInvalid, + ), + )); + }); + + testWidgets( + 'with zoomLevelInvalid error ' + 'when the provided zoom level is below minimum', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + final zoomLevelCapability = ZoomLevelCapability( + minimum: 50.0, + maximum: 100.0, + videoTrack: MockMediaStreamTrack(), + ); + + when(() => cameraService.getZoomLevelCapabilityForCamera(camera)) + .thenReturn(zoomLevelCapability); + + expect( + () => camera.setZoomLevel(105.0), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.zoomLevelInvalid, + ), + ), + ); + }); + }); + }); + }); + + group('getLensDirection', () { + testWidgets( + 'returns a lens direction ' + 'based on the first video track settings', (tester) async { + final videoElement = MockVideoElement(); + + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + )..videoElement = videoElement; + + final firstVideoTrack = MockMediaStreamTrack(); + + when(() => videoElement.srcObject).thenReturn( + FakeMediaStream([ + firstVideoTrack, + MockMediaStreamTrack(), + ]), + ); + + when(firstVideoTrack.getSettings) + .thenReturn({'facingMode': 'environment'}); + + when(() => cameraService.mapFacingModeToLensDirection('environment')) + .thenReturn(CameraLensDirection.external); + + expect( + camera.getLensDirection(), + equals(CameraLensDirection.external), + ); + }); + + testWidgets( + 'returns null ' + 'if the first video track is missing the facing mode', + (tester) async { + final videoElement = MockVideoElement(); + + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + )..videoElement = videoElement; + + final firstVideoTrack = MockMediaStreamTrack(); + + when(() => videoElement.srcObject).thenReturn( + FakeMediaStream([ + firstVideoTrack, + MockMediaStreamTrack(), + ]), + ); + + when(firstVideoTrack.getSettings).thenReturn({}); + + expect( + camera.getLensDirection(), + isNull, + ); + }); + + testWidgets( + 'returns null ' + 'if the camera is missing video tracks', (tester) async { + // Create a video stream with no video tracks. + final videoElement = VideoElement(); + mediaStream = videoElement.captureStream(); + + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + await camera.initialize(); + + expect( + camera.getLensDirection(), + isNull, + ); + }); + }); + + group('getViewType', () { + testWidgets('returns a correct view type', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + await camera.initialize(); + + expect( + camera.getViewType(), + equals('plugins.flutter.io/camera_$textureId'), + ); + }); + }); + + group('video recording', () { + const supportedVideoType = 'video/webm'; + + late MediaRecorder mediaRecorder; + + bool isVideoTypeSupported(String type) => type == supportedVideoType; + + setUp(() { + mediaRecorder = MockMediaRecorder(); + + when(() => mediaRecorder.onError) + .thenAnswer((_) => const Stream.empty()); + }); + + group('startVideoRecording', () { + testWidgets( + 'creates a media recorder ' + 'with appropriate options', (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + )..isVideoTypeSupported = isVideoTypeSupported; + + await camera.initialize(); + await camera.play(); + + await camera.startVideoRecording(); + + expect( + camera.mediaRecorder!.stream, + equals(camera.stream), + ); + + expect( + camera.mediaRecorder!.mimeType, + equals(supportedVideoType), + ); + + expect( + camera.mediaRecorder!.state, + equals('recording'), + ); + }); + + testWidgets('listens to the media recorder data events', + (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ) + ..mediaRecorder = mediaRecorder + ..isVideoTypeSupported = isVideoTypeSupported; + + await camera.initialize(); + await camera.play(); + + await camera.startVideoRecording(); + + verify( + () => mediaRecorder.addEventListener('dataavailable', any()), + ).called(1); + }); + + testWidgets('listens to the media recorder stop events', + (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ) + ..mediaRecorder = mediaRecorder + ..isVideoTypeSupported = isVideoTypeSupported; + + await camera.initialize(); + await camera.play(); + + await camera.startVideoRecording(); + + verify( + () => mediaRecorder.addEventListener('stop', any()), + ).called(1); + }); + + testWidgets('starts a video recording', (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ) + ..mediaRecorder = mediaRecorder + ..isVideoTypeSupported = isVideoTypeSupported; + + await camera.initialize(); + await camera.play(); + + await camera.startVideoRecording(); + + verify(mediaRecorder.start).called(1); + }); + + testWidgets( + 'starts a video recording ' + 'with maxVideoDuration', (tester) async { + const maxVideoDuration = Duration(hours: 1); + + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ) + ..mediaRecorder = mediaRecorder + ..isVideoTypeSupported = isVideoTypeSupported; + + await camera.initialize(); + await camera.play(); + + await camera.startVideoRecording(maxVideoDuration: maxVideoDuration); + + verify(() => mediaRecorder.start(maxVideoDuration.inMilliseconds)) + .called(1); + }); + + group('throws a CameraWebException', () { + testWidgets( + 'with notSupported error ' + 'when maxVideoDuration is 0 milliseconds or less', + (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ) + ..mediaRecorder = mediaRecorder + ..isVideoTypeSupported = isVideoTypeSupported; + + await camera.initialize(); + await camera.play(); + + expect( + () => camera.startVideoRecording(maxVideoDuration: Duration.zero), + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.notSupported, + ), + ), + ); + }); + + testWidgets( + 'with notSupported error ' + 'when no video types are supported', (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + )..isVideoTypeSupported = (type) => false; + + await camera.initialize(); + await camera.play(); + + expect( + camera.startVideoRecording, + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.notSupported, + ), + ), + ); + }); + }); + }); + + group('pauseVideoRecording', () { + testWidgets('pauses a video recording', (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + )..mediaRecorder = mediaRecorder; + + await camera.pauseVideoRecording(); + + verify(mediaRecorder.pause).called(1); + }); + + testWidgets( + 'throws a CameraWebException ' + 'with videoRecordingNotStarted error ' + 'if the video recording was not started', (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ); + + expect( + camera.pauseVideoRecording, + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.videoRecordingNotStarted, + ), + ), + ); + }); + }); + + group('resumeVideoRecording', () { + testWidgets('resumes a video recording', (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + )..mediaRecorder = mediaRecorder; + + await camera.resumeVideoRecording(); + + verify(mediaRecorder.resume).called(1); + }); + + testWidgets( + 'throws a CameraWebException ' + 'with videoRecordingNotStarted error ' + 'if the video recording was not started', (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ); + + expect( + camera.resumeVideoRecording, + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.videoRecordingNotStarted, + ), + ), + ); + }); + }); + + group('stopVideoRecording', () { + testWidgets( + 'stops a video recording and ' + 'returns the captured file ' + 'based on all video data parts', (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ) + ..mediaRecorder = mediaRecorder + ..isVideoTypeSupported = isVideoTypeSupported; + + await camera.initialize(); + await camera.play(); + + late void Function(Event) videoDataAvailableListener; + late void Function(Event) videoRecordingStoppedListener; + + when( + () => mediaRecorder.addEventListener('dataavailable', any()), + ).thenAnswer((invocation) { + videoDataAvailableListener = invocation.positionalArguments[1]; + }); + + when( + () => mediaRecorder.addEventListener('stop', any()), + ).thenAnswer((invocation) { + videoRecordingStoppedListener = invocation.positionalArguments[1]; + }); + + Blob? finalVideo; + List? videoParts; + camera.blobBuilder = (blobs, videoType) { + videoParts = [...blobs]; + finalVideo = Blob(blobs, videoType); + return finalVideo!; + }; + + await camera.startVideoRecording(); + final videoFileFuture = camera.stopVideoRecording(); + + final capturedVideoPartOne = Blob([]); + final capturedVideoPartTwo = Blob([]); + + final capturedVideoParts = [ + capturedVideoPartOne, + capturedVideoPartTwo, + ]; + + videoDataAvailableListener + ..call(FakeBlobEvent(capturedVideoPartOne)) + ..call(FakeBlobEvent(capturedVideoPartTwo)); + + videoRecordingStoppedListener.call(Event('stop')); + + final videoFile = await videoFileFuture; + + verify(mediaRecorder.stop).called(1); + + expect( + videoFile, + isNotNull, + ); + + expect( + videoFile.mimeType, + equals(supportedVideoType), + ); + + expect( + videoFile.name, + equals(finalVideo.hashCode.toString()), + ); + + expect( + videoParts, + equals(capturedVideoParts), + ); + }); + + testWidgets( + 'throws a CameraWebException ' + 'with videoRecordingNotStarted error ' + 'if the video recording was not started', (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ); + + expect( + camera.stopVideoRecording, + throwsA( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.code, + 'code', + CameraErrorCode.videoRecordingNotStarted, + ), + ), + ); + }); + }); + + group('on video data available', () { + late void Function(Event) videoDataAvailableListener; + + setUp(() { + when( + () => mediaRecorder.addEventListener('dataavailable', any()), + ).thenAnswer((invocation) { + videoDataAvailableListener = invocation.positionalArguments[1]; + }); + }); + + testWidgets( + 'stops a video recording ' + 'if maxVideoDuration is given and ' + 'the recording was not stopped manually', (tester) async { + const maxVideoDuration = Duration(hours: 1); + + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ) + ..mediaRecorder = mediaRecorder + ..isVideoTypeSupported = isVideoTypeSupported; + + await camera.initialize(); + await camera.play(); + await camera.startVideoRecording(maxVideoDuration: maxVideoDuration); + + when(() => mediaRecorder.state).thenReturn('recording'); + + videoDataAvailableListener.call(FakeBlobEvent(Blob([]))); + + await Future.microtask(() {}); + + verify(mediaRecorder.stop).called(1); + }); + }); + + group('on video recording stopped', () { + late void Function(Event) videoRecordingStoppedListener; + + setUp(() { + when( + () => mediaRecorder.addEventListener('stop', any()), + ).thenAnswer((invocation) { + videoRecordingStoppedListener = invocation.positionalArguments[1]; + }); + }); + + testWidgets('stops listening to the media recorder data events', + (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ) + ..mediaRecorder = mediaRecorder + ..isVideoTypeSupported = isVideoTypeSupported; + + await camera.initialize(); + await camera.play(); + + await camera.startVideoRecording(); + + videoRecordingStoppedListener.call(Event('stop')); + + await Future.microtask(() {}); + + verify( + () => mediaRecorder.removeEventListener('dataavailable', any()), + ).called(1); + }); + + testWidgets('stops listening to the media recorder stop events', + (tester) async { + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ) + ..mediaRecorder = mediaRecorder + ..isVideoTypeSupported = isVideoTypeSupported; + + await camera.initialize(); + await camera.play(); + + await camera.startVideoRecording(); + + videoRecordingStoppedListener.call(Event('stop')); + + await Future.microtask(() {}); + + verify( + () => mediaRecorder.removeEventListener('stop', any()), + ).called(1); + }); + + testWidgets('stops listening to the media recorder errors', + (tester) async { + final onErrorStreamController = StreamController(); + + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ) + ..mediaRecorder = mediaRecorder + ..isVideoTypeSupported = isVideoTypeSupported; + + when(() => mediaRecorder.onError) + .thenAnswer((_) => onErrorStreamController.stream); + + await camera.initialize(); + await camera.play(); + + await camera.startVideoRecording(); + + videoRecordingStoppedListener.call(Event('stop')); + + await Future.microtask(() {}); + + expect( + onErrorStreamController.hasListener, + isFalse, + ); + }); + }); + }); + + group('dispose', () { + testWidgets('resets the video element\'s source', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + await camera.initialize(); + await camera.dispose(); + + expect(camera.videoElement.srcObject, isNull); + }); + + testWidgets('closes the onEnded stream', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + await camera.initialize(); + await camera.dispose(); + + expect( + camera.onEndedController.isClosed, + isTrue, + ); + }); + + testWidgets('closes the onVideoRecordedEvent stream', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + await camera.initialize(); + await camera.dispose(); + + expect( + camera.videoRecorderController.isClosed, + isTrue, + ); + }); + + testWidgets('closes the onVideoRecordingError stream', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + await camera.initialize(); + await camera.dispose(); + + expect( + camera.videoRecordingErrorController.isClosed, + isTrue, + ); + }); + }); + + group('events', () { + group('onVideoRecordedEvent', () { + testWidgets( + 'emits a VideoRecordedEvent ' + 'when a video recording is created', (tester) async { + const maxVideoDuration = Duration(hours: 1); + const supportedVideoType = 'video/webm'; + + final mediaRecorder = MockMediaRecorder(); + when(() => mediaRecorder.onError) + .thenAnswer((_) => const Stream.empty()); + + final camera = Camera( + textureId: 1, + cameraService: cameraService, + ) + ..mediaRecorder = mediaRecorder + ..isVideoTypeSupported = (type) => type == 'video/webm'; + + await camera.initialize(); + await camera.play(); + + late void Function(Event) videoDataAvailableListener; + late void Function(Event) videoRecordingStoppedListener; + + when( + () => mediaRecorder.addEventListener('dataavailable', any()), + ).thenAnswer((invocation) { + videoDataAvailableListener = invocation.positionalArguments[1]; + }); + + when( + () => mediaRecorder.addEventListener('stop', any()), + ).thenAnswer((invocation) { + videoRecordingStoppedListener = invocation.positionalArguments[1]; + }); + + final streamQueue = StreamQueue(camera.onVideoRecordedEvent); + + await camera.startVideoRecording(maxVideoDuration: maxVideoDuration); + + Blob? finalVideo; + camera.blobBuilder = (blobs, videoType) { + finalVideo = Blob(blobs, videoType); + return finalVideo!; + }; + + videoDataAvailableListener.call(FakeBlobEvent(Blob([]))); + videoRecordingStoppedListener.call(Event('stop')); + + expect( + await streamQueue.next, + equals( + isA() + .having( + (e) => e.cameraId, + 'cameraId', + textureId, + ) + .having( + (e) => e.file, + 'file', + isA() + .having( + (f) => f.mimeType, + 'mimeType', + supportedVideoType, + ) + .having( + (f) => f.name, + 'name', + finalVideo.hashCode.toString(), + ), + ) + .having( + (e) => e.maxVideoDuration, + 'maxVideoDuration', + maxVideoDuration, + ), + ), + ); + + await streamQueue.cancel(); + }); + }); + + group('onEnded', () { + testWidgets( + 'emits the default video track ' + 'when it emits an ended event', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + final streamQueue = StreamQueue(camera.onEnded); + + await camera.initialize(); + + final videoTracks = camera.stream!.getVideoTracks(); + final defaultVideoTrack = videoTracks.first; + + defaultVideoTrack.dispatchEvent(Event('ended')); + + expect( + await streamQueue.next, + equals(defaultVideoTrack), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits the default video track ' + 'when the camera is stopped', (tester) async { + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + ); + + final streamQueue = StreamQueue(camera.onEnded); + + await camera.initialize(); + + final videoTracks = camera.stream!.getVideoTracks(); + final defaultVideoTrack = videoTracks.first; + + camera.stop(); + + expect( + await streamQueue.next, + equals(defaultVideoTrack), + ); + + await streamQueue.cancel(); + }); + }); + + group('onVideoRecordingError', () { + testWidgets( + 'emits an ErrorEvent ' + 'when the media recorder fails ' + 'when recording a video', (tester) async { + final mediaRecorder = MockMediaRecorder(); + final errorController = StreamController(); + + final camera = Camera( + textureId: textureId, + cameraService: cameraService, + )..mediaRecorder = mediaRecorder; + + when(() => mediaRecorder.onError) + .thenAnswer((_) => errorController.stream); + + final streamQueue = StreamQueue(camera.onVideoRecordingError); + + await camera.initialize(); + await camera.play(); + + await camera.startVideoRecording(); + + final errorEvent = ErrorEvent('type'); + errorController.add(errorEvent); + + expect( + await streamQueue.next, + equals(errorEvent), + ); + + await streamQueue.cancel(); + }); + }); + }); + }); +} diff --git a/packages/camera/camera_web/example/integration_test/camera_web_exception_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_exception_test.dart new file mode 100644 index 000000000000..6f8531b6f4af --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/camera_web_exception_test.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:camera_web/src/types/types.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraWebException', () { + testWidgets('sets all properties', (tester) async { + final cameraId = 1; + final code = CameraErrorCode.notFound; + final description = 'The camera is not found.'; + + final exception = CameraWebException(cameraId, code, description); + + expect(exception.cameraId, equals(cameraId)); + expect(exception.code, equals(code)); + expect(exception.description, equals(description)); + }); + + testWidgets('toString includes all properties', (tester) async { + final cameraId = 2; + final code = CameraErrorCode.notReadable; + final description = 'The camera is not readable.'; + + final exception = CameraWebException(cameraId, code, description); + + expect( + exception.toString(), + equals('CameraWebException($cameraId, $code, $description)'), + ); + }); + }); +} diff --git a/packages/camera/camera_web/example/integration_test/camera_web_test.dart b/packages/camera/camera_web/example/integration_test/camera_web_test.dart new file mode 100644 index 000000000000..9749559ed8c6 --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/camera_web_test.dart @@ -0,0 +1,2946 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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'; + +import 'package:async/async.dart'; +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/camera_web.dart'; +import 'package:camera_web/src/camera.dart'; +import 'package:camera_web/src/camera_service.dart'; +import 'package:camera_web/src/types/types.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart' as widgets; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:mocktail/mocktail.dart'; + +import 'helpers/helpers.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('CameraPlugin', () { + const cameraId = 0; + + late Window window; + late Navigator navigator; + late MediaDevices mediaDevices; + late VideoElement videoElement; + late Screen screen; + late ScreenOrientation screenOrientation; + late Document document; + late Element documentElement; + + late CameraService cameraService; + + setUp(() async { + window = MockWindow(); + navigator = MockNavigator(); + mediaDevices = MockMediaDevices(); + + videoElement = getVideoElementWithBlankStream(Size(10, 10)); + + when(() => window.navigator).thenReturn(navigator); + when(() => navigator.mediaDevices).thenReturn(mediaDevices); + + screen = MockScreen(); + screenOrientation = MockScreenOrientation(); + + when(() => screen.orientation).thenReturn(screenOrientation); + when(() => window.screen).thenReturn(screen); + + document = MockDocument(); + documentElement = MockElement(); + + when(() => document.documentElement).thenReturn(documentElement); + when(() => window.document).thenReturn(document); + + cameraService = MockCameraService(); + + when( + () => cameraService.getMediaStreamForOptions( + any(), + cameraId: any(named: 'cameraId'), + ), + ).thenAnswer( + (_) async => videoElement.captureStream(), + ); + + CameraPlatform.instance = CameraPlugin( + cameraService: cameraService, + )..window = window; + }); + + setUpAll(() { + registerFallbackValue(MockMediaStreamTrack()); + registerFallbackValue(MockCameraOptions()); + registerFallbackValue(FlashMode.off); + }); + + testWidgets('CameraPlugin is the live instance', (tester) async { + expect(CameraPlatform.instance, isA()); + }); + + group('availableCameras', () { + setUp(() { + when( + () => cameraService.getFacingModeForVideoTrack( + any(), + ), + ).thenReturn(null); + + when(mediaDevices.enumerateDevices).thenAnswer( + (_) async => [], + ); + }); + + testWidgets('requests video and audio permissions', (tester) async { + final _ = await CameraPlatform.instance.availableCameras(); + + verify( + () => cameraService.getMediaStreamForOptions( + CameraOptions( + audio: AudioConstraints(enabled: true), + ), + ), + ).called(1); + }); + + testWidgets( + 'releases the camera stream ' + 'used to request video and audio permissions', (tester) async { + final videoTrack = MockMediaStreamTrack(); + + var videoTrackStopped = false; + when(videoTrack.stop).thenAnswer((_) { + videoTrackStopped = true; + }); + + when( + () => cameraService.getMediaStreamForOptions( + CameraOptions( + audio: AudioConstraints(enabled: true), + ), + ), + ).thenAnswer( + (_) => Future.value( + FakeMediaStream([videoTrack]), + ), + ); + + final _ = await CameraPlatform.instance.availableCameras(); + + expect(videoTrackStopped, isTrue); + }); + + testWidgets( + 'gets a video stream ' + 'for a video input device', (tester) async { + final videoDevice = FakeMediaDeviceInfo( + '1', + 'Camera 1', + MediaDeviceKind.videoInput, + ); + + when(mediaDevices.enumerateDevices).thenAnswer( + (_) => Future.value([videoDevice]), + ); + + final _ = await CameraPlatform.instance.availableCameras(); + + verify( + () => cameraService.getMediaStreamForOptions( + CameraOptions( + video: VideoConstraints( + deviceId: videoDevice.deviceId, + ), + ), + ), + ).called(1); + }); + + testWidgets( + 'does not get a video stream ' + 'for the video input device ' + 'with an empty device id', (tester) async { + final videoDevice = FakeMediaDeviceInfo( + '', + 'Camera 1', + MediaDeviceKind.videoInput, + ); + + when(mediaDevices.enumerateDevices).thenAnswer( + (_) => Future.value([videoDevice]), + ); + + final _ = await CameraPlatform.instance.availableCameras(); + + verifyNever( + () => cameraService.getMediaStreamForOptions( + CameraOptions( + video: VideoConstraints( + deviceId: videoDevice.deviceId, + ), + ), + ), + ); + }); + + testWidgets( + 'gets the facing mode ' + 'from the first available video track ' + 'of the video input device', (tester) async { + final videoDevice = FakeMediaDeviceInfo( + '1', + 'Camera 1', + MediaDeviceKind.videoInput, + ); + + final videoStream = + FakeMediaStream([MockMediaStreamTrack(), MockMediaStreamTrack()]); + + when( + () => cameraService.getMediaStreamForOptions( + CameraOptions( + video: VideoConstraints(deviceId: videoDevice.deviceId), + ), + ), + ).thenAnswer((_) => Future.value(videoStream)); + + when(mediaDevices.enumerateDevices).thenAnswer( + (_) => Future.value([videoDevice]), + ); + + final _ = await CameraPlatform.instance.availableCameras(); + + verify( + () => cameraService.getFacingModeForVideoTrack( + videoStream.getVideoTracks().first, + ), + ).called(1); + }); + + testWidgets( + 'returns appropriate camera descriptions ' + 'for multiple video devices ' + 'based on video streams', (tester) async { + final firstVideoDevice = FakeMediaDeviceInfo( + '1', + 'Camera 1', + MediaDeviceKind.videoInput, + ); + + final secondVideoDevice = FakeMediaDeviceInfo( + '4', + 'Camera 4', + MediaDeviceKind.videoInput, + ); + + // Create a video stream for the first video device. + final firstVideoStream = + FakeMediaStream([MockMediaStreamTrack(), MockMediaStreamTrack()]); + + // Create a video stream for the second video device. + final secondVideoStream = FakeMediaStream([MockMediaStreamTrack()]); + + // Mock media devices to return two video input devices + // and two audio devices. + when(mediaDevices.enumerateDevices).thenAnswer( + (_) => Future.value([ + firstVideoDevice, + FakeMediaDeviceInfo( + '2', + 'Audio Input 2', + MediaDeviceKind.audioInput, + ), + FakeMediaDeviceInfo( + '3', + 'Audio Output 3', + MediaDeviceKind.audioOutput, + ), + secondVideoDevice, + ]), + ); + + // Mock camera service to return the first video stream + // for the first video device. + when( + () => cameraService.getMediaStreamForOptions( + CameraOptions( + video: VideoConstraints(deviceId: firstVideoDevice.deviceId), + ), + ), + ).thenAnswer((_) => Future.value(firstVideoStream)); + + // Mock camera service to return the second video stream + // for the second video device. + when( + () => cameraService.getMediaStreamForOptions( + CameraOptions( + video: VideoConstraints(deviceId: secondVideoDevice.deviceId), + ), + ), + ).thenAnswer((_) => Future.value(secondVideoStream)); + + // Mock camera service to return a user facing mode + // for the first video stream. + when( + () => cameraService.getFacingModeForVideoTrack( + firstVideoStream.getVideoTracks().first, + ), + ).thenReturn('user'); + + when(() => cameraService.mapFacingModeToLensDirection('user')) + .thenReturn(CameraLensDirection.front); + + // Mock camera service to return an environment facing mode + // for the second video stream. + when( + () => cameraService.getFacingModeForVideoTrack( + secondVideoStream.getVideoTracks().first, + ), + ).thenReturn('environment'); + + when(() => cameraService.mapFacingModeToLensDirection('environment')) + .thenReturn(CameraLensDirection.back); + + final cameras = await CameraPlatform.instance.availableCameras(); + + // Expect two cameras and ignore two audio devices. + expect( + cameras, + equals([ + CameraDescription( + name: firstVideoDevice.label!, + lensDirection: CameraLensDirection.front, + sensorOrientation: 0, + ), + CameraDescription( + name: secondVideoDevice.label!, + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ) + ]), + ); + }); + + testWidgets( + 'sets camera metadata ' + 'for the camera description', (tester) async { + final videoDevice = FakeMediaDeviceInfo( + '1', + 'Camera 1', + MediaDeviceKind.videoInput, + ); + + final videoStream = + FakeMediaStream([MockMediaStreamTrack(), MockMediaStreamTrack()]); + + when(mediaDevices.enumerateDevices).thenAnswer( + (_) => Future.value([videoDevice]), + ); + + when( + () => cameraService.getMediaStreamForOptions( + CameraOptions( + video: VideoConstraints(deviceId: videoDevice.deviceId), + ), + ), + ).thenAnswer((_) => Future.value(videoStream)); + + when( + () => cameraService.getFacingModeForVideoTrack( + videoStream.getVideoTracks().first, + ), + ).thenReturn('left'); + + when(() => cameraService.mapFacingModeToLensDirection('left')) + .thenReturn(CameraLensDirection.external); + + final camera = (await CameraPlatform.instance.availableCameras()).first; + + expect( + (CameraPlatform.instance as CameraPlugin).camerasMetadata, + equals({ + camera: CameraMetadata( + deviceId: videoDevice.deviceId!, + facingMode: 'left', + ) + }), + ); + }); + + group('throws CameraException', () { + testWidgets( + 'with notSupported error ' + 'when there are no media devices', (tester) async { + when(() => navigator.mediaDevices).thenReturn(null); + + expect( + () => CameraPlatform.instance.availableCameras(), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notSupported.toString(), + ), + ), + ); + }); + + testWidgets('when MediaDevices.enumerateDevices throws DomException', + (tester) async { + final exception = FakeDomException(DomException.UNKNOWN); + + when(mediaDevices.enumerateDevices).thenThrow(exception); + + expect( + () => CameraPlatform.instance.availableCameras(), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets( + 'when CameraService.getMediaStreamForOptions ' + 'throws CameraWebException', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.security, + 'description', + ); + + when(() => cameraService.getMediaStreamForOptions(any())) + .thenThrow(exception); + + expect( + () => CameraPlatform.instance.availableCameras(), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + + testWidgets( + 'when CameraService.getMediaStreamForOptions ' + 'throws PlatformException', (tester) async { + final exception = PlatformException( + code: CameraErrorCode.notSupported.toString(), + message: 'message', + ); + + when(() => cameraService.getMediaStreamForOptions(any())) + .thenThrow(exception); + + expect( + () => CameraPlatform.instance.availableCameras(), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + group('createCamera', () { + group('creates a camera', () { + const ultraHighResolutionSize = Size(3840, 2160); + const maxResolutionSize = Size(3840, 2160); + + final cameraDescription = CameraDescription( + name: 'name', + lensDirection: CameraLensDirection.front, + sensorOrientation: 0, + ); + + final cameraMetadata = CameraMetadata( + deviceId: 'deviceId', + facingMode: 'user', + ); + + setUp(() { + // Add metadata for the camera description. + (CameraPlatform.instance as CameraPlugin) + .camerasMetadata[cameraDescription] = cameraMetadata; + + when( + () => cameraService.mapFacingModeToCameraType('user'), + ).thenReturn(CameraType.user); + }); + + testWidgets('with appropriate options', (tester) async { + when( + () => cameraService + .mapResolutionPresetToSize(ResolutionPreset.ultraHigh), + ).thenReturn(ultraHighResolutionSize); + + final cameraId = await CameraPlatform.instance.createCamera( + cameraDescription, + ResolutionPreset.ultraHigh, + enableAudio: true, + ); + + expect( + (CameraPlatform.instance as CameraPlugin).cameras[cameraId], + isA() + .having( + (camera) => camera.textureId, + 'textureId', + cameraId, + ) + .having( + (camera) => camera.options, + 'options', + CameraOptions( + audio: AudioConstraints(enabled: true), + video: VideoConstraints( + facingMode: FacingModeConstraint(CameraType.user), + width: VideoSizeConstraint( + ideal: ultraHighResolutionSize.width.toInt(), + ), + height: VideoSizeConstraint( + ideal: ultraHighResolutionSize.height.toInt(), + ), + deviceId: cameraMetadata.deviceId, + ), + ), + ), + ); + }); + + testWidgets( + 'with a max resolution preset ' + 'and enabled audio set to false ' + 'when no options are specified', (tester) async { + when( + () => cameraService.mapResolutionPresetToSize(ResolutionPreset.max), + ).thenReturn(maxResolutionSize); + + final cameraId = await CameraPlatform.instance.createCamera( + cameraDescription, + null, + ); + + expect( + (CameraPlatform.instance as CameraPlugin).cameras[cameraId], + isA().having( + (camera) => camera.options, + 'options', + CameraOptions( + audio: AudioConstraints(enabled: false), + video: VideoConstraints( + facingMode: FacingModeConstraint(CameraType.user), + width: VideoSizeConstraint( + ideal: maxResolutionSize.width.toInt(), + ), + height: VideoSizeConstraint( + ideal: maxResolutionSize.height.toInt(), + ), + deviceId: cameraMetadata.deviceId, + ), + ), + ), + ); + }); + }); + + testWidgets( + 'throws CameraException ' + 'with missingMetadata error ' + 'if there is no metadata ' + 'for the given camera description', (tester) async { + expect( + () => CameraPlatform.instance.createCamera( + CameraDescription( + name: 'name', + lensDirection: CameraLensDirection.back, + sensorOrientation: 0, + ), + ResolutionPreset.ultraHigh, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.missingMetadata.toString(), + ), + ), + ); + }); + }); + + group('initializeCamera', () { + late Camera camera; + late VideoElement videoElement; + + late StreamController errorStreamController, abortStreamController; + late StreamController endedStreamController; + + setUp(() { + camera = MockCamera(); + videoElement = MockVideoElement(); + + errorStreamController = StreamController(); + abortStreamController = StreamController(); + endedStreamController = StreamController(); + + when(camera.getVideoSize).thenReturn(Size(10, 10)); + when(camera.initialize).thenAnswer((_) => Future.value()); + when(camera.play).thenAnswer((_) => Future.value()); + + when(() => camera.videoElement).thenReturn(videoElement); + when(() => videoElement.onError) + .thenAnswer((_) => FakeElementStream(errorStreamController.stream)); + when(() => videoElement.onAbort) + .thenAnswer((_) => FakeElementStream(abortStreamController.stream)); + + when(() => camera.onEnded) + .thenAnswer((_) => endedStreamController.stream); + }); + + testWidgets('initializes and plays the camera', (tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.initializeCamera(cameraId); + + verify(camera.initialize).called(1); + verify(camera.play).called(1); + }); + + testWidgets('starts listening to the camera video error and abort events', + (tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect(errorStreamController.hasListener, isFalse); + expect(abortStreamController.hasListener, isFalse); + + await CameraPlatform.instance.initializeCamera(cameraId); + + expect(errorStreamController.hasListener, isTrue); + expect(abortStreamController.hasListener, isTrue); + }); + + testWidgets('starts listening to the camera ended events', + (tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect(endedStreamController.hasListener, isFalse); + + await CameraPlatform.instance.initializeCamera(cameraId); + + expect(endedStreamController.hasListener, isTrue); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () => CameraPlatform.instance.initializeCamera(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when camera throws CameraWebException', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.permissionDenied, + 'description', + ); + + when(camera.initialize).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.initializeCamera(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + + testWidgets('when camera throws DomException', (tester) async { + final exception = FakeDomException(DomException.NOT_ALLOWED); + + when(camera.initialize).thenAnswer((_) => Future.value()); + when(camera.play).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.initializeCamera(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name.toString(), + ), + ), + ); + }); + }); + }); + + group('lockCaptureOrientation', () { + setUp(() { + when( + () => cameraService.mapDeviceOrientationToOrientationType(any()), + ).thenReturn(OrientationType.portraitPrimary); + }); + + testWidgets( + 'requests full-screen mode ' + 'on documentElement', (tester) async { + await CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.portraitUp, + ); + + verify(documentElement.requestFullscreen).called(1); + }); + + testWidgets( + 'locks the capture orientation ' + 'based on the given device orientation', (tester) async { + when( + () => cameraService.mapDeviceOrientationToOrientationType( + DeviceOrientation.landscapeRight, + ), + ).thenReturn(OrientationType.landscapeSecondary); + + await CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.landscapeRight, + ); + + verify( + () => cameraService.mapDeviceOrientationToOrientationType( + DeviceOrientation.landscapeRight, + ), + ).called(1); + + verify( + () => screenOrientation.lock( + OrientationType.landscapeSecondary, + ), + ).called(1); + }); + + group('throws PlatformException', () { + testWidgets( + 'with orientationNotSupported error ' + 'when screen is not supported', (tester) async { + when(() => window.screen).thenReturn(null); + + expect( + () => CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.portraitUp, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.orientationNotSupported.toString(), + ), + ), + ); + }); + + testWidgets( + 'with orientationNotSupported error ' + 'when screen orientation is not supported', (tester) async { + when(() => screen.orientation).thenReturn(null); + + expect( + () => CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.portraitUp, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.orientationNotSupported.toString(), + ), + ), + ); + }); + + testWidgets( + 'with orientationNotSupported error ' + 'when documentElement is not available', (tester) async { + when(() => document.documentElement).thenReturn(null); + + expect( + () => CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.portraitUp, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.orientationNotSupported.toString(), + ), + ), + ); + }); + + testWidgets('when lock throws DomException', (tester) async { + final exception = FakeDomException(DomException.NOT_ALLOWED); + + when(() => screenOrientation.lock(any())).thenThrow(exception); + + expect( + () => CameraPlatform.instance.lockCaptureOrientation( + cameraId, + DeviceOrientation.portraitDown, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + }); + }); + + group('unlockCaptureOrientation', () { + setUp(() { + when( + () => cameraService.mapDeviceOrientationToOrientationType(any()), + ).thenReturn(OrientationType.portraitPrimary); + }); + + testWidgets('unlocks the capture orientation', (tester) async { + await CameraPlatform.instance.unlockCaptureOrientation( + cameraId, + ); + + verify(screenOrientation.unlock).called(1); + }); + + group('throws PlatformException', () { + testWidgets( + 'with orientationNotSupported error ' + 'when screen is not supported', (tester) async { + when(() => window.screen).thenReturn(null); + + expect( + () => CameraPlatform.instance.unlockCaptureOrientation( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.orientationNotSupported.toString(), + ), + ), + ); + }); + + testWidgets( + 'with orientationNotSupported error ' + 'when screen orientation is not supported', (tester) async { + when(() => screen.orientation).thenReturn(null); + + expect( + () => CameraPlatform.instance.unlockCaptureOrientation( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.orientationNotSupported.toString(), + ), + ), + ); + }); + + testWidgets( + 'with orientationNotSupported error ' + 'when documentElement is not available', (tester) async { + when(() => document.documentElement).thenReturn(null); + + expect( + () => CameraPlatform.instance.unlockCaptureOrientation( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.orientationNotSupported.toString(), + ), + ), + ); + }); + + testWidgets('when unlock throws DomException', (tester) async { + final exception = FakeDomException(DomException.NOT_ALLOWED); + + when(screenOrientation.unlock).thenThrow(exception); + + expect( + () => CameraPlatform.instance.unlockCaptureOrientation( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + }); + }); + + group('takePicture', () { + testWidgets('captures a picture', (tester) async { + final camera = MockCamera(); + final capturedPicture = MockXFile(); + + when(camera.takePicture) + .thenAnswer((_) => Future.value(capturedPicture)); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + final picture = await CameraPlatform.instance.takePicture(cameraId); + + verify(camera.takePicture).called(1); + + expect(picture, equals(capturedPicture)); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () => CameraPlatform.instance.takePicture(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when takePicture throws DomException', (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.NOT_SUPPORTED); + + when(camera.takePicture).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.takePicture(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when takePicture throws CameraWebException', + (tester) async { + final camera = MockCamera(); + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.takePicture).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.takePicture(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + group('startVideoRecording', () { + late Camera camera; + + setUp(() { + camera = MockCamera(); + + when(camera.startVideoRecording).thenAnswer((_) async {}); + + when(() => camera.onVideoRecordingError) + .thenAnswer((_) => const Stream.empty()); + }); + + testWidgets('starts a video recording', (tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.startVideoRecording(cameraId); + + verify(camera.startVideoRecording).called(1); + }); + + testWidgets('listens to the onVideoRecordingError stream', + (tester) async { + final videoRecordingErrorController = StreamController(); + + when(() => camera.onVideoRecordingError) + .thenAnswer((_) => videoRecordingErrorController.stream); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.startVideoRecording(cameraId); + + expect( + videoRecordingErrorController.hasListener, + isTrue, + ); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () => CameraPlatform.instance.startVideoRecording(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when startVideoRecording throws DomException', + (tester) async { + final exception = FakeDomException(DomException.INVALID_STATE); + + when(camera.startVideoRecording).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.startVideoRecording(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when startVideoRecording throws CameraWebException', + (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.startVideoRecording).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.startVideoRecording(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + group('stopVideoRecording', () { + testWidgets('stops a video recording', (tester) async { + final camera = MockCamera(); + final capturedVideo = MockXFile(); + + when(camera.stopVideoRecording) + .thenAnswer((_) => Future.value(capturedVideo)); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + final video = + await CameraPlatform.instance.stopVideoRecording(cameraId); + + verify(camera.stopVideoRecording).called(1); + + expect(video, capturedVideo); + }); + + testWidgets('stops listening to the onVideoRecordingError stream', + (tester) async { + final camera = MockCamera(); + final videoRecordingErrorController = StreamController(); + + when(camera.startVideoRecording).thenAnswer((_) async => {}); + + when(camera.stopVideoRecording) + .thenAnswer((_) => Future.value(MockXFile())); + + when(() => camera.onVideoRecordingError) + .thenAnswer((_) => videoRecordingErrorController.stream); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.startVideoRecording(cameraId); + final _ = await CameraPlatform.instance.stopVideoRecording(cameraId); + + expect( + videoRecordingErrorController.hasListener, + isFalse, + ); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () => CameraPlatform.instance.stopVideoRecording(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when stopVideoRecording throws DomException', + (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.INVALID_STATE); + + when(camera.stopVideoRecording).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.stopVideoRecording(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when stopVideoRecording throws CameraWebException', + (tester) async { + final camera = MockCamera(); + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.stopVideoRecording).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.stopVideoRecording(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + group('pauseVideoRecording', () { + testWidgets('pauses a video recording', (tester) async { + final camera = MockCamera(); + + when(camera.pauseVideoRecording).thenAnswer((_) async {}); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.pauseVideoRecording(cameraId); + + verify(camera.pauseVideoRecording).called(1); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () => CameraPlatform.instance.pauseVideoRecording(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when pauseVideoRecording throws DomException', + (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.INVALID_STATE); + + when(camera.pauseVideoRecording).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.pauseVideoRecording(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when pauseVideoRecording throws CameraWebException', + (tester) async { + final camera = MockCamera(); + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.pauseVideoRecording).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.pauseVideoRecording(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + group('resumeVideoRecording', () { + testWidgets('resumes a video recording', (tester) async { + final camera = MockCamera(); + + when(camera.resumeVideoRecording).thenAnswer((_) async {}); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.resumeVideoRecording(cameraId); + + verify(camera.resumeVideoRecording).called(1); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () => CameraPlatform.instance.resumeVideoRecording(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when resumeVideoRecording throws DomException', + (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.INVALID_STATE); + + when(camera.resumeVideoRecording).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.resumeVideoRecording(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when resumeVideoRecording throws CameraWebException', + (tester) async { + final camera = MockCamera(); + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.resumeVideoRecording).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.resumeVideoRecording(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + group('setFlashMode', () { + testWidgets('calls setFlashMode on the camera', (tester) async { + final camera = MockCamera(); + const flashMode = FlashMode.always; + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.setFlashMode( + cameraId, + flashMode, + ); + + verify(() => camera.setFlashMode(flashMode)).called(1); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () => CameraPlatform.instance.setFlashMode( + cameraId, + FlashMode.always, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when setFlashMode throws DomException', (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.NOT_SUPPORTED); + + when(() => camera.setFlashMode(any())).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.setFlashMode( + cameraId, + FlashMode.always, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when setFlashMode throws CameraWebException', + (tester) async { + final camera = MockCamera(); + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(() => camera.setFlashMode(any())).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.setFlashMode( + cameraId, + FlashMode.torch, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + testWidgets('setExposureMode throws UnimplementedError', (tester) async { + expect( + () => CameraPlatform.instance.setExposureMode( + cameraId, + ExposureMode.auto, + ), + throwsUnimplementedError, + ); + }); + + testWidgets('setExposurePoint throws UnimplementedError', (tester) async { + expect( + () => CameraPlatform.instance.setExposurePoint( + cameraId, + const Point(0, 0), + ), + throwsUnimplementedError, + ); + }); + + testWidgets('getMinExposureOffset throws UnimplementedError', + (tester) async { + expect( + () => CameraPlatform.instance.getMinExposureOffset(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('getMaxExposureOffset throws UnimplementedError', + (tester) async { + expect( + () => CameraPlatform.instance.getMaxExposureOffset(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('getExposureOffsetStepSize throws UnimplementedError', + (tester) async { + expect( + () => CameraPlatform.instance.getExposureOffsetStepSize(cameraId), + throwsUnimplementedError, + ); + }); + + testWidgets('setExposureOffset throws UnimplementedError', (tester) async { + expect( + () => CameraPlatform.instance.setExposureOffset( + cameraId, + 0, + ), + throwsUnimplementedError, + ); + }); + + testWidgets('setFocusMode throws UnimplementedError', (tester) async { + expect( + () => CameraPlatform.instance.setFocusMode( + cameraId, + FocusMode.auto, + ), + throwsUnimplementedError, + ); + }); + + testWidgets('setFocusPoint throws UnimplementedError', (tester) async { + expect( + () => CameraPlatform.instance.setFocusPoint( + cameraId, + const Point(0, 0), + ), + throwsUnimplementedError, + ); + }); + + group('getMaxZoomLevel', () { + testWidgets('calls getMaxZoomLevel on the camera', (tester) async { + final camera = MockCamera(); + const maximumZoomLevel = 100.0; + + when(camera.getMaxZoomLevel).thenReturn(maximumZoomLevel); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + await CameraPlatform.instance.getMaxZoomLevel( + cameraId, + ), + equals(maximumZoomLevel), + ); + + verify(camera.getMaxZoomLevel).called(1); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () async => await CameraPlatform.instance.getMaxZoomLevel( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when getMaxZoomLevel throws DomException', (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.NOT_SUPPORTED); + + when(camera.getMaxZoomLevel).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.getMaxZoomLevel( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when getMaxZoomLevel throws CameraWebException', + (tester) async { + final camera = MockCamera(); + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.getMaxZoomLevel).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.getMaxZoomLevel( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + group('getMinZoomLevel', () { + testWidgets('calls getMinZoomLevel on the camera', (tester) async { + final camera = MockCamera(); + const minimumZoomLevel = 100.0; + + when(camera.getMinZoomLevel).thenReturn(minimumZoomLevel); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + await CameraPlatform.instance.getMinZoomLevel( + cameraId, + ), + equals(minimumZoomLevel), + ); + + verify(camera.getMinZoomLevel).called(1); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () async => await CameraPlatform.instance.getMinZoomLevel( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when getMinZoomLevel throws DomException', (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.NOT_SUPPORTED); + + when(camera.getMinZoomLevel).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.getMinZoomLevel( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when getMinZoomLevel throws CameraWebException', + (tester) async { + final camera = MockCamera(); + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.getMinZoomLevel).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.getMinZoomLevel( + cameraId, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + group('setZoomLevel', () { + testWidgets('calls setZoomLevel on the camera', (tester) async { + final camera = MockCamera(); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + const zoom = 100.0; + + await CameraPlatform.instance.setZoomLevel(cameraId, zoom); + + verify(() => camera.setZoomLevel(zoom)).called(1); + }); + + group('throws CameraException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () async => await CameraPlatform.instance.setZoomLevel( + cameraId, + 100.0, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when setZoomLevel throws DomException', (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.NOT_SUPPORTED); + + when(() => camera.setZoomLevel(any())).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.setZoomLevel( + cameraId, + 100.0, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when setZoomLevel throws PlatformException', + (tester) async { + final camera = MockCamera(); + final exception = PlatformException( + code: CameraErrorCode.notSupported.toString(), + message: 'message', + ); + + when(() => camera.setZoomLevel(any())).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.setZoomLevel( + cameraId, + 100.0, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code, + ), + ), + ); + }); + + testWidgets('when setZoomLevel throws CameraWebException', + (tester) async { + final camera = MockCamera(); + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(() => camera.setZoomLevel(any())).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.setZoomLevel( + cameraId, + 100.0, + ), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + group('pausePreview', () { + testWidgets('calls pause on the camera', (tester) async { + final camera = MockCamera(); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.pausePreview(cameraId); + + verify(camera.pause).called(1); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () async => await CameraPlatform.instance.pausePreview(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when pause throws DomException', (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.NOT_SUPPORTED); + + when(camera.pause).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.pausePreview(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + }); + }); + + group('resumePreview', () { + testWidgets('calls play on the camera', (tester) async { + final camera = MockCamera(); + + when(camera.play).thenAnswer((_) async => {}); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.resumePreview(cameraId); + + verify(camera.play).called(1); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () async => await CameraPlatform.instance.resumePreview(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when play throws DomException', (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.NOT_SUPPORTED); + + when(camera.play).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.resumePreview(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + + testWidgets('when play throws CameraWebException', (tester) async { + final camera = MockCamera(); + final exception = CameraWebException( + cameraId, + CameraErrorCode.unknown, + 'description', + ); + + when(camera.play).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () async => await CameraPlatform.instance.resumePreview(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.code.toString(), + ), + ), + ); + }); + }); + }); + + testWidgets( + 'buildPreview returns an HtmlElementView ' + 'with an appropriate view type', (tester) async { + final camera = Camera( + textureId: cameraId, + cameraService: cameraService, + ); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + CameraPlatform.instance.buildPreview(cameraId), + isA().having( + (view) => view.viewType, + 'viewType', + camera.getViewType(), + ), + ); + }); + + group('dispose', () { + late Camera camera; + late VideoElement videoElement; + + late StreamController errorStreamController, abortStreamController; + late StreamController endedStreamController; + late StreamController videoRecordingErrorController; + + setUp(() { + camera = MockCamera(); + videoElement = MockVideoElement(); + + errorStreamController = StreamController(); + abortStreamController = StreamController(); + endedStreamController = StreamController(); + videoRecordingErrorController = StreamController(); + + when(camera.getVideoSize).thenReturn(Size(10, 10)); + when(camera.initialize).thenAnswer((_) => Future.value()); + when(camera.play).thenAnswer((_) => Future.value()); + when(camera.dispose).thenAnswer((_) => Future.value()); + + when(() => camera.videoElement).thenReturn(videoElement); + when(() => videoElement.onError) + .thenAnswer((_) => FakeElementStream(errorStreamController.stream)); + when(() => videoElement.onAbort) + .thenAnswer((_) => FakeElementStream(abortStreamController.stream)); + + when(() => camera.onEnded) + .thenAnswer((_) => endedStreamController.stream); + + when(() => camera.onVideoRecordingError) + .thenAnswer((_) => videoRecordingErrorController.stream); + + when(camera.startVideoRecording).thenAnswer((_) async {}); + }); + + testWidgets('disposes the correct camera', (tester) async { + const firstCameraId = 0; + const secondCameraId = 1; + + final firstCamera = MockCamera(); + final secondCamera = MockCamera(); + + when(firstCamera.dispose).thenAnswer((_) => Future.value()); + when(secondCamera.dispose).thenAnswer((_) => Future.value()); + + // Save cameras in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras.addAll({ + firstCameraId: firstCamera, + secondCameraId: secondCamera, + }); + + // Dispose the first camera. + await CameraPlatform.instance.dispose(firstCameraId); + + // The first camera should be disposed. + verify(firstCamera.dispose).called(1); + verifyNever(secondCamera.dispose); + + // The first camera should be removed from the camera plugin. + expect( + (CameraPlatform.instance as CameraPlugin).cameras, + equals({ + secondCameraId: secondCamera, + }), + ); + }); + + testWidgets('cancels the camera video error and abort subscriptions', + (tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.initializeCamera(cameraId); + await CameraPlatform.instance.dispose(cameraId); + + expect(errorStreamController.hasListener, isFalse); + expect(abortStreamController.hasListener, isFalse); + }); + + testWidgets('cancels the camera ended subscriptions', (tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.initializeCamera(cameraId); + await CameraPlatform.instance.dispose(cameraId); + + expect(endedStreamController.hasListener, isFalse); + }); + + testWidgets('cancels the camera video recording error subscriptions', + (tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + await CameraPlatform.instance.initializeCamera(cameraId); + await CameraPlatform.instance.startVideoRecording(cameraId); + await CameraPlatform.instance.dispose(cameraId); + + expect(videoRecordingErrorController.hasListener, isFalse); + }); + + group('throws PlatformException', () { + testWidgets( + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () => CameraPlatform.instance.dispose(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + + testWidgets('when dispose throws DomException', (tester) async { + final camera = MockCamera(); + final exception = FakeDomException(DomException.INVALID_ACCESS); + + when(camera.dispose).thenThrow(exception); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + () => CameraPlatform.instance.dispose(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + exception.name, + ), + ), + ); + }); + }); + }); + + group('getCamera', () { + testWidgets('returns the correct camera', (tester) async { + final camera = Camera( + textureId: cameraId, + cameraService: cameraService, + ); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + expect( + (CameraPlatform.instance as CameraPlugin).getCamera(cameraId), + equals(camera), + ); + }); + + testWidgets( + 'throws PlatformException ' + 'with notFound error ' + 'if the camera does not exist', (tester) async { + expect( + () => (CameraPlatform.instance as CameraPlugin).getCamera(cameraId), + throwsA( + isA().having( + (e) => e.code, + 'code', + CameraErrorCode.notFound.toString(), + ), + ), + ); + }); + }); + + group('events', () { + late Camera camera; + late VideoElement videoElement; + + late StreamController errorStreamController, abortStreamController; + late StreamController endedStreamController; + late StreamController videoRecordingErrorController; + + setUp(() { + camera = MockCamera(); + videoElement = MockVideoElement(); + + errorStreamController = StreamController(); + abortStreamController = StreamController(); + endedStreamController = StreamController(); + videoRecordingErrorController = StreamController(); + + when(camera.getVideoSize).thenReturn(Size(10, 10)); + when(camera.initialize).thenAnswer((_) => Future.value()); + when(camera.play).thenAnswer((_) => Future.value()); + + when(() => camera.videoElement).thenReturn(videoElement); + when(() => videoElement.onError) + .thenAnswer((_) => FakeElementStream(errorStreamController.stream)); + when(() => videoElement.onAbort) + .thenAnswer((_) => FakeElementStream(abortStreamController.stream)); + + when(() => camera.onEnded) + .thenAnswer((_) => endedStreamController.stream); + + when(() => camera.onVideoRecordingError) + .thenAnswer((_) => videoRecordingErrorController.stream); + + when(() => camera.startVideoRecording()).thenAnswer((_) async => {}); + }); + + testWidgets( + 'onCameraInitialized emits a CameraInitializedEvent ' + 'on initializeCamera', (tester) async { + // Mock the camera to use a blank video stream of size 1280x720. + const videoSize = Size(1280, 720); + + videoElement = getVideoElementWithBlankStream(videoSize); + + when( + () => cameraService.getMediaStreamForOptions( + any(), + cameraId: cameraId, + ), + ).thenAnswer((_) async => videoElement.captureStream()); + + final camera = Camera( + textureId: cameraId, + cameraService: cameraService, + ); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + final Stream eventStream = + CameraPlatform.instance.onCameraInitialized(cameraId); + + final streamQueue = StreamQueue(eventStream); + + await CameraPlatform.instance.initializeCamera(cameraId); + + expect( + await streamQueue.next, + equals( + CameraInitializedEvent( + cameraId, + videoSize.width, + videoSize.height, + ExposureMode.auto, + false, + FocusMode.auto, + false, + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets('onCameraResolutionChanged emits an empty stream', + (tester) async { + expect( + CameraPlatform.instance.onCameraResolutionChanged(cameraId), + emits(isEmpty), + ); + }); + + testWidgets( + 'onCameraClosing emits a CameraClosingEvent ' + 'on the camera ended event', (tester) async { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + final Stream eventStream = + CameraPlatform.instance.onCameraClosing(cameraId); + + final streamQueue = StreamQueue(eventStream); + + await CameraPlatform.instance.initializeCamera(cameraId); + + endedStreamController.add(MockMediaStreamTrack()); + + expect( + await streamQueue.next, + equals( + CameraClosingEvent(cameraId), + ), + ); + + await streamQueue.cancel(); + }); + + group('onCameraError', () { + setUp(() { + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on the camera video error event ' + 'with a message', (tester) async { + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + await CameraPlatform.instance.initializeCamera(cameraId); + + final error = FakeMediaError( + MediaError.MEDIA_ERR_NETWORK, + 'A network error occured.', + ); + + final errorCode = CameraErrorCode.fromMediaError(error); + + when(() => videoElement.error).thenReturn(error); + + errorStreamController.add(Event('error')); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${errorCode}, error message: ${error.message}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on the camera video error event ' + 'with no message', (tester) async { + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + await CameraPlatform.instance.initializeCamera(cameraId); + + final error = FakeMediaError(MediaError.MEDIA_ERR_NETWORK); + final errorCode = CameraErrorCode.fromMediaError(error); + + when(() => videoElement.error).thenReturn(error); + + errorStreamController.add(Event('error')); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${errorCode}, error message: No further diagnostic information can be determined or provided.', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on the camera video abort event', (tester) async { + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + await CameraPlatform.instance.initializeCamera(cameraId); + + abortStreamController.add(Event('abort')); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${CameraErrorCode.abort}, error message: The video element\'s source has not fully loaded.', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on takePicture error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.takePicture).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => await CameraPlatform.instance.takePicture(cameraId), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on setFlashMode error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(() => camera.setFlashMode(any())).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => await CameraPlatform.instance.setFlashMode( + cameraId, + FlashMode.always, + ), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on getMaxZoomLevel error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.zoomLevelNotSupported, + 'description', + ); + + when(camera.getMaxZoomLevel).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => await CameraPlatform.instance.getMaxZoomLevel( + cameraId, + ), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on getMinZoomLevel error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.zoomLevelNotSupported, + 'description', + ); + + when(camera.getMinZoomLevel).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => await CameraPlatform.instance.getMinZoomLevel( + cameraId, + ), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on setZoomLevel error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.zoomLevelNotSupported, + 'description', + ); + + when(() => camera.setZoomLevel(any())).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => await CameraPlatform.instance.setZoomLevel( + cameraId, + 100.0, + ), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on resumePreview error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.unknown, + 'description', + ); + + when(camera.play).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => await CameraPlatform.instance.resumePreview(cameraId), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on startVideoRecording error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(() => camera.onVideoRecordingError) + .thenAnswer((_) => const Stream.empty()); + + when( + () => camera.startVideoRecording( + maxVideoDuration: any(named: 'maxVideoDuration'), + ), + ).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => + await CameraPlatform.instance.startVideoRecording(cameraId), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on the camera video recording error event', (tester) async { + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + await CameraPlatform.instance.initializeCamera(cameraId); + await CameraPlatform.instance.startVideoRecording(cameraId); + + final errorEvent = FakeErrorEvent('type', 'message'); + + videoRecordingErrorController.add(errorEvent); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${errorEvent.type}, error message: ${errorEvent.message}.', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on stopVideoRecording error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.stopVideoRecording).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => + await CameraPlatform.instance.stopVideoRecording(cameraId), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on pauseVideoRecording error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.pauseVideoRecording).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => + await CameraPlatform.instance.pauseVideoRecording(cameraId), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits a CameraErrorEvent ' + 'on resumeVideoRecording error', (tester) async { + final exception = CameraWebException( + cameraId, + CameraErrorCode.notStarted, + 'description', + ); + + when(camera.resumeVideoRecording).thenThrow(exception); + + final Stream eventStream = + CameraPlatform.instance.onCameraError(cameraId); + + final streamQueue = StreamQueue(eventStream); + + expect( + () async => + await CameraPlatform.instance.resumeVideoRecording(cameraId), + throwsA( + isA(), + ), + ); + + expect( + await streamQueue.next, + equals( + CameraErrorEvent( + cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ), + ); + + await streamQueue.cancel(); + }); + }); + + testWidgets('onVideoRecordedEvent emits a VideoRecordedEvent', + (tester) async { + final camera = MockCamera(); + final capturedVideo = MockXFile(); + final stream = Stream.value( + VideoRecordedEvent(cameraId, capturedVideo, Duration.zero)); + when(() => camera.onVideoRecordedEvent).thenAnswer((_) => stream); + + // Save the camera in the camera plugin. + (CameraPlatform.instance as CameraPlugin).cameras[cameraId] = camera; + + final streamQueue = + StreamQueue(CameraPlatform.instance.onVideoRecordedEvent(cameraId)); + + expect( + await streamQueue.next, + equals( + VideoRecordedEvent(cameraId, capturedVideo, Duration.zero), + ), + ); + }); + + group('onDeviceOrientationChanged', () { + group('emits an empty stream', () { + testWidgets('when screen is not supported', (tester) async { + when(() => window.screen).thenReturn(null); + + expect( + CameraPlatform.instance.onDeviceOrientationChanged(), + emits(isEmpty), + ); + }); + + testWidgets('when screen orientation is not supported', + (tester) async { + when(() => screen.orientation).thenReturn(null); + + expect( + CameraPlatform.instance.onDeviceOrientationChanged(), + emits(isEmpty), + ); + }); + }); + + testWidgets('emits the initial DeviceOrientationChangedEvent', + (tester) async { + when( + () => cameraService.mapOrientationTypeToDeviceOrientation( + OrientationType.portraitPrimary, + ), + ).thenReturn(DeviceOrientation.portraitUp); + + // Set the initial screen orientation to portraitPrimary. + when(() => screenOrientation.type) + .thenReturn(OrientationType.portraitPrimary); + + final eventStreamController = StreamController(); + + when(() => screenOrientation.onChange) + .thenAnswer((_) => eventStreamController.stream); + + final Stream eventStream = + CameraPlatform.instance.onDeviceOrientationChanged(); + + final streamQueue = StreamQueue(eventStream); + + expect( + await streamQueue.next, + equals( + DeviceOrientationChangedEvent( + DeviceOrientation.portraitUp, + ), + ), + ); + + await streamQueue.cancel(); + }); + + testWidgets( + 'emits a DeviceOrientationChangedEvent ' + 'when the screen orientation is changed', (tester) async { + when( + () => cameraService.mapOrientationTypeToDeviceOrientation( + OrientationType.landscapePrimary, + ), + ).thenReturn(DeviceOrientation.landscapeLeft); + + when( + () => cameraService.mapOrientationTypeToDeviceOrientation( + OrientationType.portraitSecondary, + ), + ).thenReturn(DeviceOrientation.portraitDown); + + final eventStreamController = StreamController(); + + when(() => screenOrientation.onChange) + .thenAnswer((_) => eventStreamController.stream); + + final Stream eventStream = + CameraPlatform.instance.onDeviceOrientationChanged(); + + final streamQueue = StreamQueue(eventStream); + + // Change the screen orientation to landscapePrimary and + // emit an event on the screenOrientation.onChange stream. + when(() => screenOrientation.type) + .thenReturn(OrientationType.landscapePrimary); + + eventStreamController.add(Event('change')); + + expect( + await streamQueue.next, + equals( + DeviceOrientationChangedEvent( + DeviceOrientation.landscapeLeft, + ), + ), + ); + + // Change the screen orientation to portraitSecondary and + // emit an event on the screenOrientation.onChange stream. + when(() => screenOrientation.type) + .thenReturn(OrientationType.portraitSecondary); + + eventStreamController.add(Event('change')); + + expect( + await streamQueue.next, + equals( + DeviceOrientationChangedEvent( + DeviceOrientation.portraitDown, + ), + ), + ); + + await streamQueue.cancel(); + }); + }); + }); + }); +} diff --git a/packages/camera/camera_web/example/integration_test/helpers/helpers.dart b/packages/camera/camera_web/example/integration_test/helpers/helpers.dart new file mode 100644 index 000000000000..7094f55bb62e --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/helpers/helpers.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 'mocks.dart'; diff --git a/packages/camera/camera_web/example/integration_test/helpers/mocks.dart b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart new file mode 100644 index 000000000000..77e9077356f7 --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/helpers/mocks.dart @@ -0,0 +1,172 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:html'; +import 'dart:ui'; + +import 'package:camera_web/src/camera.dart'; +import 'package:camera_web/src/camera_service.dart'; +import 'package:camera_web/src/shims/dart_js_util.dart'; +import 'package:camera_web/src/types/types.dart'; +import 'package:cross_file/cross_file.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockWindow extends Mock implements Window {} + +class MockScreen extends Mock implements Screen {} + +class MockScreenOrientation extends Mock implements ScreenOrientation {} + +class MockDocument extends Mock implements Document {} + +class MockElement extends Mock implements Element {} + +class MockNavigator extends Mock implements Navigator {} + +class MockMediaDevices extends Mock implements MediaDevices {} + +class MockCameraService extends Mock implements CameraService {} + +class MockMediaStreamTrack extends Mock implements MediaStreamTrack {} + +class MockCamera extends Mock implements Camera {} + +class MockCameraOptions extends Mock implements CameraOptions {} + +class MockVideoElement extends Mock implements VideoElement {} + +class MockXFile extends Mock implements XFile {} + +class MockJsUtil extends Mock implements JsUtil {} + +class MockMediaRecorder extends Mock implements MediaRecorder {} + +/// A fake [MediaStream] that returns the provided [_videoTracks]. +class FakeMediaStream extends Fake implements MediaStream { + FakeMediaStream(this._videoTracks); + + final List _videoTracks; + + @override + List getVideoTracks() => _videoTracks; +} + +/// A fake [MediaDeviceInfo] that returns the provided [_deviceId], [_label] and [_kind]. +class FakeMediaDeviceInfo extends Fake implements MediaDeviceInfo { + FakeMediaDeviceInfo(this._deviceId, this._label, this._kind); + + final String _deviceId; + final String _label; + final String _kind; + + @override + String? get deviceId => _deviceId; + + @override + String? get label => _label; + + @override + String? get kind => _kind; +} + +/// A fake [MediaError] that returns the provided error [_code] and [_message]. +class FakeMediaError extends Fake implements MediaError { + FakeMediaError( + this._code, [ + String message = '', + ]) : _message = message; + + final int _code; + final String _message; + + @override + int get code => _code; + + @override + String? get message => _message; +} + +/// A fake [DomException] that returns the provided error [_name] and [_message]. +class FakeDomException extends Fake implements DomException { + FakeDomException( + this._name, [ + String? message, + ]) : _message = message; + + final String _name; + final String? _message; + + @override + String get name => _name; + + @override + String? get message => _message; +} + +/// A fake [ElementStream] that listens to the provided [_stream] on [listen]. +class FakeElementStream extends Fake + implements ElementStream { + FakeElementStream(this._stream); + + final Stream _stream; + + @override + StreamSubscription listen(void onData(T event)?, + {Function? onError, void onDone()?, bool? cancelOnError}) { + return _stream.listen( + onData, + onError: onError, + onDone: onDone, + cancelOnError: cancelOnError, + ); + } +} + +/// A fake [BlobEvent] that returns the provided blob [data]. +class FakeBlobEvent extends Fake implements BlobEvent { + FakeBlobEvent(this._blob); + + final Blob? _blob; + + @override + Blob? get data => _blob; +} + +/// A fake [DomException] that returns the provided error [_name] and [_message]. +class FakeErrorEvent extends Fake implements ErrorEvent { + FakeErrorEvent( + String type, [ + String? message, + ]) : _type = type, + _message = message; + + final String _type; + final String? _message; + + @override + String get type => _type; + + @override + String? get message => _message; +} + +/// Returns a video element with a blank stream of size [videoSize]. +/// +/// Can be used to mock a video stream: +/// ```dart +/// final videoElement = getVideoElementWithBlankStream(Size(100, 100)); +/// final videoStream = videoElement.captureStream(); +/// ``` +VideoElement getVideoElementWithBlankStream(Size videoSize) { + final canvasElement = CanvasElement( + width: videoSize.width.toInt(), + height: videoSize.height.toInt(), + )..context2D.fillRect(0, 0, videoSize.width, videoSize.height); + + final videoElement = VideoElement() + ..srcObject = canvasElement.captureStream(); + + return videoElement; +} diff --git a/packages/camera/camera_web/example/integration_test/zoom_level_capability_test.dart b/packages/camera/camera_web/example/integration_test/zoom_level_capability_test.dart new file mode 100644 index 000000000000..09de03100871 --- /dev/null +++ b/packages/camera/camera_web/example/integration_test/zoom_level_capability_test.dart @@ -0,0 +1,50 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera_web/src/types/types.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'helpers/helpers.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('ZoomLevelCapability', () { + testWidgets('sets all properties', (tester) async { + const minimum = 100.0; + const maximum = 400.0; + final videoTrack = MockMediaStreamTrack(); + + final capability = ZoomLevelCapability( + minimum: minimum, + maximum: maximum, + videoTrack: videoTrack, + ); + + expect(capability.minimum, equals(minimum)); + expect(capability.maximum, equals(maximum)); + expect(capability.videoTrack, equals(videoTrack)); + }); + + testWidgets('supports value equality', (tester) async { + final videoTrack = MockMediaStreamTrack(); + + expect( + ZoomLevelCapability( + minimum: 0.0, + maximum: 100.0, + videoTrack: videoTrack, + ), + equals( + ZoomLevelCapability( + minimum: 0.0, + maximum: 100.0, + videoTrack: videoTrack, + ), + ), + ); + }); + }); +} diff --git a/packages/camera/camera_web/example/lib/main.dart b/packages/camera/camera_web/example/lib/main.dart new file mode 100644 index 000000000000..6e8f85e74f40 --- /dev/null +++ b/packages/camera/camera_web/example/lib/main.dart @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; + +void main() => runApp(MyApp()); + +/// App for testing +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Directionality( + textDirection: TextDirection.ltr, + child: Text('Testing... Look at the console output for results!'), + ); + } +} diff --git a/packages/camera/camera_web/example/pubspec.yaml b/packages/camera/camera_web/example/pubspec.yaml new file mode 100644 index 000000000000..1e075712325e --- /dev/null +++ b/packages/camera/camera_web/example/pubspec.yaml @@ -0,0 +1,21 @@ +name: camera_web_integration_tests +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + mocktail: ^0.1.4 + camera_web: + path: ../ + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter diff --git a/packages/camera/camera_web/example/run_test.sh b/packages/camera/camera_web/example/run_test.sh new file mode 100755 index 000000000000..00482faa53df --- /dev/null +++ b/packages/camera/camera_web/example/run_test.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env 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/camera/camera_web/example/test_driver/integration_test.dart b/packages/camera/camera_web/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/camera/camera_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/camera/camera_web/example/web/index.html b/packages/camera/camera_web/example/web/index.html new file mode 100644 index 000000000000..f3c6a5e8a8e3 --- /dev/null +++ b/packages/camera/camera_web/example/web/index.html @@ -0,0 +1,12 @@ + + + + + Browser Tests + + + + + diff --git a/packages/camera/camera_web/lib/camera_web.dart b/packages/camera/camera_web/lib/camera_web.dart new file mode 100644 index 000000000000..dcefc9293b88 --- /dev/null +++ b/packages/camera/camera_web/lib/camera_web.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. + +library camera_web; + +export 'src/camera_web.dart'; diff --git a/packages/camera/camera_web/lib/src/camera.dart b/packages/camera/camera_web/lib/src/camera.dart new file mode 100644 index 000000000000..cf0187057188 --- /dev/null +++ b/packages/camera/camera_web/lib/src/camera.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 'dart:async'; +import 'dart:html' as html; +import 'dart:ui'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/src/camera_service.dart'; +import 'package:camera_web/src/types/types.dart'; +import 'package:flutter/foundation.dart'; + +import 'shims/dart_ui.dart' as ui; + +String _getViewType(int cameraId) => 'plugins.flutter.io/camera_$cameraId'; + +/// A camera initialized from the media devices in the current window. +/// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices +/// +/// The obtained camera stream is constrained by [options] and fetched +/// with [CameraService.getMediaStreamForOptions]. +/// +/// The camera stream is displayed in the [videoElement] wrapped in the +/// [divElement] to avoid overriding the custom styles applied to +/// the video element in [_applyDefaultVideoStyles]. +/// See: https://github.com/flutter/flutter/issues/79519 +/// +/// The camera stream can be played/stopped by calling [play]/[stop], +/// may capture a picture by calling [takePicture] or capture a video +/// by calling [startVideoRecording], [pauseVideoRecording], +/// [resumeVideoRecording] or [stopVideoRecording]. +/// +/// The camera zoom may be adjusted with [setZoomLevel]. The provided +/// zoom level must be a value in the range of [getMinZoomLevel] to +/// [getMaxZoomLevel]. +/// +/// The [textureId] is used to register a camera view with the id +/// defined by [_getViewType]. +class Camera { + /// Creates a new instance of [Camera] + /// with the given [textureId] and optional + /// [options] and [window]. + Camera({ + required this.textureId, + required CameraService cameraService, + this.options = const CameraOptions(), + }) : _cameraService = cameraService; + + // A torch mode constraint name. + // See: https://w3c.github.io/mediacapture-image/#dom-mediatracksupportedconstraints-torch + static const _torchModeKey = "torch"; + + /// The texture id used to register the camera view. + final int textureId; + + /// The camera options used to initialize a camera, empty by default. + final CameraOptions options; + + /// The video element that displays the camera stream. + /// Initialized in [initialize]. + late final html.VideoElement videoElement; + + /// The wrapping element for the [videoElement] to avoid overriding + /// the custom styles applied in [_applyDefaultVideoStyles]. + /// Initialized in [initialize]. + late final html.DivElement divElement; + + /// The camera stream displayed in the [videoElement]. + /// Initialized in [initialize] and [play], reset in [stop]. + html.MediaStream? stream; + + /// The stream of the camera video tracks that have ended playing. + /// + /// This occurs when there is no more camera stream data, e.g. + /// the user has stopped the stream by changing the camera device, + /// revoked the camera permissions or ejected the camera device. + /// + /// MediaStreamTrack.onended: + /// https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/onended + Stream get onEnded => onEndedController.stream; + + /// The stream controller for the [onEnded] stream. + @visibleForTesting + final onEndedController = StreamController.broadcast(); + + StreamSubscription? _onEndedSubscription; + + /// The stream of the camera video recording errors. + /// + /// This occurs when the video recording is not allowed or an unsupported + /// codec is used. + /// + /// MediaRecorder.error: + /// https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/error_event + Stream get onVideoRecordingError => + videoRecordingErrorController.stream; + + /// The stream controller for the [onVideoRecordingError] stream. + @visibleForTesting + final videoRecordingErrorController = + StreamController.broadcast(); + + StreamSubscription? _onVideoRecordingErrorSubscription; + + /// The camera flash mode. + @visibleForTesting + FlashMode? flashMode; + + /// The camera service used to get the media stream for the camera. + final CameraService _cameraService; + + /// The current browser window used to access media devices. + @visibleForTesting + html.Window? window = html.window; + + /// The recorder used to record a video from the camera. + @visibleForTesting + html.MediaRecorder? mediaRecorder; + + /// Whether the video of the given type is supported. + @visibleForTesting + bool Function(String) isVideoTypeSupported = + html.MediaRecorder.isTypeSupported; + + /// The list of consecutive video data files recorded with [mediaRecorder]. + List _videoData = []; + + /// Completes when the video recording is stopped/finished. + Completer? _videoAvailableCompleter; + + /// A data listener fired when a new part of video data is available. + void Function(html.Event)? _videoDataAvailableListener; + + /// A listener fired when a video recording is stopped. + void Function(html.Event)? _videoRecordingStoppedListener; + + /// A builder to merge a list of blobs into a single blob. + @visibleForTesting + html.Blob Function(List blobs, String type) blobBuilder = + (blobs, type) => html.Blob(blobs, type); + + /// The stream that emits a [VideoRecordedEvent] when a video recording is created. + Stream get onVideoRecordedEvent => + videoRecorderController.stream; + + /// The stream controller for the [onVideoRecordedEvent] stream. + @visibleForTesting + final StreamController videoRecorderController = + StreamController.broadcast(); + + /// Initializes the camera stream displayed in the [videoElement]. + /// Registers the camera view with [textureId] under [_getViewType] type. + /// Emits the camera default video track on the [onEnded] stream when it ends. + Future initialize() async { + stream = await _cameraService.getMediaStreamForOptions( + options, + cameraId: textureId, + ); + + videoElement = html.VideoElement(); + + divElement = html.DivElement() + ..style.setProperty('object-fit', 'cover') + ..append(videoElement); + + ui.platformViewRegistry.registerViewFactory( + _getViewType(textureId), + (_) => divElement, + ); + + videoElement + ..autoplay = false + ..muted = true + ..srcObject = stream + ..setAttribute('playsinline', ''); + + _applyDefaultVideoStyles(videoElement); + + final videoTracks = stream!.getVideoTracks(); + + if (videoTracks.isNotEmpty) { + final defaultVideoTrack = videoTracks.first; + + _onEndedSubscription = defaultVideoTrack.onEnded.listen((html.Event _) { + onEndedController.add(defaultVideoTrack); + }); + } + } + + /// Starts the camera stream. + /// + /// Initializes the camera source if the camera was previously stopped. + Future play() async { + if (videoElement.srcObject == null) { + stream = await _cameraService.getMediaStreamForOptions( + options, + cameraId: textureId, + ); + videoElement.srcObject = stream; + } + await videoElement.play(); + } + + /// Pauses the camera stream on the current frame. + void pause() { + videoElement.pause(); + } + + /// Stops the camera stream and resets the camera source. + void stop() { + final videoTracks = stream!.getVideoTracks(); + if (videoTracks.isNotEmpty) { + onEndedController.add(videoTracks.first); + } + + final tracks = stream?.getTracks(); + if (tracks != null) { + for (final track in tracks) { + track.stop(); + } + } + videoElement.srcObject = null; + stream = null; + } + + /// Captures a picture and returns the saved file in a JPEG format. + /// + /// Enables the camera flash (torch mode) for a period of taking a picture + /// if the flash mode is either [FlashMode.auto] or [FlashMode.always]. + Future takePicture() async { + final shouldEnableTorchMode = + flashMode == FlashMode.auto || flashMode == FlashMode.always; + + if (shouldEnableTorchMode) { + _setTorchMode(enabled: true); + } + + final videoWidth = videoElement.videoWidth; + final videoHeight = videoElement.videoHeight; + final canvas = html.CanvasElement(width: videoWidth, height: videoHeight); + final isBackCamera = getLensDirection() == CameraLensDirection.back; + + // Flip the picture horizontally if it is not taken from a back camera. + if (!isBackCamera) { + canvas.context2D + ..translate(videoWidth, 0) + ..scale(-1, 1); + } + + canvas.context2D + .drawImageScaled(videoElement, 0, 0, videoWidth, videoHeight); + + final blob = await canvas.toBlob('image/jpeg'); + + if (shouldEnableTorchMode) { + _setTorchMode(enabled: false); + } + + return XFile(html.Url.createObjectUrl(blob)); + } + + /// Returns a size of the camera video based on its first video track size. + /// + /// Returns [Size.zero] if the camera is missing a video track or + /// the video track does not include the width or height setting. + Size getVideoSize() { + final videoTracks = videoElement.srcObject?.getVideoTracks() ?? []; + + if (videoTracks.isEmpty) { + return Size.zero; + } + + final defaultVideoTrack = videoTracks.first; + final defaultVideoTrackSettings = defaultVideoTrack.getSettings(); + + final width = defaultVideoTrackSettings['width']; + final height = defaultVideoTrackSettings['height']; + + if (width != null && height != null) { + return Size(width, height); + } else { + return Size.zero; + } + } + + /// Sets the camera flash mode to [mode] by modifying the camera + /// torch mode constraint. + /// + /// The torch mode is enabled for [FlashMode.torch] and + /// disabled for [FlashMode.off]. + /// + /// For [FlashMode.auto] and [FlashMode.always] the torch mode is enabled + /// only for a period of taking a picture in [takePicture]. + /// + /// Throws a [CameraWebException] if the torch mode is not supported + /// or the camera has not been initialized or started. + void setFlashMode(FlashMode mode) { + final mediaDevices = window?.navigator.mediaDevices; + final supportedConstraints = mediaDevices?.getSupportedConstraints(); + final torchModeSupported = supportedConstraints?[_torchModeKey] ?? false; + + if (!torchModeSupported) { + throw CameraWebException( + textureId, + CameraErrorCode.torchModeNotSupported, + 'The torch mode is not supported in the current browser.', + ); + } + + // Save the updated flash mode to be used later when taking a picture. + flashMode = mode; + + // Enable the torch mode only if the flash mode is torch. + _setTorchMode(enabled: mode == FlashMode.torch); + } + + /// Sets the camera torch mode constraint to [enabled]. + /// + /// Throws a [CameraWebException] if the torch mode is not supported + /// or the camera has not been initialized or started. + void _setTorchMode({required bool enabled}) { + final videoTracks = stream?.getVideoTracks() ?? []; + + if (videoTracks.isNotEmpty) { + final defaultVideoTrack = videoTracks.first; + + final bool canEnableTorchMode = + defaultVideoTrack.getCapabilities()[_torchModeKey] ?? false; + + if (canEnableTorchMode) { + defaultVideoTrack.applyConstraints({ + "advanced": [ + { + _torchModeKey: enabled, + } + ] + }); + } else { + throw CameraWebException( + textureId, + CameraErrorCode.torchModeNotSupported, + 'The torch mode is not supported by the current camera.', + ); + } + } else { + throw CameraWebException( + textureId, + CameraErrorCode.notStarted, + 'The camera has not been initialized or started.', + ); + } + } + + /// Returns the camera maximum zoom level. + /// + /// Throws a [CameraWebException] if the zoom level is not supported + /// or the camera has not been initialized or started. + double getMaxZoomLevel() => + _cameraService.getZoomLevelCapabilityForCamera(this).maximum; + + /// Returns the camera minimum zoom level. + /// + /// Throws a [CameraWebException] if the zoom level is not supported + /// or the camera has not been initialized or started. + double getMinZoomLevel() => + _cameraService.getZoomLevelCapabilityForCamera(this).minimum; + + /// Sets the camera zoom level to [zoom]. + /// + /// Throws a [CameraWebException] if the zoom level is invalid, + /// not supported or the camera has not been initialized or started. + void setZoomLevel(double zoom) { + final zoomLevelCapability = + _cameraService.getZoomLevelCapabilityForCamera(this); + + if (zoom < zoomLevelCapability.minimum || + zoom > zoomLevelCapability.maximum) { + throw CameraWebException( + textureId, + CameraErrorCode.zoomLevelInvalid, + 'The provided zoom level must be in the range of ${zoomLevelCapability.minimum} to ${zoomLevelCapability.maximum}.', + ); + } + + zoomLevelCapability.videoTrack.applyConstraints({ + "advanced": [ + { + ZoomLevelCapability.constraintName: zoom, + } + ] + }); + } + + /// Returns a lens direction of this camera. + /// + /// Returns null if the camera is missing a video track or + /// the video track does not include the facing mode setting. + CameraLensDirection? getLensDirection() { + final videoTracks = videoElement.srcObject?.getVideoTracks() ?? []; + + if (videoTracks.isEmpty) { + return null; + } + + final defaultVideoTrack = videoTracks.first; + final defaultVideoTrackSettings = defaultVideoTrack.getSettings(); + + final facingMode = defaultVideoTrackSettings['facingMode']; + + if (facingMode != null) { + return _cameraService.mapFacingModeToLensDirection(facingMode); + } else { + return null; + } + } + + /// Returns the registered view type of the camera. + String getViewType() => _getViewType(textureId); + + /// Starts a new video recording using [html.MediaRecorder]. + /// + /// Throws a [CameraWebException] if the provided maximum video duration is invalid + /// or the browser does not support any of the available video mime types + /// from [_videoMimeType]. + Future startVideoRecording({Duration? maxVideoDuration}) async { + if (maxVideoDuration != null && maxVideoDuration.inMilliseconds <= 0) { + throw CameraWebException( + textureId, + CameraErrorCode.notSupported, + 'The maximum video duration must be greater than 0 milliseconds.', + ); + } + + mediaRecorder ??= html.MediaRecorder(videoElement.srcObject!, { + 'mimeType': _videoMimeType, + }); + + _videoAvailableCompleter = Completer(); + + _videoDataAvailableListener = + (event) => _onVideoDataAvailable(event, maxVideoDuration); + + _videoRecordingStoppedListener = + (event) => _onVideoRecordingStopped(event, maxVideoDuration); + + mediaRecorder!.addEventListener( + 'dataavailable', + _videoDataAvailableListener, + ); + + mediaRecorder!.addEventListener( + 'stop', + _videoRecordingStoppedListener, + ); + + _onVideoRecordingErrorSubscription = + mediaRecorder!.onError.listen((html.Event event) { + final error = event as html.ErrorEvent; + if (error != null) { + videoRecordingErrorController.add(error); + } + }); + + if (maxVideoDuration != null) { + mediaRecorder!.start(maxVideoDuration.inMilliseconds); + } else { + // Don't pass the null duration as that will fire a `dataavailable` event directly. + mediaRecorder!.start(); + } + } + + void _onVideoDataAvailable( + html.Event event, [ + Duration? maxVideoDuration, + ]) { + final blob = (event as html.BlobEvent).data; + + // Append the recorded part of the video to the list of all video data files. + if (blob != null) { + _videoData.add(blob); + } + + // Stop the recorder if the video has a maxVideoDuration + // and the recording was not stopped manually. + if (maxVideoDuration != null && mediaRecorder!.state == 'recording') { + mediaRecorder!.stop(); + } + } + + Future _onVideoRecordingStopped( + html.Event event, [ + Duration? maxVideoDuration, + ]) async { + if (_videoData.isNotEmpty) { + // Concatenate all video data files into a single blob. + final videoType = _videoData.first.type; + final videoBlob = blobBuilder(_videoData, videoType); + + // Create a file containing the video blob. + final file = XFile( + html.Url.createObjectUrl(videoBlob), + mimeType: _videoMimeType, + name: videoBlob.hashCode.toString(), + ); + + // Emit an event containing the recorded video file. + videoRecorderController.add( + VideoRecordedEvent(this.textureId, file, maxVideoDuration), + ); + + _videoAvailableCompleter?.complete(file); + } + + // Clean up the media recorder with its event listeners and video data. + mediaRecorder!.removeEventListener( + 'dataavailable', + _videoDataAvailableListener, + ); + + mediaRecorder!.removeEventListener( + 'stop', + _videoDataAvailableListener, + ); + + await _onVideoRecordingErrorSubscription?.cancel(); + + mediaRecorder = null; + _videoDataAvailableListener = null; + _videoRecordingStoppedListener = null; + _videoData.clear(); + } + + /// Pauses the current video recording. + /// + /// Throws a [CameraWebException] if the video recorder is uninitialized. + Future pauseVideoRecording() async { + if (mediaRecorder == null) { + throw _videoRecordingNotStartedException; + } + mediaRecorder!.pause(); + } + + /// Resumes the current video recording. + /// + /// Throws a [CameraWebException] if the video recorder is uninitialized. + Future resumeVideoRecording() async { + if (mediaRecorder == null) { + throw _videoRecordingNotStartedException; + } + mediaRecorder!.resume(); + } + + /// Stops the video recording and returns the captured video file. + /// + /// Throws a [CameraWebException] if the video recorder is uninitialized. + Future stopVideoRecording() async { + if (mediaRecorder == null || _videoAvailableCompleter == null) { + throw _videoRecordingNotStartedException; + } + + mediaRecorder!.stop(); + + return _videoAvailableCompleter!.future; + } + + /// Disposes the camera by stopping the camera stream, + /// the video recording and reloading the camera source. + Future dispose() async { + // Stop the camera stream. + stop(); + + await videoRecorderController.close(); + mediaRecorder = null; + _videoDataAvailableListener = null; + + // Reset the [videoElement] to its initial state. + videoElement + ..srcObject = null + ..load(); + + await _onEndedSubscription?.cancel(); + _onEndedSubscription = null; + await onEndedController.close(); + + await _onVideoRecordingErrorSubscription?.cancel(); + _onVideoRecordingErrorSubscription = null; + await videoRecordingErrorController.close(); + } + + /// Returns the first supported video mime type (amongst mp4 and webm) + /// to use when recording a video. + /// + /// Throws a [CameraWebException] if the browser does not support + /// any of the available video mime types. + String get _videoMimeType { + const types = [ + 'video/mp4', + 'video/webm', + ]; + + return types.firstWhere( + (type) => isVideoTypeSupported(type), + orElse: () => throw CameraWebException( + textureId, + CameraErrorCode.notSupported, + 'The browser does not support any of the following video types: ${types.join(',')}.', + ), + ); + } + + CameraWebException get _videoRecordingNotStartedException => + CameraWebException( + textureId, + CameraErrorCode.videoRecordingNotStarted, + 'The video recorder is uninitialized. The recording might not have been started. Make sure to call `startVideoRecording` first.', + ); + + /// Applies default styles to the video [element]. + void _applyDefaultVideoStyles(html.VideoElement element) { + final isBackCamera = getLensDirection() == CameraLensDirection.back; + + // Flip the video horizontally if it is not taken from a back camera. + if (!isBackCamera) { + element.style.transform = 'scaleX(-1)'; + } + + element.style + ..transformOrigin = 'center' + ..pointerEvents = 'none' + ..width = '100%' + ..height = '100%' + ..objectFit = 'cover'; + } +} diff --git a/packages/camera/camera_web/lib/src/camera_service.dart b/packages/camera/camera_web/lib/src/camera_service.dart new file mode 100644 index 000000000000..5ba5c80395cc --- /dev/null +++ b/packages/camera/camera_web/lib/src/camera_service.dart @@ -0,0 +1,326 @@ +// Copyright 2013 The Flutter Authors. All 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:ui'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/src/camera.dart'; +import 'package:camera_web/src/shims/dart_js_util.dart'; +import 'package:camera_web/src/types/types.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +/// A service to fetch, map camera settings and +/// obtain the camera stream. +class CameraService { + // A facing mode constraint name. + static const _facingModeKey = "facingMode"; + + /// The current browser window used to access media devices. + @visibleForTesting + html.Window? window = html.window; + + /// The utility to manipulate JavaScript interop objects. + @visibleForTesting + JsUtil jsUtil = JsUtil(); + + /// Returns a media stream associated with the camera device + /// with [cameraId] and constrained by [options]. + Future getMediaStreamForOptions( + CameraOptions options, { + int cameraId = 0, + }) async { + final mediaDevices = window?.navigator.mediaDevices; + + // Throw a not supported exception if the current browser window + // does not support any media devices. + if (mediaDevices == null) { + throw PlatformException( + code: CameraErrorCode.notSupported.toString(), + message: 'The camera is not supported on this device.', + ); + } + + try { + final constraints = await options.toJson(); + return await mediaDevices.getUserMedia(constraints); + } on html.DomException catch (e) { + switch (e.name) { + case 'NotFoundError': + case 'DevicesNotFoundError': + throw CameraWebException( + cameraId, + CameraErrorCode.notFound, + 'No camera found for the given camera options.', + ); + case 'NotReadableError': + case 'TrackStartError': + throw CameraWebException( + cameraId, + CameraErrorCode.notReadable, + 'The camera is not readable due to a hardware error ' + 'that prevented access to the device.', + ); + case 'OverconstrainedError': + case 'ConstraintNotSatisfiedError': + throw CameraWebException( + cameraId, + CameraErrorCode.overconstrained, + 'The camera options are impossible to satisfy.', + ); + case 'NotAllowedError': + case 'PermissionDeniedError': + throw CameraWebException( + cameraId, + CameraErrorCode.permissionDenied, + 'The camera cannot be used or the permission ' + 'to access the camera is not granted.', + ); + case 'TypeError': + throw CameraWebException( + cameraId, + CameraErrorCode.type, + 'The camera options are incorrect or attempted' + 'to access the media input from an insecure context.', + ); + case 'AbortError': + throw CameraWebException( + cameraId, + CameraErrorCode.abort, + 'Some problem occurred that prevented the camera from being used.', + ); + case 'SecurityError': + throw CameraWebException( + cameraId, + CameraErrorCode.security, + 'The user media support is disabled in the current browser.', + ); + default: + throw CameraWebException( + cameraId, + CameraErrorCode.unknown, + 'An unknown error occured when fetching the camera stream.', + ); + } + } catch (_) { + throw CameraWebException( + cameraId, + CameraErrorCode.unknown, + 'An unknown error occured when fetching the camera stream.', + ); + } + } + + /// Returns the zoom level capability for the given [camera]. + /// + /// Throws a [CameraWebException] if the zoom level is not supported + /// or the camera has not been initialized or started. + ZoomLevelCapability getZoomLevelCapabilityForCamera( + Camera camera, + ) { + final mediaDevices = window?.navigator.mediaDevices; + final supportedConstraints = mediaDevices?.getSupportedConstraints(); + final zoomLevelSupported = + supportedConstraints?[ZoomLevelCapability.constraintName] ?? false; + + if (!zoomLevelSupported) { + throw CameraWebException( + camera.textureId, + CameraErrorCode.zoomLevelNotSupported, + 'The zoom level is not supported in the current browser.', + ); + } + + final videoTracks = camera.stream?.getVideoTracks() ?? []; + + if (videoTracks.isNotEmpty) { + final defaultVideoTrack = videoTracks.first; + + /// The zoom level capability is represented by MediaSettingsRange. + /// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaSettingsRange + final zoomLevelCapability = defaultVideoTrack + .getCapabilities()[ZoomLevelCapability.constraintName] ?? + {}; + + // The zoom level capability is a nested JS object, therefore + // we need to access its properties with the js_util library. + // See: https://api.dart.dev/stable/2.13.4/dart-js_util/getProperty.html + final minimumZoomLevel = jsUtil.getProperty(zoomLevelCapability, 'min'); + final maximumZoomLevel = jsUtil.getProperty(zoomLevelCapability, 'max'); + + if (minimumZoomLevel != null && maximumZoomLevel != null) { + return ZoomLevelCapability( + minimum: minimumZoomLevel.toDouble(), + maximum: maximumZoomLevel.toDouble(), + videoTrack: defaultVideoTrack, + ); + } else { + throw CameraWebException( + camera.textureId, + CameraErrorCode.zoomLevelNotSupported, + 'The zoom level is not supported by the current camera.', + ); + } + } else { + throw CameraWebException( + camera.textureId, + CameraErrorCode.notStarted, + 'The camera has not been initialized or started.', + ); + } + } + + /// Returns a facing mode of the [videoTrack] + /// (null if the facing mode is not available). + String? getFacingModeForVideoTrack(html.MediaStreamTrack videoTrack) { + final mediaDevices = window?.navigator.mediaDevices; + + // Throw a not supported exception if the current browser window + // does not support any media devices. + if (mediaDevices == null) { + throw PlatformException( + code: CameraErrorCode.notSupported.toString(), + message: 'The camera is not supported on this device.', + ); + } + + // Check if the camera facing mode is supported by the current browser. + final supportedConstraints = mediaDevices.getSupportedConstraints(); + final facingModeSupported = supportedConstraints[_facingModeKey] ?? false; + + // Return null if the facing mode is not supported. + if (!facingModeSupported) { + return null; + } + + // Extract the facing mode from the video track settings. + // The property may not be available if it's not supported + // by the browser or not available due to context. + // + // MediaTrackSettings: + // https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSettings + final videoTrackSettings = videoTrack.getSettings(); + final facingMode = videoTrackSettings[_facingModeKey]; + + if (facingMode == null) { + // If the facing mode does not exist in the video track settings, + // check for the facing mode in the video track capabilities. + // + // MediaTrackCapabilities: + // https://www.w3.org/TR/mediacapture-streams/#dom-mediatrackcapabilities + + // Check if getting the video track capabilities is supported. + // + // The method may not be supported on Firefox. + // See: https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/getCapabilities#browser_compatibility + if (!jsUtil.hasProperty(videoTrack, 'getCapabilities')) { + // Return null if the video track capabilites are not supported. + return null; + } + + final videoTrackCapabilities = videoTrack.getCapabilities(); + + // A list of facing mode capabilities as + // the camera may support multiple facing modes. + final facingModeCapabilities = + List.from(videoTrackCapabilities[_facingModeKey] ?? []); + + if (facingModeCapabilities.isNotEmpty) { + final facingModeCapability = facingModeCapabilities.first; + return facingModeCapability; + } else { + // Return null if there are no facing mode capabilities. + return null; + } + } + + return facingMode; + } + + /// Maps the given [facingMode] to [CameraLensDirection]. + /// + /// The following values for the facing mode are supported: + /// https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSettings/facingMode + CameraLensDirection mapFacingModeToLensDirection(String facingMode) { + switch (facingMode) { + case 'user': + return CameraLensDirection.front; + case 'environment': + return CameraLensDirection.back; + case 'left': + case 'right': + default: + return CameraLensDirection.external; + } + } + + /// Maps the given [facingMode] to [CameraType]. + /// + /// See [CameraMetadata.facingMode] for more details. + CameraType mapFacingModeToCameraType(String facingMode) { + switch (facingMode) { + case 'user': + return CameraType.user; + case 'environment': + return CameraType.environment; + case 'left': + case 'right': + default: + return CameraType.user; + } + } + + /// Maps the given [resolutionPreset] to [Size]. + Size mapResolutionPresetToSize(ResolutionPreset resolutionPreset) { + switch (resolutionPreset) { + case ResolutionPreset.max: + case ResolutionPreset.ultraHigh: + return Size(4096, 2160); + case ResolutionPreset.veryHigh: + return Size(1920, 1080); + case ResolutionPreset.high: + return Size(1280, 720); + case ResolutionPreset.medium: + return Size(720, 480); + case ResolutionPreset.low: + default: + return Size(320, 240); + } + } + + /// Maps the given [deviceOrientation] to [OrientationType]. + String mapDeviceOrientationToOrientationType( + DeviceOrientation deviceOrientation, + ) { + switch (deviceOrientation) { + case DeviceOrientation.portraitUp: + return OrientationType.portraitPrimary; + case DeviceOrientation.landscapeLeft: + return OrientationType.landscapePrimary; + case DeviceOrientation.portraitDown: + return OrientationType.portraitSecondary; + case DeviceOrientation.landscapeRight: + return OrientationType.landscapeSecondary; + } + } + + /// Maps the given [orientationType] to [DeviceOrientation]. + DeviceOrientation mapOrientationTypeToDeviceOrientation( + String orientationType, + ) { + switch (orientationType) { + case OrientationType.portraitPrimary: + return DeviceOrientation.portraitUp; + case OrientationType.landscapePrimary: + return DeviceOrientation.landscapeLeft; + case OrientationType.portraitSecondary: + return DeviceOrientation.portraitDown; + case OrientationType.landscapeSecondary: + return DeviceOrientation.landscapeRight; + default: + return DeviceOrientation.portraitUp; + } + } +} diff --git a/packages/camera/camera_web/lib/src/camera_web.dart b/packages/camera/camera_web/lib/src/camera_web.dart new file mode 100644 index 000000000000..0021ee47cbde --- /dev/null +++ b/packages/camera/camera_web/lib/src/camera_web.dart @@ -0,0 +1,672 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:html' as html; +import 'dart:math'; + +import 'package:camera_platform_interface/camera_platform_interface.dart'; +import 'package:camera_web/src/camera.dart'; +import 'package:camera_web/src/camera_service.dart'; +import 'package:camera_web/src/types/types.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:stream_transform/stream_transform.dart'; + +// The default error message, when the error is an empty string. +// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/message +const String _kDefaultErrorMessage = + 'No further diagnostic information can be determined or provided.'; + +/// The web implementation of [CameraPlatform]. +/// +/// This class implements the `package:camera` functionality for the web. +class CameraPlugin extends CameraPlatform { + /// Creates a new instance of [CameraPlugin] + /// with the given [cameraService]. + CameraPlugin({required CameraService cameraService}) + : _cameraService = cameraService; + + /// Registers this class as the default instance of [CameraPlatform]. + static void registerWith(Registrar registrar) { + CameraPlatform.instance = CameraPlugin( + cameraService: CameraService(), + ); + } + + final CameraService _cameraService; + + /// The cameras managed by the [CameraPlugin]. + @visibleForTesting + final cameras = {}; + var _textureCounter = 1; + + /// Metadata associated with each camera description. + /// Populated in [availableCameras]. + @visibleForTesting + final camerasMetadata = {}; + + /// The controller used to broadcast different camera events. + /// + /// It is `broadcast` as multiple controllers may subscribe + /// to different stream views of this controller. + @visibleForTesting + final cameraEventStreamController = StreamController.broadcast(); + + final _cameraVideoErrorSubscriptions = + >{}; + + final _cameraVideoAbortSubscriptions = + >{}; + + final _cameraEndedSubscriptions = + >{}; + + final _cameraVideoRecordingErrorSubscriptions = + >{}; + + /// Returns a stream of camera events for the given [cameraId]. + Stream _cameraEvents(int cameraId) => + cameraEventStreamController.stream + .where((event) => event.cameraId == cameraId); + + /// The current browser window used to access media devices. + @visibleForTesting + html.Window? window = html.window; + + @override + Future> availableCameras() async { + try { + final mediaDevices = window?.navigator.mediaDevices; + final cameras = []; + + // Throw a not supported exception if the current browser window + // does not support any media devices. + if (mediaDevices == null) { + throw PlatformException( + code: CameraErrorCode.notSupported.toString(), + message: 'The camera is not supported on this device.', + ); + } + + // Request video and audio permissions. + final cameraStream = await _cameraService.getMediaStreamForOptions( + CameraOptions( + audio: AudioConstraints(enabled: true), + ), + ); + + // Release the camera stream used to request video and audio permissions. + cameraStream.getVideoTracks().forEach((videoTrack) => videoTrack.stop()); + + // Request available media devices. + final devices = await mediaDevices.enumerateDevices(); + + // Filter video input devices. + final videoInputDevices = devices + .whereType() + .where((device) => device.kind == MediaDeviceKind.videoInput) + + /// The device id property is currently not supported on Internet Explorer: + /// https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/deviceId#browser_compatibility + .where( + (device) => device.deviceId != null && device.deviceId!.isNotEmpty, + ); + + // Map video input devices to camera descriptions. + for (final videoInputDevice in videoInputDevices) { + // Get the video stream for the current video input device + // to later use for the available video tracks. + final videoStream = await _getVideoStreamForDevice( + videoInputDevice.deviceId!, + ); + + // Get all video tracks in the video stream + // to later extract the lens direction from the first track. + final videoTracks = videoStream.getVideoTracks(); + + if (videoTracks.isNotEmpty) { + // Get the facing mode from the first available video track. + final facingMode = + _cameraService.getFacingModeForVideoTrack(videoTracks.first); + + // Get the lens direction based on the facing mode. + // Fallback to the external lens direction + // if the facing mode is not available. + final lensDirection = facingMode != null + ? _cameraService.mapFacingModeToLensDirection(facingMode) + : CameraLensDirection.external; + + // Create a camera description. + // + // The name is a camera label which might be empty + // if no permissions to media devices have been granted. + // + // MediaDeviceInfo.label: + // https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/label + // + // Sensor orientation is currently not supported. + final cameraLabel = videoInputDevice.label ?? ''; + final camera = CameraDescription( + name: cameraLabel, + lensDirection: lensDirection, + sensorOrientation: 0, + ); + + final cameraMetadata = CameraMetadata( + deviceId: videoInputDevice.deviceId!, + facingMode: facingMode, + ); + + cameras.add(camera); + + camerasMetadata[camera] = cameraMetadata; + } else { + // Ignore as no video tracks exist in the current video input device. + continue; + } + } + + return cameras; + } on html.DomException catch (e) { + throw CameraException(e.name, e.message); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw CameraException(e.code.toString(), e.description); + } + } + + @override + Future createCamera( + CameraDescription cameraDescription, + ResolutionPreset? resolutionPreset, { + bool enableAudio = false, + }) async { + try { + if (!camerasMetadata.containsKey(cameraDescription)) { + throw PlatformException( + code: CameraErrorCode.missingMetadata.toString(), + message: + 'Missing camera metadata. Make sure to call `availableCameras` before creating a camera.', + ); + } + + final textureId = _textureCounter++; + + final cameraMetadata = camerasMetadata[cameraDescription]!; + + final cameraType = cameraMetadata.facingMode != null + ? _cameraService.mapFacingModeToCameraType(cameraMetadata.facingMode!) + : null; + + // Use the highest resolution possible + // if the resolution preset is not specified. + final videoSize = _cameraService + .mapResolutionPresetToSize(resolutionPreset ?? ResolutionPreset.max); + + // Create a camera with the given audio and video constraints. + // Sensor orientation is currently not supported. + final camera = Camera( + textureId: textureId, + cameraService: _cameraService, + options: CameraOptions( + audio: AudioConstraints(enabled: enableAudio), + video: VideoConstraints( + facingMode: + cameraType != null ? FacingModeConstraint(cameraType) : null, + width: VideoSizeConstraint( + ideal: videoSize.width.toInt(), + ), + height: VideoSizeConstraint( + ideal: videoSize.height.toInt(), + ), + deviceId: cameraMetadata.deviceId, + ), + ), + ); + + cameras[textureId] = camera; + + return textureId; + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } + } + + @override + Future initializeCamera( + int cameraId, { + // The image format group is currently not supported. + ImageFormatGroup imageFormatGroup = ImageFormatGroup.unknown, + }) async { + try { + final camera = getCamera(cameraId); + + await camera.initialize(); + + // Add camera's video error events to the camera events stream. + // The error event fires when the video element's source has failed to load, or can't be used. + _cameraVideoErrorSubscriptions[cameraId] = + camera.videoElement.onError.listen((html.Event _) { + // The Event itself (_) doesn't contain information about the actual error. + // We need to look at the HTMLMediaElement.error. + // See: https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/error + final error = camera.videoElement.error!; + final errorCode = CameraErrorCode.fromMediaError(error); + final errorMessage = + error.message != '' ? error.message : _kDefaultErrorMessage; + + cameraEventStreamController.add( + CameraErrorEvent( + cameraId, + 'Error code: ${errorCode}, error message: ${errorMessage}', + ), + ); + }); + + // Add camera's video abort events to the camera events stream. + // The abort event fires when the video element's source has not fully loaded. + _cameraVideoAbortSubscriptions[cameraId] = + camera.videoElement.onAbort.listen((html.Event _) { + cameraEventStreamController.add( + CameraErrorEvent( + cameraId, + 'Error code: ${CameraErrorCode.abort}, error message: The video element\'s source has not fully loaded.', + ), + ); + }); + + await camera.play(); + + // Add camera's closing events to the camera events stream. + // The onEnded stream fires when there is no more camera stream data. + _cameraEndedSubscriptions[cameraId] = + camera.onEnded.listen((html.MediaStreamTrack _) { + cameraEventStreamController.add( + CameraClosingEvent(cameraId), + ); + }); + + final cameraSize = camera.getVideoSize(); + + cameraEventStreamController.add( + CameraInitializedEvent( + cameraId, + cameraSize.width, + cameraSize.height, + // TODO(camera_web): Add support for exposure mode and point (https://github.com/flutter/flutter/issues/86857). + ExposureMode.auto, + false, + // TODO(camera_web): Add support for focus mode and point (https://github.com/flutter/flutter/issues/86858). + FocusMode.auto, + false, + ), + ); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); + } + } + + @override + Stream onCameraInitialized(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + /// Emits an empty stream as there is no event corresponding to a change + /// in the camera resolution on the web. + /// + /// In order to change the camera resolution a new camera with appropriate + /// [CameraOptions.video] constraints has to be created and initialized. + @override + Stream onCameraResolutionChanged(int cameraId) { + return const Stream.empty(); + } + + @override + Stream onCameraClosing(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onCameraError(int cameraId) { + return _cameraEvents(cameraId).whereType(); + } + + @override + Stream onVideoRecordedEvent(int cameraId) { + return getCamera(cameraId).onVideoRecordedEvent; + } + + @override + Stream onDeviceOrientationChanged() { + final orientation = window?.screen?.orientation; + + if (orientation != null) { + // Create an initial orientation event that emits the device orientation + // as soon as subscribed to this stream. + final initialOrientationEvent = html.Event("change"); + + return orientation.onChange.startWith(initialOrientationEvent).map( + (html.Event _) { + final deviceOrientation = _cameraService + .mapOrientationTypeToDeviceOrientation(orientation.type!); + return DeviceOrientationChangedEvent(deviceOrientation); + }, + ); + } else { + return const Stream.empty(); + } + } + + @override + Future lockCaptureOrientation( + int cameraId, + DeviceOrientation deviceOrientation, + ) async { + try { + final orientation = window?.screen?.orientation; + final documentElement = window?.document.documentElement; + + if (orientation != null && documentElement != null) { + final orientationType = _cameraService + .mapDeviceOrientationToOrientationType(deviceOrientation); + + // Full-screen mode may be required to modify the device orientation. + // See: https://w3c.github.io/screen-orientation/#interaction-with-fullscreen-api + documentElement.requestFullscreen(); + await orientation.lock(orientationType.toString()); + } else { + throw PlatformException( + code: CameraErrorCode.orientationNotSupported.toString(), + message: 'Orientation is not supported in the current browser.', + ); + } + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } + } + + @override + Future unlockCaptureOrientation(int cameraId) async { + try { + final orientation = window?.screen?.orientation; + final documentElement = window?.document.documentElement; + + if (orientation != null && documentElement != null) { + orientation.unlock(); + } else { + throw PlatformException( + code: CameraErrorCode.orientationNotSupported.toString(), + message: 'Orientation is not supported in the current browser.', + ); + } + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } + } + + @override + Future takePicture(int cameraId) { + try { + return getCamera(cameraId).takePicture(); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); + } + } + + @override + Future prepareForVideoRecording() async { + // This is a no-op as it is not required for the web. + } + + @override + Future startVideoRecording(int cameraId, {Duration? maxVideoDuration}) { + try { + final camera = getCamera(cameraId); + + // Add camera's video recording errors to the camera events stream. + // The error event fires when the video recording is not allowed or an unsupported + // codec is used. + _cameraVideoRecordingErrorSubscriptions[cameraId] = + camera.onVideoRecordingError.listen((html.ErrorEvent errorEvent) { + cameraEventStreamController.add( + CameraErrorEvent( + cameraId, + 'Error code: ${errorEvent.type}, error message: ${errorEvent.message}.', + ), + ); + }); + + return camera.startVideoRecording(maxVideoDuration: maxVideoDuration); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); + } + } + + @override + Future stopVideoRecording(int cameraId) async { + try { + final videoRecording = await getCamera(cameraId).stopVideoRecording(); + await _cameraVideoRecordingErrorSubscriptions[cameraId]?.cancel(); + return videoRecording; + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); + } + } + + @override + Future pauseVideoRecording(int cameraId) { + try { + return getCamera(cameraId).pauseVideoRecording(); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); + } + } + + @override + Future resumeVideoRecording(int cameraId) { + try { + return getCamera(cameraId).resumeVideoRecording(); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); + } + } + + @override + Future setFlashMode(int cameraId, FlashMode mode) async { + try { + getCamera(cameraId).setFlashMode(mode); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); + } + } + + @override + Future setExposureMode(int cameraId, ExposureMode mode) { + throw UnimplementedError('setExposureMode() is not implemented.'); + } + + @override + Future setExposurePoint(int cameraId, Point? point) { + throw UnimplementedError('setExposurePoint() is not implemented.'); + } + + @override + Future getMinExposureOffset(int cameraId) { + throw UnimplementedError('getMinExposureOffset() is not implemented.'); + } + + @override + Future getMaxExposureOffset(int cameraId) { + throw UnimplementedError('getMaxExposureOffset() is not implemented.'); + } + + @override + Future getExposureOffsetStepSize(int cameraId) { + throw UnimplementedError('getExposureOffsetStepSize() is not implemented.'); + } + + @override + Future setExposureOffset(int cameraId, double offset) { + throw UnimplementedError('setExposureOffset() is not implemented.'); + } + + @override + Future setFocusMode(int cameraId, FocusMode mode) { + throw UnimplementedError('setFocusMode() is not implemented.'); + } + + @override + Future setFocusPoint(int cameraId, Point? point) { + throw UnimplementedError('setFocusPoint() is not implemented.'); + } + + @override + Future getMaxZoomLevel(int cameraId) async { + try { + return getCamera(cameraId).getMaxZoomLevel(); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); + } + } + + @override + Future getMinZoomLevel(int cameraId) async { + try { + return getCamera(cameraId).getMinZoomLevel(); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); + } + } + + @override + Future setZoomLevel(int cameraId, double zoom) async { + try { + getCamera(cameraId).setZoomLevel(zoom); + } on html.DomException catch (e) { + throw CameraException(e.name, e.message); + } on PlatformException catch (e) { + throw CameraException(e.code, e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw CameraException(e.code.toString(), e.description); + } + } + + @override + Future pausePreview(int cameraId) async { + try { + getCamera(cameraId).pause(); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } + } + + @override + Future resumePreview(int cameraId) async { + try { + await getCamera(cameraId).play(); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } on CameraWebException catch (e) { + _addCameraErrorEvent(e); + throw PlatformException(code: e.code.toString(), message: e.description); + } + } + + @override + Widget buildPreview(int cameraId) { + return HtmlElementView( + viewType: getCamera(cameraId).getViewType(), + ); + } + + @override + Future dispose(int cameraId) async { + try { + await getCamera(cameraId).dispose(); + await _cameraVideoErrorSubscriptions[cameraId]?.cancel(); + await _cameraVideoAbortSubscriptions[cameraId]?.cancel(); + await _cameraEndedSubscriptions[cameraId]?.cancel(); + await _cameraVideoRecordingErrorSubscriptions[cameraId]?.cancel(); + + cameras.remove(cameraId); + _cameraVideoErrorSubscriptions.remove(cameraId); + _cameraVideoAbortSubscriptions.remove(cameraId); + _cameraEndedSubscriptions.remove(cameraId); + } on html.DomException catch (e) { + throw PlatformException(code: e.name, message: e.message); + } + } + + /// Returns a media video stream for the device with the given [deviceId]. + Future _getVideoStreamForDevice( + String deviceId, + ) { + // Create camera options with the desired device id. + final cameraOptions = CameraOptions( + video: VideoConstraints(deviceId: deviceId), + ); + + return _cameraService.getMediaStreamForOptions(cameraOptions); + } + + /// Returns a camera for the given [cameraId]. + /// + /// Throws a [CameraException] if the camera does not exist. + @visibleForTesting + Camera getCamera(int cameraId) { + final camera = cameras[cameraId]; + + if (camera == null) { + throw PlatformException( + code: CameraErrorCode.notFound.toString(), + message: 'No camera found for the given camera id $cameraId.', + ); + } + + return camera; + } + + /// Adds a [CameraErrorEvent], associated with the [exception], + /// to the stream of camera events. + void _addCameraErrorEvent(CameraWebException exception) { + cameraEventStreamController.add( + CameraErrorEvent( + exception.cameraId, + 'Error code: ${exception.code}, error message: ${exception.description}', + ), + ); + } +} diff --git a/packages/camera/camera_web/lib/src/shims/dart_js_util.dart b/packages/camera/camera_web/lib/src/shims/dart_js_util.dart new file mode 100644 index 000000000000..6601bec6f529 --- /dev/null +++ b/packages/camera/camera_web/lib/src/shims/dart_js_util.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 'dart:js_util' as js_util; + +/// A utility that shims dart:js_util to manipulate JavaScript interop objects. +class JsUtil { + /// Returns true if the object [o] has the property [name]. + bool hasProperty(Object o, Object name) => js_util.hasProperty(o, name); + + /// Returns the value of the property [name] in the object [o]. + dynamic getProperty(Object o, Object name) => js_util.getProperty(o, name); +} diff --git a/packages/camera/camera_web/lib/src/shims/dart_ui.dart b/packages/camera/camera_web/lib/src/shims/dart_ui.dart new file mode 100644 index 000000000000..5eacec5fe867 --- /dev/null +++ b/packages/camera/camera_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/camera/camera_web/lib/src/shims/dart_ui_fake.dart b/packages/camera/camera_web/lib/src/shims/dart_ui_fake.dart new file mode 100644 index 000000000000..f2862af8b704 --- /dev/null +++ b/packages/camera/camera_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/camera/camera_web/lib/src/shims/dart_ui_real.dart b/packages/camera/camera_web/lib/src/shims/dart_ui_real.dart new file mode 100644 index 000000000000..276b768c76c5 --- /dev/null +++ b/packages/camera/camera_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/camera/camera_web/lib/src/types/camera_error_code.dart b/packages/camera/camera_web/lib/src/types/camera_error_code.dart new file mode 100644 index 000000000000..f70925b4bede --- /dev/null +++ b/packages/camera/camera_web/lib/src/types/camera_error_code.dart @@ -0,0 +1,95 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:html' as html; + +/// Error codes that may occur during the camera initialization, +/// configuration or video streaming. +class CameraErrorCode { + const CameraErrorCode._(this._type); + + final String _type; + + @override + String toString() => _type; + + /// The camera is not supported. + static const CameraErrorCode notSupported = + CameraErrorCode._('cameraNotSupported'); + + /// The camera is not found. + static const CameraErrorCode notFound = CameraErrorCode._('cameraNotFound'); + + /// The camera is not readable. + static const CameraErrorCode notReadable = + CameraErrorCode._('cameraNotReadable'); + + /// The camera options are impossible to satisfy. + static const CameraErrorCode overconstrained = + CameraErrorCode._('cameraOverconstrained'); + + /// The camera cannot be used or the permission + /// to access the camera is not granted. + static const CameraErrorCode permissionDenied = + CameraErrorCode._('cameraPermission'); + + /// The camera options are incorrect or attempted + /// to access the media input from an insecure context. + static const CameraErrorCode type = CameraErrorCode._('cameraType'); + + /// Some problem occurred that prevented the camera from being used. + static const CameraErrorCode abort = CameraErrorCode._('cameraAbort'); + + /// The user media support is disabled in the current browser. + static const CameraErrorCode security = CameraErrorCode._('cameraSecurity'); + + /// The camera metadata is missing. + static const CameraErrorCode missingMetadata = + CameraErrorCode._('cameraMissingMetadata'); + + /// The camera orientation is not supported. + static const CameraErrorCode orientationNotSupported = + CameraErrorCode._('orientationNotSupported'); + + /// The camera torch mode is not supported. + static const CameraErrorCode torchModeNotSupported = + CameraErrorCode._('torchModeNotSupported'); + + /// The camera zoom level is not supported. + static const CameraErrorCode zoomLevelNotSupported = + CameraErrorCode._('zoomLevelNotSupported'); + + /// The camera zoom level is invalid. + static const CameraErrorCode zoomLevelInvalid = + CameraErrorCode._('zoomLevelInvalid'); + + /// The camera has not been initialized or started. + static const CameraErrorCode notStarted = + CameraErrorCode._('cameraNotStarted'); + + /// The video recording was not started. + static const CameraErrorCode videoRecordingNotStarted = + CameraErrorCode._('videoRecordingNotStarted'); + + /// An unknown camera error. + static const CameraErrorCode unknown = CameraErrorCode._('cameraUnknown'); + + /// Returns a camera error code based on the media error. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code + static CameraErrorCode fromMediaError(html.MediaError error) { + switch (error.code) { + case html.MediaError.MEDIA_ERR_ABORTED: + return CameraErrorCode._('mediaErrorAborted'); + case html.MediaError.MEDIA_ERR_NETWORK: + return CameraErrorCode._('mediaErrorNetwork'); + case html.MediaError.MEDIA_ERR_DECODE: + return CameraErrorCode._('mediaErrorDecode'); + case html.MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED: + return CameraErrorCode._('mediaErrorSourceNotSupported'); + default: + return CameraErrorCode._('mediaErrorUnknown'); + } + } +} diff --git a/packages/camera/camera_web/lib/src/types/camera_metadata.dart b/packages/camera/camera_web/lib/src/types/camera_metadata.dart new file mode 100644 index 000000000000..c9998e58a52c --- /dev/null +++ b/packages/camera/camera_web/lib/src/types/camera_metadata.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 'dart:ui' show hashValues; + +/// Metadata used along the camera description +/// to store additional web-specific camera details. +class CameraMetadata { + /// Creates a new instance of [CameraMetadata] + /// with the given [deviceId] and [facingMode]. + const CameraMetadata({required this.deviceId, required this.facingMode}); + + /// Uniquely identifies the camera device. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/deviceId + final String deviceId; + + /// Describes the direction the camera is facing towards. + /// May be `user`, `environment`, `left`, `right` + /// or null if the facing mode is not available. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackSettings/facingMode + final String? facingMode; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is CameraMetadata && + other.deviceId == deviceId && + other.facingMode == facingMode; + } + + @override + int get hashCode => hashValues(deviceId.hashCode, facingMode.hashCode); +} diff --git a/packages/camera/camera_web/lib/src/types/camera_options.dart b/packages/camera/camera_web/lib/src/types/camera_options.dart new file mode 100644 index 000000000000..2a4cdbf15348 --- /dev/null +++ b/packages/camera/camera_web/lib/src/types/camera_options.dart @@ -0,0 +1,245 @@ +// Copyright 2013 The Flutter Authors. All 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; + +/// Options used to create a camera with the given +/// [audio] and [video] media constraints. +/// +/// These options represent web `MediaStreamConstraints` +/// and can be used to request the browser for media streams +/// with audio and video tracks containing the requested types of media. +/// +/// https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints +class CameraOptions { + /// Creates a new instance of [CameraOptions] + /// with the given [audio] and [video] constraints. + const CameraOptions({ + AudioConstraints? audio, + VideoConstraints? video, + }) : audio = audio ?? const AudioConstraints(), + video = video ?? const VideoConstraints(); + + /// The audio constraints for the camera. + final AudioConstraints audio; + + /// The video constraints for the camera. + final VideoConstraints video; + + /// Converts the current instance to a Map. + Map toJson() { + return { + 'audio': audio.toJson(), + 'video': video.toJson(), + }; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is CameraOptions && + other.audio == audio && + other.video == video; + } + + @override + int get hashCode => hashValues(audio, video); +} + +/// Indicates whether the audio track is requested. +/// +/// By default, the audio track is not requested. +class AudioConstraints { + /// Creates a new instance of [AudioConstraints] + /// with the given [enabled] constraint. + const AudioConstraints({this.enabled = false}); + + /// Whether the audio track should be enabled. + final bool enabled; + + /// Converts the current instance to a Map. + Object toJson() => enabled; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is AudioConstraints && other.enabled == enabled; + } + + @override + int get hashCode => enabled.hashCode; +} + +/// Defines constraints that the video track must have +/// to be considered acceptable. +class VideoConstraints { + /// Creates a new instance of [VideoConstraints] + /// with the given constraints. + const VideoConstraints({ + this.facingMode, + this.width, + this.height, + this.deviceId, + }); + + /// The facing mode of the video track. + final FacingModeConstraint? facingMode; + + /// The width of the video track. + final VideoSizeConstraint? width; + + /// The height of the video track. + final VideoSizeConstraint? height; + + /// The device id of the video track. + final String? deviceId; + + /// Converts the current instance to a Map. + Object toJson() { + final json = {}; + + if (width != null) json['width'] = width!.toJson(); + if (height != null) json['height'] = height!.toJson(); + if (facingMode != null) json['facingMode'] = facingMode!.toJson(); + if (deviceId != null) json['deviceId'] = {'exact': deviceId!}; + + return json; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is VideoConstraints && + other.facingMode == facingMode && + other.width == width && + other.height == height && + other.deviceId == deviceId; + } + + @override + int get hashCode => hashValues(facingMode, width, height, deviceId); +} + +/// The camera type used in [FacingModeConstraint]. +/// +/// Specifies whether the requested camera should be facing away +/// or toward the user. +class CameraType { + const CameraType._(this._type); + + final String _type; + + @override + String toString() => _type; + + /// The camera is facing away from the user, viewing their environment. + /// This includes the back camera on a smartphone. + static const CameraType environment = CameraType._('environment'); + + /// The camera is facing toward the user. + /// This includes the front camera on a smartphone. + static const CameraType user = CameraType._('user'); +} + +/// Indicates the direction in which the desired camera should be pointing. +class FacingModeConstraint { + /// Creates a new instance of [FacingModeConstraint] + /// with the given [ideal] and [exact] constraints. + const FacingModeConstraint._({this.ideal, this.exact}); + + /// Creates a new instance of [FacingModeConstraint] + /// with [ideal] constraint set to [type]. + factory FacingModeConstraint(CameraType type) => + FacingModeConstraint._(ideal: type); + + /// Creates a new instance of [FacingModeConstraint] + /// with [exact] constraint set to [type]. + factory FacingModeConstraint.exact(CameraType type) => + FacingModeConstraint._(exact: type); + + /// The ideal facing mode constraint. + /// + /// If this constraint is used, then the camera would ideally have + /// the desired facing [type] but it may be considered optional. + final CameraType? ideal; + + /// The exact facing mode constraint. + /// + /// If this constraint is used, then the camera must have + /// the desired facing [type] to be considered acceptable. + final CameraType? exact; + + /// Converts the current instance to a Map. + Object? toJson() { + return { + if (ideal != null) 'ideal': ideal.toString(), + if (exact != null) 'exact': exact.toString(), + }; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is FacingModeConstraint && + other.ideal == ideal && + other.exact == exact; + } + + @override + int get hashCode => hashValues(ideal, exact); +} + +/// The size of the requested video track used in +/// [VideoConstraints.width] and [VideoConstraints.height]. +/// +/// The obtained video track will have a size between [minimum] and [maximum] +/// with ideally a size of [ideal]. The size is determined by +/// the capabilities of the hardware and the other specified constraints. +class VideoSizeConstraint { + /// Creates a new instance of [VideoSizeConstraint] with the given + /// [minimum], [ideal] and [maximum] constraints. + const VideoSizeConstraint({this.minimum, this.ideal, this.maximum}); + + /// The minimum video size. + final int? minimum; + + /// The ideal video size. + /// + /// The video would ideally have the [ideal] size + /// but it may be considered optional. If not possible + /// to satisfy, the size will be as close as possible + /// to [ideal]. + final int? ideal; + + /// The maximum video size. + final int? maximum; + + /// Converts the current instance to a Map. + Object toJson() { + final json = {}; + + if (ideal != null) json['ideal'] = ideal; + if (minimum != null) json['min'] = minimum; + if (maximum != null) json['max'] = maximum; + + return json; + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is VideoSizeConstraint && + other.minimum == minimum && + other.ideal == ideal && + other.maximum == maximum; + } + + @override + int get hashCode => hashValues(minimum, ideal, maximum); +} diff --git a/packages/camera/camera_web/lib/src/types/camera_web_exception.dart b/packages/camera/camera_web/lib/src/types/camera_web_exception.dart new file mode 100644 index 000000000000..c21106cc462e --- /dev/null +++ b/packages/camera/camera_web/lib/src/types/camera_web_exception.dart @@ -0,0 +1,29 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:camera_web/src/types/types.dart'; + +/// An exception thrown when the camera with id [cameraId] reports +/// an initialization, configuration or video streaming error, +/// or enters into an unexpected state. +/// +/// This error should be emitted on the `onCameraError` stream +/// of the camera platform. +class CameraWebException implements Exception { + /// Creates a new instance of [CameraWebException] + /// with the given error [cameraId], [code] and [description]. + CameraWebException(this.cameraId, this.code, this.description); + + /// The id of the camera this exception is associated to. + int cameraId; + + /// The error code of this exception. + CameraErrorCode code; + + /// The description of this exception. + String description; + + @override + String toString() => 'CameraWebException($cameraId, $code, $description)'; +} diff --git a/packages/camera/camera_web/lib/src/types/media_device_kind.dart b/packages/camera/camera_web/lib/src/types/media_device_kind.dart new file mode 100644 index 000000000000..1f746808df9e --- /dev/null +++ b/packages/camera/camera_web/lib/src/types/media_device_kind.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. + +/// A kind of a media device. +/// +/// See: https://developer.mozilla.org/en-US/docs/Web/API/MediaDeviceInfo/kind +abstract class MediaDeviceKind { + /// A video input media device kind. + static const videoInput = 'videoinput'; + + /// An audio input media device kind. + static const audioInput = 'audioinput'; + + /// An audio output media device kind. + static const audioOutput = 'audiooutput'; +} diff --git a/packages/camera/camera_web/lib/src/types/orientation_type.dart b/packages/camera/camera_web/lib/src/types/orientation_type.dart new file mode 100644 index 000000000000..717f5f399541 --- /dev/null +++ b/packages/camera/camera_web/lib/src/types/orientation_type.dart @@ -0,0 +1,26 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; + +/// A screen orientation type. +/// +/// See: https://developer.mozilla.org/en-US/docs/Web/API/ScreenOrientation/type +abstract class OrientationType { + /// The primary portrait mode orientation. + /// Corresponds to [DeviceOrientation.portraitUp]. + static const String portraitPrimary = 'portrait-primary'; + + /// The secondary portrait mode orientation. + /// Corresponds to [DeviceOrientation.portraitSecondary]. + static const String portraitSecondary = 'portrait-secondary'; + + /// The primary landscape mode orientation. + /// Corresponds to [DeviceOrientation.landscapeLeft]. + static const String landscapePrimary = 'landscape-primary'; + + /// The secondary landscape mode orientation. + /// Corresponds to [DeviceOrientation.landscapeRight]. + static const String landscapeSecondary = 'landscape-secondary'; +} diff --git a/packages/camera/camera_web/lib/src/types/types.dart b/packages/camera/camera_web/lib/src/types/types.dart new file mode 100644 index 000000000000..72d7fb85af14 --- /dev/null +++ b/packages/camera/camera_web/lib/src/types/types.dart @@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +export 'camera_error_code.dart'; +export 'camera_metadata.dart'; +export 'camera_options.dart'; +export 'camera_web_exception.dart'; +export 'media_device_kind.dart'; +export 'orientation_type.dart'; +export 'zoom_level_capability.dart'; diff --git a/packages/camera/camera_web/lib/src/types/zoom_level_capability.dart b/packages/camera/camera_web/lib/src/types/zoom_level_capability.dart new file mode 100644 index 000000000000..ace57140d956 --- /dev/null +++ b/packages/camera/camera_web/lib/src/types/zoom_level_capability.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:html' as html; +import 'dart:ui' show hashValues; + +/// The possible range of values for the zoom level configurable +/// on the camera video track. +class ZoomLevelCapability { + /// Creates a new instance of [ZoomLevelCapability] with the given + /// zoom level range of [minimum] to [maximum] configurable + /// on the [videoTrack]. + ZoomLevelCapability({ + required this.minimum, + required this.maximum, + required this.videoTrack, + }); + + /// The zoom level constraint name. + /// See: https://w3c.github.io/mediacapture-image/#dom-mediatracksupportedconstraints-zoom + static const constraintName = "zoom"; + + /// The minimum zoom level. + final double minimum; + + /// The maximum zoom level. + final double maximum; + + /// The video track capable of configuring the zoom level. + final html.MediaStreamTrack videoTrack; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is ZoomLevelCapability && + other.minimum == minimum && + other.maximum == maximum && + other.videoTrack == videoTrack; + } + + @override + int get hashCode => hashValues(minimum, maximum, videoTrack); +} diff --git a/packages/camera/camera_web/pubspec.yaml b/packages/camera/camera_web/pubspec.yaml new file mode 100644 index 000000000000..f37500ad6e22 --- /dev/null +++ b/packages/camera/camera_web/pubspec.yaml @@ -0,0 +1,30 @@ +name: camera_web +description: A Flutter plugin for getting information about and controlling the camera on Web. +repository: https://github.com/flutter/plugins/tree/master/packages/camera/camera_web +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+camera%22 +version: 0.2.1+1 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +flutter: + plugin: + implements: camera + platforms: + web: + pluginClass: CameraPlugin + fileName: camera_web.dart + +dependencies: + camera_platform_interface: ^2.1.0 + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + stream_transform: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + pedantic: ^1.11.1 diff --git a/packages/camera/camera_web/test/README.md b/packages/camera/camera_web/test/README.md new file mode 100644 index 000000000000..7c5b4ad682ba --- /dev/null +++ b/packages/camera/camera_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/camera/camera_web/test/more_tests_exist_elsewhere_test.dart b/packages/camera/camera_web/test/more_tests_exist_elsewhere_test.dart new file mode 100644 index 000000000000..dc2b64c111d7 --- /dev/null +++ b/packages/camera/camera_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 more tests', () { + print('---'); + print('This package also uses integration_test for its tests.'); + print('See `example/README.md` for more info.'); + print('---'); + }); +} diff --git a/packages/camera/example/android.iml b/packages/camera/example/android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/camera/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - 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 95b5f4373b62..000000000000 --- a/packages/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.flutter.plugins.cameraexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -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/MainActivityTest.java b/packages/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/MainActivityTest.java deleted file mode 100644 index 5d1b95578dc0..000000000000 --- a/packages/camera/example/android/app/src/androidTestDebug/java/io/flutter/plugins/cameraexample/MainActivityTest.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.flutter.plugins.cameraexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class MainActivityTest { - @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.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 aad8d98bfa27..000000000000 --- a/packages/camera/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,39 +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 9e86560d3ff4..000000000000 --- a/packages/camera/example/android/app/src/main/java/io/flutter/plugins/cameraexample/EmbeddingV1Activity.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.flutter.plugins.cameraexample; - -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; - -public class EmbeddingV1Activity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); - } -} diff --git a/packages/camera/example/android/app/src/main/java/io/flutter/plugins/cameraexample/MainActivity.java b/packages/camera/example/android/app/src/main/java/io/flutter/plugins/cameraexample/MainActivity.java deleted file mode 100644 index bbe9e45be2db..000000000000 --- a/packages/camera/example/android/app/src/main/java/io/flutter/plugins/cameraexample/MainActivity.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.flutter.plugins.cameraexample; - -import androidx.annotation.NonNull; -import io.flutter.embedding.android.FlutterActivity; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistry; -import io.flutter.plugins.camera.CameraPlugin; -import io.flutter.plugins.pathprovider.PathProviderPlugin; -import io.flutter.plugins.videoplayer.VideoPlayerPlugin; - -public class MainActivity extends FlutterActivity { - @Override - public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { - flutterEngine.getPlugins().add(new CameraPlugin()); - - ShimPluginRegistry shimPluginRegistry = new ShimPluginRegistry(flutterEngine); - PathProviderPlugin.registerWith( - shimPluginRegistry.registrarFor("io.flutter.plugins.pathprovider.PathProviderPlugin")); - VideoPlayerPlugin.registerWith( - shimPluginRegistry.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/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 a066129eebd4..000000000000 --- a/packages/camera/example/pubspec.yaml +++ /dev/null @@ -1,25 +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 - e2e: "^0.2.0" - -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/camera_e2e.dart b/packages/camera/example/test_driver/camera_e2e.dart deleted file mode 100644 index a1cc8ad9ca02..000000000000 --- a/packages/camera/example/test_driver/camera_e2e.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:e2e/e2e.dart'; - -void main() { - Directory testDir; - - E2EWidgetsFlutterBinding.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/test_driver/camera_e2e_test.dart b/packages/camera/example/test_driver/camera_e2e_test.dart deleted file mode 100644 index 4963854dea72..000000000000 --- a/packages/camera/example/test_driver/camera_e2e_test.dart +++ /dev/null @@ -1,55 +0,0 @@ -import 'dart:async'; -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 result = - 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' - ]); - exit(result == 'pass' ? 0 : 1); -} diff --git a/packages/camera/ios/Classes/CameraPlugin.m b/packages/camera/ios/Classes/CameraPlugin.m deleted file mode 100644 index 42cdb6d5fdf9..000000000000 --- a/packages/camera/ios/Classes/CameraPlugin.m +++ /dev/null @@ -1,904 +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; - -- initWithPath:(NSString *)filename - result:(FlutterResult)result - motionManager:(CMMotionManager *)motionManager - cameraPosition:(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 { - 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)(); -@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; -@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; -- (instancetype)initWithCameraName:(NSString *)cameraName - resolutionPreset:(NSString *)resolutionPreset - enableAudio:(BOOL)enableAudio - dispatchQueue:(dispatch_queue_t)dispatchQueue - error:(NSError **)error; - -- (void)start; -- (void)stop; -- (void)startVideoRecordingAtPath:(NSString *)path result:(FlutterResult)result; -- (void)stopVideoRecordingWithResult:(FlutterResult)result; -- (void)startImageStreamWithMessenger:(NSObject *)messenger; -- (void)stopImageStream; -- (void)captureToFile:(NSString *)filename result:(FlutterResult)result; -@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]; - _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 { - 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: - if ([_captureSession canSetSessionPreset:AVCaptureSessionPresetHigh]) { - _captureSession.sessionPreset = AVCaptureSessionPresetHigh; - _previewSize = - CGSizeMake(_captureDevice.activeFormat.highResolutionStillImageDimensions.width, - _captureDevice.activeFormat.highResolutionStillImageDimensions.height); - break; - } - case ultraHigh: - if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset3840x2160]) { - _captureSession.sessionPreset = AVCaptureSessionPreset3840x2160; - _previewSize = CGSizeMake(3840, 2160); - 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 { - 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]) { - 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 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; - cam.onFrameAvailable = ^{ - [_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]) { - [_camera captureToFile:call.arguments[@"path"] result:result]; - } 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/ios/camera.podspec b/packages/camera/ios/camera.podspec deleted file mode 100644 index dfe566ca79cc..000000000000 --- a/packages/camera/ios/camera.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 = 'camera' - s.version = '0.0.1' - s.summary = 'A new flutter plugin project.' - s.description = <<-DESC -A new flutter plugin project. - DESC - s.homepage = 'http://example.com' - s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } - s.source = { :path => '.' } - 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/**/*' - end -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 6d70ed7439ff..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+1 - -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 9325f1a7868f..932565842efd 100644 --- a/packages/connectivity/connectivity/CHANGELOG.md +++ b/packages/connectivity/connectivity/CHANGELOG.md @@ -1,3 +1,98 @@ +## NEXT + +* Remove references to the Android V1 embedding. +* Updated Android lint settings. +* Specify Java 8 for Android build. + +## 3.0.6 + +* Update README to point to Plus Plugins version. + +## 3.0.5 + +* Ignore Reachability pointer to int cast warning. + +## 3.0.4 + +* Migrate maven repository from jcenter to mavenCentral. + +## 3.0.3 + +* Re-endorse connectivity_for_web + +## 3.0.2 + +* Update platform_plugin_interface version requirement. + +## 3.0.1 + +* Migrate tests to null safety. + +## 3.0.0 + +* Migrate to null safety. +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) +* Android: Cleanup the NetworkCallback object when a connectivity stream is cancelled + +## 2.0.3 + +* Update Flutter SDK constraint. + +## 2.0.2 + +* Android: Fix IllegalArgumentException. +* Android: Update Example project. + +## 2.0.1 + +* Remove unused `test` dependency. +* Update Dart SDK constraint in example. + +## 2.0.0 + +* [Breaking Change] The `getWifiName`, `getWifiBSSID` and `getWifiIP` are removed to [wifi_info_flutter](https://github.com/flutter/plugins/tree/master/packages/wifi_info_flutter) +* Migration guide: + + If you don't use any of the above APIs, your code should work as is. In addition, you can also remove `NSLocationAlwaysAndWhenInUseUsageDescription` and `NSLocationWhenInUseUsageDescription` in `ios/Runner/Info.plist` + + If you use any of the above APIs, you can find the same APIs in the [wifi_info_flutter](https://github.com/flutter/plugins/tree/master/packages/wifi_info_flutter/wifi_info_flutter) plugin. + For example, to migrate `getWifiName`, use the new plugin: + ```dart + final WifiInfo _wifiInfo = WifiInfo(); + final String wifiName = await _wifiInfo.getWifiName(); + ``` + +## 1.0.0 + +* Mark wifi related code deprecated. +* Announce 1.0.0! + +## 0.4.9+5 + +* Update android compileSdkVersion to 29. + +## 0.4.9+4 + +* Update README with the updated information about WifiInfo on Android O or higher. +* Android: Avoiding uses or overrides a deprecated API + +## 0.4.9+3 + +* Keep handling deprecated Android v1 classes for backward compatibility. + +## 0.4.9+2 + +* Update package:e2e to use package:integration_test + +## 0.4.9+1 + +* Update package:e2e reference to use the local version in the flutter/plugins + repository. + +## 0.4.9 + +* Add support for `web` (by endorsing `connectivity_for_web` 0.3.0) + ## 0.4.8+6 * Update lower bound of dart dependency to 2.1.0. diff --git a/packages/connectivity/connectivity/LICENSE b/packages/connectivity/connectivity/LICENSE index c89293372cf3..c6823b81eb84 100644 --- a/packages/connectivity/connectivity/LICENSE +++ b/packages/connectivity/connectivity/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/connectivity/connectivity/README.md b/packages/connectivity/connectivity/README.md index f51be070d8f5..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,53 +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. -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 b9b147b29b77..e1ba0c7c892e 100644 --- a/packages/connectivity/connectivity/android/build.gradle +++ b/packages/connectivity/connectivity/android/build.gradle @@ -1,28 +1,33 @@ group 'io.flutter.plugins.connectivity' 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 @@ -30,5 +35,23 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } } diff --git a/packages/connectivity/connectivity/android/gradle.properties b/packages/connectivity/connectivity/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/connectivity/connectivity/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M 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 2d07641a42c1..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"; } @@ -100,4 +57,8 @@ private String getNetworkTypeLegacy() { return "none"; } } + + public ConnectivityManager getConnectivityManager() { + return connectivityManager; + } } diff --git a/packages/connectivity/connectivity/android/src/main/java/io/flutter/plugins/connectivity/ConnectivityBroadcastReceiver.java b/packages/connectivity/connectivity/android/src/main/java/io/flutter/plugins/connectivity/ConnectivityBroadcastReceiver.java index be8b47eff944..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. @@ -9,6 +9,10 @@ import android.content.Intent; import android.content.IntentFilter; import android.net.ConnectivityManager; +import android.net.Network; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; import io.flutter.plugin.common.EventChannel; /** @@ -19,13 +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; } @@ -33,12 +40,35 @@ class ConnectivityBroadcastReceiver extends BroadcastReceiver @Override public void onListen(Object arguments, EventChannel.EventSink events) { this.events = events; - context.registerReceiver(this, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + networkCallback = + new ConnectivityManager.NetworkCallback() { + @Override + public void onAvailable(Network network) { + sendEvent(); + } + + @Override + public void onLost(Network network) { + sendEvent(); + } + }; + connectivity.getConnectivityManager().registerDefaultNetworkCallback(networkCallback); + } else { + context.registerReceiver(this, new IntentFilter(CONNECTIVITY_ACTION)); + } } @Override public void onCancel(Object arguments) { - context.unregisterReceiver(this); + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (networkCallback != null) { + connectivity.getConnectivityManager().unregisterNetworkCallback(networkCallback); + networkCallback = null; + } + } else { + context.unregisterReceiver(this); + } } @Override @@ -47,4 +77,19 @@ public void onReceive(Context context, Intent intent) { events.success(connectivity.getNetworkType()); } } + + public ConnectivityManager.NetworkCallback getNetworkCallback() { + return networkCallback; + } + + private void sendEvent() { + Runnable runnable = + new Runnable() { + @Override + public void run() { + events.success(connectivity.getNetworkType()); + } + }; + mainHandler.post(runnable); + } } diff --git a/packages/connectivity/connectivity/android/src/main/java/io/flutter/plugins/connectivity/ConnectivityMethodChannelHandler.java b/packages/connectivity/connectivity/android/src/main/java/io/flutter/plugins/connectivity/ConnectivityMethodChannelHandler.java 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 b458fa97e203..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,12 +6,10 @@ 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; import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.PluginRegistry.Registrar; /** ConnectivityPlugin */ public class ConnectivityPlugin implements FlutterPlugin { @@ -20,7 +18,8 @@ public class ConnectivityPlugin implements FlutterPlugin { private EventChannel eventChannel; /** Plugin registration. */ - public static void registerWith(Registrar registrar) { + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { ConnectivityPlugin plugin = new ConnectivityPlugin(); plugin.setupChannels(registrar.messenger(), registrar.context()); @@ -41,9 +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.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/AndroidManifest.xml b/packages/connectivity/connectivity/example/android/app/src/main/AndroidManifest.xml index 902642e0ca49..abce0da89989 100644 --- a/packages/connectivity/connectivity/example/android/app/src/main/AndroidManifest.xml +++ b/packages/connectivity/connectivity/example/android/app/src/main/AndroidManifest.xml @@ -3,15 +3,7 @@ - - - + rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} 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 35391fd5e0a0..b4a67622f8dc 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,16 +1,18 @@ -// 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.connectivityexample; import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; +import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; -@RunWith(FlutterRunner.class) +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) public class FlutterActivityTest { @Rule public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); diff --git a/packages/connectivity/connectivity/example/android/app/src/test/java/io/flutter/plugins/connectivityexample/ActivityTest.java b/packages/connectivity/connectivity/example/android/app/src/test/java/io/flutter/plugins/connectivityexample/ActivityTest.java 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..b653a1f3b889 100644 --- a/packages/connectivity/connectivity/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/connectivity/connectivity/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,10 +9,6 @@ /* 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 */; }; @@ -28,8 +24,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -41,14 +35,12 @@ 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 = ""; }; @@ -63,8 +55,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, EB0BA966000B5C35B13186D7 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -92,9 +82,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, @@ -159,7 +147,6 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 6A2F146AD353BE7A0C3E797E /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -177,7 +164,7 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; + ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; @@ -229,7 +216,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"; }; 3BAF367E8BACBC7576CEE653 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; @@ -249,21 +236,6 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 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; @@ -315,7 +287,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 +343,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -437,7 +407,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 +428,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.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/connectivity/connectivity/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16ed0f..919434a6254f 100644 --- a/packages/connectivity/connectivity/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/packages/connectivity/connectivity/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> 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 4ad30972679a..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(); @@ -91,7 +91,7 @@ class _MyHomePageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('Plugin example app'), + title: const Text('Connectivity example app'), ), body: Center(child: Text('Connection Status: $_connectionStatus')), ); @@ -100,66 +100,6 @@ class _MyHomePageState extends State { Future _updateConnectionStatus(ConnectivityResult result) async { switch (result) { case ConnectivityResult.wifi: - String wifiName, wifiBSSID, wifiIP; - - try { - if (Platform.isIOS) { - LocationAuthorizationStatus status = - await _connectivity.getLocationServiceAuthorization(); - if (status == LocationAuthorizationStatus.notDetermined) { - status = - await _connectivity.requestLocationServiceAuthorization(); - } - if (status == LocationAuthorizationStatus.authorizedAlways || - status == LocationAuthorizationStatus.authorizedWhenInUse) { - wifiName = await _connectivity.getWifiName(); - } else { - wifiName = await _connectivity.getWifiName(); - } - } else { - wifiName = await _connectivity.getWifiName(); - } - } on PlatformException catch (e) { - print(e.toString()); - wifiName = "Failed to get Wifi Name"; - } - - try { - if (Platform.isIOS) { - LocationAuthorizationStatus status = - await _connectivity.getLocationServiceAuthorization(); - if (status == LocationAuthorizationStatus.notDetermined) { - status = - await _connectivity.requestLocationServiceAuthorization(); - } - if (status == LocationAuthorizationStatus.authorizedAlways || - status == LocationAuthorizationStatus.authorizedWhenInUse) { - wifiBSSID = await _connectivity.getWifiBSSID(); - } else { - wifiBSSID = await _connectivity.getWifiBSSID(); - } - } else { - wifiBSSID = await _connectivity.getWifiBSSID(); - } - } on PlatformException catch (e) { - print(e.toString()); - wifiBSSID = "Failed to get Wifi BSSID"; - } - - try { - wifiIP = await _connectivity.getWifiIP(); - } on PlatformException catch (e) { - print(e.toString()); - wifiIP = "Failed to get Wifi IP"; - } - - setState(() { - _connectionStatus = '$result\n' - 'Wifi Name: $wifiName\n' - 'Wifi BSSID: $wifiBSSID\n' - 'Wifi IP: $wifiIP\n'; - }); - break; case ConnectivityResult.mobile: case ConnectivityResult.none: setState(() => _connectionStatus = result.toString()); 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 a16e60442736..1707d3482a98 100644 --- a/packages/connectivity/connectivity/example/pubspec.yaml +++ b/packages/connectivity/connectivity/example/pubspec.yaml @@ -1,18 +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 - e2e: ^0.2.0 - pedantic: ^1.8.0 + test: ^1.16.3 + integration_test: + 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/test/connectivity_e2e.dart b/packages/connectivity/connectivity/example/test_driver/test/connectivity_e2e.dart deleted file mode 100644 index 10c4bda34e0d..000000000000 --- a/packages/connectivity/connectivity/example/test_driver/test/connectivity_e2e.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:e2e/e2e.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:connectivity/connectivity.dart'; - -void main() { - E2EWidgetsFlutterBinding.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/connectivity_e2e_test.dart b/packages/connectivity/connectivity/example/test_driver/test/connectivity_e2e_test.dart deleted file mode 100644 index 84b7ae6a47ab..000000000000 --- a/packages/connectivity/connectivity/example/test_driver/test/connectivity_e2e_test.dart +++ /dev/null @@ -1,14 +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:flutter_driver/flutter_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); -} diff --git a/packages/connectivity/connectivity/example/web/favicon.png b/packages/connectivity/connectivity/example/web/favicon.png new file mode 100644 index 000000000000..8aaa46ac1ae2 Binary files /dev/null and b/packages/connectivity/connectivity/example/web/favicon.png differ diff --git a/packages/connectivity/connectivity/example/web/icons/Icon-192.png b/packages/connectivity/connectivity/example/web/icons/Icon-192.png new file mode 100644 index 000000000000..b749bfef0747 Binary files /dev/null and b/packages/connectivity/connectivity/example/web/icons/Icon-192.png differ diff --git a/packages/connectivity/connectivity/example/web/icons/Icon-512.png b/packages/connectivity/connectivity/example/web/icons/Icon-512.png new file mode 100644 index 000000000000..88cfd48dff11 Binary files /dev/null and b/packages/connectivity/connectivity/example/web/icons/Icon-512.png differ diff --git a/packages/connectivity/connectivity/example/web/index.html b/packages/connectivity/connectivity/example/web/index.html new file mode 100644 index 000000000000..c6fa1623be95 --- /dev/null +++ b/packages/connectivity/connectivity/example/web/index.html @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + example + + + + + + + + diff --git a/packages/connectivity/connectivity/example/web/manifest.json b/packages/connectivity/connectivity/example/web/manifest.json new file mode 100644 index 000000000000..8c012917dab7 --- /dev/null +++ b/packages/connectivity/connectivity/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/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 9aaa2620f82c..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.8+6 +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: @@ -17,25 +19,22 @@ flutter: pluginClass: FLTConnectivityPlugin macos: default_package: connectivity_macos + web: + default_package: connectivity_for_web dependencies: flutter: sdk: flutter - meta: "^1.0.5" - connectivity_platform_interface: ^1.0.2 - connectivity_macos: ^0.1.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 - e2e: ^0.2.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" + plugin_platform_interface: ^2.0.0 + pedantic: ^1.10.0 + test: ^1.16.3 diff --git a/packages/connectivity/connectivity/test/connectivity_e2e.dart b/packages/connectivity/connectivity/test/connectivity_e2e.dart deleted file mode 100644 index 10c4bda34e0d..000000000000 --- a/packages/connectivity/connectivity/test/connectivity_e2e.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:e2e/e2e.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:connectivity/connectivity.dart'; - -void main() { - E2EWidgetsFlutterBinding.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/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/.gitignore b/packages/connectivity/connectivity_for_web/.gitignore new file mode 100644 index 000000000000..d7dee828a6b9 --- /dev/null +++ b/packages/connectivity/connectivity_for_web/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ +lib/generated_plugin_registrant.dart diff --git a/packages/connectivity/connectivity_for_web/.metadata b/packages/connectivity/connectivity_for_web/.metadata new file mode 100644 index 000000000000..23eb55ba6da2 --- /dev/null +++ b/packages/connectivity/connectivity_for_web/.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: 52ee8a6c6565cd421dfa32042941eb40691f4746 + channel: master + +project_type: plugin 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 new file mode 100644 index 000000000000..97e5032c8dd4 --- /dev/null +++ b/packages/connectivity/connectivity_for_web/CHANGELOG.md @@ -0,0 +1,41 @@ +## 0.4.0+1 + +* Add `implements` to pubspec. + +## 0.4.0 + +* Migrate to null-safety +* Run tests using flutter driver + +## 0.3.1+4 + +* Remove unused `test` dependency. + +## 0.3.1+3 + +* Fix homepage in `pubspec.yaml`. + +## 0.3.1+2 + +* Update package:e2e to use package:integration_test + +## 0.3.1+1 + +* Update package:e2e reference to use the local version in the flutter/plugins + repository. + +## 0.3.1 + +* Use NetworkInformation API from dart:html, instead of the JS-interop version. + +## 0.3.0 + +* Rename from "experimental_connectivity_web" to "connectivity_for_web", and move to flutter/plugins master. + +## 0.2.0 + +* Add fallback on dart:html for browsers where NetworkInformationAPI is not supported. + +## 0.1.0 + +* Initial release. diff --git a/packages/connectivity/connectivity_for_web/LICENSE b/packages/connectivity/connectivity_for_web/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/connectivity/connectivity_for_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/connectivity/connectivity_for_web/README.md b/packages/connectivity/connectivity_for_web/README.md new file mode 100644 index 000000000000..66efc49fc840 --- /dev/null +++ b/packages/connectivity/connectivity_for_web/README.md @@ -0,0 +1,62 @@ +# connectivity_for_web + +A web implementation of [connectivity](https://pub.dev/connectivity/connectivity). Currently this package uses an experimental API, with a fallback to dart:html, so not all features may be available to all browsers. + +## Usage + +### Import the package + +This package is a non-endorsed implementation of `connectivity` for the web platform, so you need to modify your `pubspec.yaml` to use it: + +```yaml +... +dependencies: + ... + connectivity: ^0.4.9 + connectivity_for_web: ^0.3.0 + ... +... +``` + +## Example + +Find the example wiring in the [Google sign-in example application](https://github.com/ditman/plugins/blob/connectivity-web/packages/connectivity/connectivity/example/lib/main.dart). + +## Limitations on the web platform + +In order to retrieve information about the quality/speed of a browser's connection, the web implementation of the `connectivity` plugin uses the browser's [**NetworkInformation** Web API](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation), which as of this writing (June 2020) is still "experimental", and not available in all browsers: + +![Data on support for the netinfo feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/netinfo.png) + +On desktop browsers, this API only returns a very broad set of connectivity statuses (One of `'slow-2g', '2g', '3g', or '4g'`), and may *not* provide a Stream of changes. Firefox still hasn't enabled this feature by default. + +**Fallback to `navigator.onLine`** + +For those browsers where the NetworkInformation Web API is not available, the plugin falls back to the [**NavigatorOnLine** Web API](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorOnLine), which is more broadly supported: + +![Data on support for the online-status feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/online-status.png) + + +The NavigatorOnLine API is [provided by `dart:html`](https://api.dart.dev/stable/2.7.2/dart-html/Navigator/onLine.html), and only supports a boolean connectivity status (either online or offline), with no network speed information. In those cases the plugin will return either `wifi` (when the browser is online) or `none` (when it's not). + +Other than the approximate "downlink" speed, where available, and due to security and privacy concerns, **no Web browser will provide** any specific information about the actual network your users' device is connected to, like **the SSID on a Wi-Fi, or the MAC address of their device.** + +## Contributions and Testing + +Tests are crucial to contributions to this package. All new contributions should be reasonably tested. + +In order to run tests in this package, do: + +``` +cd test +flutter run -d chrome +``` + +All contributions to this package are welcome. Read the [Contributing to Flutter Plugins](https://github.com/flutter/plugins/blob/master/CONTRIBUTING.md) guide to get started. + +## Issues and feedback + +Please file an [issue](https://github.com/ditman/plugins/issues/new) +to send feedback or report a bug. + +**Thank you!** diff --git a/packages/connectivity/connectivity_for_web/example/README.md b/packages/connectivity/connectivity_for_web/example/README.md new file mode 100644 index 000000000000..8a6e74b107ea --- /dev/null +++ b/packages/connectivity/connectivity_for_web/example/README.md @@ -0,0 +1,9 @@ +# Testing + +This package uses `package:integration_test` to run its tests in a web browser. + +See [Plugin Tests > Web Tests](https://github.com/flutter/flutter/wiki/Plugin-Tests#web-tests) +in the Flutter wiki for instructions to setup and run the tests in this package. + +Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) +for more info. \ No newline at end of file diff --git a/packages/connectivity/connectivity_for_web/example/integration_test/network_information_test.dart b/packages/connectivity/connectivity_for_web/example/integration_test/network_information_test.dart 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/lib/connectivity_for_web.dart b/packages/connectivity/connectivity_for_web/lib/connectivity_for_web.dart new file mode 100644 index 000000000000..d1c6811f5349 --- /dev/null +++ b/packages/connectivity/connectivity_for_web/lib/connectivity_for_web.dart @@ -0,0 +1,67 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +import 'src/network_information_api_connectivity_plugin.dart'; +import 'src/dart_html_connectivity_plugin.dart'; + +/// The web implementation of the ConnectivityPlatform of the Connectivity plugin. +class ConnectivityPlugin extends ConnectivityPlatform { + /// Factory method that initializes the connectivity plugin platform with an instance + /// of the plugin for the web. + static void registerWith(Registrar registrar) { + if (NetworkInformationApiConnectivityPlugin.isSupported()) { + ConnectivityPlatform.instance = NetworkInformationApiConnectivityPlugin(); + } else { + ConnectivityPlatform.instance = DartHtmlConnectivityPlugin(); + } + } + + // The following are completely unsupported methods on the web platform. + + // Creates an "unsupported_operation" PlatformException for a given `method` name. + Object _unsupported(String method) { + return PlatformException( + code: 'UNSUPPORTED_OPERATION', + message: '$method() is not supported on the web platform.', + ); + } + + /// Obtains the wifi name (SSID) of the connected network + @override + Future getWifiName() { + throw _unsupported('getWifiName'); + } + + /// Obtains the wifi BSSID of the connected network. + @override + Future getWifiBSSID() { + throw _unsupported('getWifiBSSID'); + } + + /// Obtains the IP address of the connected wifi network + @override + Future getWifiIP() { + throw _unsupported('getWifiIP'); + } + + /// Request to authorize the location service (Only on iOS). + @override + Future requestLocationServiceAuthorization({ + bool requestAlwaysLocationUsage = false, + }) { + throw _unsupported('requestLocationServiceAuthorization'); + } + + /// Get the current location service authorization (Only on iOS). + @override + Future getLocationServiceAuthorization() { + throw _unsupported('getLocationServiceAuthorization'); + } +} diff --git a/packages/connectivity/connectivity_for_web/lib/src/dart_html_connectivity_plugin.dart b/packages/connectivity/connectivity_for_web/lib/src/dart_html_connectivity_plugin.dart new file mode 100644 index 000000000000..475ec0d675b7 --- /dev/null +++ b/packages/connectivity/connectivity_for_web/lib/src/dart_html_connectivity_plugin.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 'dart:async'; +import 'dart:html' as html show window; + +import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; +import 'package:connectivity_for_web/connectivity_for_web.dart'; + +/// The web implementation of the ConnectivityPlatform of the Connectivity plugin. +class DartHtmlConnectivityPlugin extends ConnectivityPlugin { + /// Checks the connection status of the device. + @override + Future checkConnectivity() async { + return html.window.navigator.onLine ?? false + ? ConnectivityResult.wifi + : ConnectivityResult.none; + } + + StreamController? _connectivityResult; + + /// Returns a Stream of ConnectivityResults changes. + @override + Stream get onConnectivityChanged { + if (_connectivityResult == null) { + _connectivityResult = StreamController.broadcast(); + // Fallback to dart:html window.onOnline / window.onOffline + html.window.onOnline.listen((event) { + _connectivityResult!.add(ConnectivityResult.wifi); + }); + html.window.onOffline.listen((event) { + _connectivityResult!.add(ConnectivityResult.none); + }); + } + return _connectivityResult!.stream; + } +} diff --git a/packages/connectivity/connectivity_for_web/lib/src/network_information_api_connectivity_plugin.dart b/packages/connectivity/connectivity_for_web/lib/src/network_information_api_connectivity_plugin.dart new file mode 100644 index 000000000000..6554f7a8c124 --- /dev/null +++ b/packages/connectivity/connectivity_for_web/lib/src/network_information_api_connectivity_plugin.dart @@ -0,0 +1,67 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:html' as html show window, NetworkInformation; +import 'dart:js' show allowInterop; +import 'dart:js_util' show setProperty; + +import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; +import 'package:connectivity_for_web/connectivity_for_web.dart'; +import 'package:flutter/foundation.dart'; + +import 'utils/connectivity_result.dart'; + +/// The web implementation of the ConnectivityPlatform of the Connectivity plugin. +class NetworkInformationApiConnectivityPlugin extends ConnectivityPlugin { + final html.NetworkInformation _networkInformation; + + /// A check to determine if this version of the plugin can be used. + static bool isSupported() => html.window.navigator.connection != null; + + /// The constructor of the plugin. + NetworkInformationApiConnectivityPlugin() + : this.withConnection(html.window.navigator.connection!); + + /// Creates the plugin, with an override of the NetworkInformation object. + @visibleForTesting + NetworkInformationApiConnectivityPlugin.withConnection( + html.NetworkInformation connection) + : _networkInformation = connection; + + /// Checks the connection status of the device. + @override + Future checkConnectivity() async { + return networkInformationToConnectivityResult(_networkInformation); + } + + StreamController? _connectivityResultStreamController; + late Stream _connectivityResultStream; + + /// Returns a Stream of ConnectivityResults changes. + @override + Stream get onConnectivityChanged { + if (_connectivityResultStreamController == null) { + _connectivityResultStreamController = + StreamController(); + + // Directly write the 'onchange' function on the networkInformation object. + setProperty(_networkInformation, 'onchange', allowInterop((_) { + _connectivityResultStreamController! + .add(networkInformationToConnectivityResult(_networkInformation)); + })); + // TODO: Implement the above with _networkInformation.onChange: + // _networkInformation.onChange.listen((_) { + // _connectivityResult + // .add(networkInformationToConnectivityResult(_networkInformation)); + // }); + // Once we can detect when to *cancel* a subscription to the _networkInformation + // onChange Stream upon hot restart. + // https://github.com/dart-lang/sdk/issues/42679 + _connectivityResultStream = + _connectivityResultStreamController!.stream.asBroadcastStream(); + } + return _connectivityResultStream; + } +} diff --git a/packages/connectivity/connectivity_for_web/lib/src/utils/connectivity_result.dart b/packages/connectivity/connectivity_for_web/lib/src/utils/connectivity_result.dart new file mode 100644 index 000000000000..691bd6da3bfb --- /dev/null +++ b/packages/connectivity/connectivity_for_web/lib/src/utils/connectivity_result.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 'dart:html' as html show NetworkInformation; +import 'package:connectivity_platform_interface/connectivity_platform_interface.dart'; + +/// Converts an incoming NetworkInformation object into the correct ConnectivityResult. +ConnectivityResult networkInformationToConnectivityResult( + html.NetworkInformation? info, +) { + if (info == null) { + return ConnectivityResult.none; + } + if (info.downlink == 0 && info.rtt == 0) { + return ConnectivityResult.none; + } + if (info.effectiveType != null) { + return _effectiveTypeToConnectivityResult(info.effectiveType!); + } + if (info.type != null) { + return _typeToConnectivityResult(info.type!); + } + return ConnectivityResult.none; +} + +ConnectivityResult _effectiveTypeToConnectivityResult(String effectiveType) { + // Possible values: + /*'2g'|'3g'|'4g'|'slow-2g'*/ + switch (effectiveType) { + case 'slow-2g': + case '2g': + case '3g': + return ConnectivityResult.mobile; + default: + return ConnectivityResult.wifi; + } +} + +ConnectivityResult _typeToConnectivityResult(String type) { + // Possible values: + /*'bluetooth'|'cellular'|'ethernet'|'mixed'|'none'|'other'|'unknown'|'wifi'|'wimax'*/ + switch (type) { + case 'none': + return ConnectivityResult.none; + case 'bluetooth': + case 'cellular': + case 'mixed': + case 'other': + case 'unknown': + return ConnectivityResult.mobile; + default: + return ConnectivityResult.wifi; + } +} diff --git a/packages/connectivity/connectivity_for_web/pubspec.yaml b/packages/connectivity/connectivity_for_web/pubspec.yaml new file mode 100644 index 000000000000..2aaa8bd978fa --- /dev/null +++ b/packages/connectivity/connectivity_for_web/pubspec.yaml @@ -0,0 +1,28 @@ +name: connectivity_for_web +description: An implementation for the web platform of the Flutter `connectivity` plugin. This uses the NetworkInformation Web API, with a fallback to Navigator.onLine. +repository: https://github.com/flutter/plugins/tree/master/packages/connectivity/connectivity_for_web +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+connectivity%22 +version: 0.4.0+1 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" + +flutter: + plugin: + implements: connectivity + platforms: + web: + pluginClass: ConnectivityPlugin + fileName: connectivity_for_web.dart + +dependencies: + connectivity_platform_interface: ^2.0.0 + flutter_web_plugins: + sdk: flutter + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter diff --git a/packages/connectivity/connectivity_for_web/test/README.md b/packages/connectivity/connectivity_for_web/test/README.md 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/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_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 814239a12b2b..46a4038f91ee 100644 --- a/packages/connectivity/connectivity_macos/CHANGELOG.md +++ b/packages/connectivity/connectivity_macos/CHANGELOG.md @@ -1,6 +1,43 @@ +## 0.2.1+2 + +* Add Swift language version to podspec. +* Fix `implements` package name in pubspec. + +## 0.2.1+1 + +* Ignore Reachability pointer to int cast warning. + +## 0.2.1 + +* Add `implements` to pubspec.yaml. + +## 0.2.0 + +* Remove placeholder Dart file. +* Update Dart SDK constraint for compatibility with null safety. + +## 0.1.0+8 + +* Update Flutter SDK constraint. + +## 0.1.0+7 + +* Remove unused `test` dependency. +* Update Dart SDK constraint in example. + +## 0.1.0+6 + +* Update license headers. + +## 0.1.0+5 + +* Update package:e2e reference to use the local version in the flutter/plugins + repository. +* Remove no-op android folder in the example app. + ## 0.1.0+4 -* Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). +* Remove Android folder from `connectivity_macos`. ## 0.1.0+3 @@ -8,6 +45,7 @@ * Clean up various Android workarounds no longer needed after framework v1.12. * Complete v2 embedding support. * Fix CocoaPods podspec lint warnings. +* Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). ## 0.1.0+2 diff --git a/packages/connectivity/connectivity_macos/LICENSE b/packages/connectivity/connectivity_macos/LICENSE index 0c382ce171cc..c6823b81eb84 100644 --- a/packages/connectivity/connectivity_macos/LICENSE +++ b/packages/connectivity/connectivity_macos/LICENSE @@ -1,27 +1,25 @@ -// 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. +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/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/android/README.md b/packages/connectivity/connectivity_macos/android/README.md deleted file mode 100644 index 1b2533402146..000000000000 --- a/packages/connectivity/connectivity_macos/android/README.md +++ /dev/null @@ -1 +0,0 @@ -This is a dummy project used to silence an error raised on Flutter version 1.9.1 where it looks for an android directory inside the plugin's macos directory. Once this error is fixed in new versions of Flutter, the android folder should be removed. \ No newline at end of file diff --git a/packages/connectivity/connectivity_macos/android/build.gradle b/packages/connectivity/connectivity_macos/android/build.gradle deleted file mode 100644 index 59e66b221134..000000000000 --- a/packages/connectivity/connectivity_macos/android/build.gradle +++ /dev/null @@ -1,37 +0,0 @@ -group 'com.example.connectivity' -version '1.0' - -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.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' - } -} diff --git a/packages/connectivity/connectivity_macos/android/gradle/wrapper/gradle-wrapper.properties b/packages/connectivity/connectivity_macos/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 019065d1d650..000000000000 --- a/packages/connectivity/connectivity_macos/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/connectivity/connectivity_macos/android/settings.gradle b/packages/connectivity/connectivity_macos/android/settings.gradle deleted file mode 100644 index 4fbed4753c9c..000000000000 --- a/packages/connectivity/connectivity_macos/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'connectivity' diff --git a/packages/connectivity/connectivity_macos/android/src/main/AndroidManifest.xml b/packages/connectivity/connectivity_macos/android/src/main/AndroidManifest.xml deleted file mode 100644 index 8b6683c651e4..000000000000 --- a/packages/connectivity/connectivity_macos/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/packages/connectivity/connectivity_macos/android/src/main/java/com/example/connectivity/ConnectivityPlugin.java b/packages/connectivity/connectivity_macos/android/src/main/java/com/example/connectivity/ConnectivityPlugin.java deleted file mode 100644 index 405cb0396a0f..000000000000 --- a/packages/connectivity/connectivity_macos/android/src/main/java/com/example/connectivity/ConnectivityPlugin.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.example.connectivity; - -import androidx.annotation.NonNull; -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugin.common.PluginRegistry.Registrar; - -/** ConnectivityPlugin */ -public class ConnectivityPlugin implements FlutterPlugin, MethodCallHandler { - @Override - public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {} - - public static void registerWith(Registrar registrar) {} - - @Override - public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {} - - @Override - public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {} -} diff --git a/packages/connectivity/connectivity_macos/example/android/app/build.gradle b/packages/connectivity/connectivity_macos/example/android/app/build.gradle deleted file mode 100644 index 5d1f138bfe1a..000000000000 --- a/packages/connectivity/connectivity_macos/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.connectivityexample" - 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/connectivity/connectivity_macos/example/android/app/src/main/AndroidManifest.xml b/packages/connectivity/connectivity_macos/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 664b74303548..000000000000 --- a/packages/connectivity/connectivity_macos/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/packages/connectivity/connectivity_macos/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/EmbeddingV1Activity.java b/packages/connectivity/connectivity_macos/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/EmbeddingV1Activity.java deleted file mode 100644 index fa10cd5b7f52..000000000000 --- a/packages/connectivity/connectivity_macos/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/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.connectivityexample; - -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.connectivity.ConnectivityPlugin; - -public class EmbeddingV1Activity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ConnectivityPlugin.registerWith( - registrarFor("io.flutter.plugins.connectivity.ConnectivityPlugin")); - } -} diff --git a/packages/connectivity/connectivity_macos/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/EmbeddingV1ActivityTest.java b/packages/connectivity/connectivity_macos/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/EmbeddingV1ActivityTest.java deleted file mode 100644 index a34755399117..000000000000 --- a/packages/connectivity/connectivity_macos/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/EmbeddingV1ActivityTest.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.connectivityexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/connectivity/connectivity_macos/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/FlutterActivityTest.java b/packages/connectivity/connectivity_macos/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/FlutterActivityTest.java deleted file mode 100644 index 35391fd5e0a0..000000000000 --- a/packages/connectivity/connectivity_macos/example/android/app/src/main/java/io/flutter/plugins/connectivityexample/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.connectivityexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import io.flutter.embedding.android.FlutterActivity; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class FlutterActivityTest { - @Rule - public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); -} diff --git a/packages/connectivity/connectivity_macos/example/android/build.gradle b/packages/connectivity/connectivity_macos/example/android/build.gradle deleted file mode 100644 index 541636cc492a..000000000000 --- a/packages/connectivity/connectivity_macos/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/connectivity/connectivity_macos/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/connectivity/connectivity_macos/example/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 019065d1d650..000000000000 --- a/packages/connectivity/connectivity_macos/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/connectivity/connectivity_macos/example/android/settings.gradle b/packages/connectivity/connectivity_macos/example/android/settings.gradle deleted file mode 100644 index a159ea7cb99f..000000000000 --- a/packages/connectivity/connectivity_macos/example/android/settings.gradle +++ /dev/null @@ -1,15 +0,0 @@ -include ':app' - -def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() - -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withInputStream { stream -> plugins.load(stream) } -} - -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory -} diff --git a/packages/connectivity/connectivity_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/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 877250021f9a..0af2e1587b00 100644 --- a/packages/connectivity/connectivity_macos/example/pubspec.yaml +++ b/packages/connectivity/connectivity_macos/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.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 - 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/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/test/connectivity_e2e.dart b/packages/connectivity/connectivity_macos/example/test_driver/test/connectivity_e2e.dart deleted file mode 100644 index 10c4bda34e0d..000000000000 --- a/packages/connectivity/connectivity_macos/example/test_driver/test/connectivity_e2e.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:e2e/e2e.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:connectivity/connectivity.dart'; - -void main() { - E2EWidgetsFlutterBinding.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/connectivity_e2e_test.dart b/packages/connectivity/connectivity_macos/example/test_driver/test/connectivity_e2e_test.dart deleted file mode 100644 index 84b7ae6a47ab..000000000000 --- a/packages/connectivity/connectivity_macos/example/test_driver/test/connectivity_e2e_test.dart +++ /dev/null @@ -1,14 +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:flutter_driver/flutter_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); -} 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 91d8ae1eb3c6..69efe80df5ac 100644 --- a/packages/connectivity/connectivity_macos/macos/Classes/ConnectivityPlugin.swift +++ b/packages/connectivity/connectivity_macos/macos/Classes/ConnectivityPlugin.swift @@ -1,16 +1,6 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. import Cocoa import CoreWLAN diff --git a/packages/connectivity/connectivity_macos/macos/Classes/IPHelper.h b/packages/connectivity/connectivity_macos/macos/Classes/IPHelper.h index 5fef2b97213c..e5370fb349c3 100644 --- a/packages/connectivity/connectivity_macos/macos/Classes/IPHelper.h +++ b/packages/connectivity/connectivity_macos/macos/Classes/IPHelper.h @@ -1,16 +1,6 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// Copyright 2013 The Flutter Authors. 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/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 968b0e8c01a4..b98f23d34eb7 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+3 -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+2 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" flutter: plugin: + implements: connectivity 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 0c91662b3f2f..c6823b81eb84 100644 --- a/packages/connectivity/connectivity_platform_interface/LICENSE +++ b/packages/connectivity/connectivity_platform_interface/LICENSE @@ -1,27 +1,25 @@ -// Copyright 2020 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/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..b69feae252eb 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,13 +42,14 @@ void main() { .setMockMethodCallHandler((MethodCall methodCall) async { switch (methodCall.method) { case 'listen': - await ServicesBinding.instance.defaultBinaryMessenger + await _ambiguate(ServicesBinding.instance)! + .defaultBinaryMessenger .handlePlatformMessage( - methodChannelConnectivity.eventChannel.name, - methodChannelConnectivity.eventChannel.codec - .encodeSuccessEnvelope('wifi'), - (_) {}, - ); + methodChannelConnectivity.eventChannel.name, + methodChannelConnectivity.eventChannel.codec + .encodeSuccessEnvelope('wifi'), + (_) {}, + ); break; case 'cancel': default: @@ -64,7 +65,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 +79,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 +93,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, @@ -151,3 +152,10 @@ void main() { }); }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +// TODO(ianh): Remove this once we roll stable in late 2021. +T? _ambiguate(T? value) => value; diff --git a/packages/device_info/CHANGELOG.md b/packages/device_info/CHANGELOG.md deleted file mode 100644 index 057638d01cac..000000000000 --- a/packages/device_info/CHANGELOG.md +++ /dev/null @@ -1,130 +0,0 @@ -## 0.4.2+4 - -Update lower bound of dart dependency to 2.1.0. - -## 0.4.2+3 - -* Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). - -## 0.4.2+2 - -* Fix CocoaPods podspec lint warnings. - -## 0.4.2+1 - -* Bump the minimum Flutter version to 1.12.13+hotfix.5. -* Remove deprecated API usage warning in AndroidIntentPlugin.java. -* Migrates the Android example to V2 embedding. -* Bumps AGP to 3.6.1. - -## 0.4.2 - -* Add systemFeatures to AndroidDeviceInfo. - -## 0.4.1+5 - -* Make the pedantic dev_dependency explicit. - -## 0.4.1+4 - -* Remove the deprecated `author:` field from pubspec.yaml -* Migrate the plugin to the pubspec platforms manifest. -* Require Flutter SDK 1.10.0 or greater. - -## 0.4.1+3 - -* Fix pedantic errors. Adds some missing documentation and fixes unawaited - futures in the tests. - -## 0.4.1+2 - -* Remove AndroidX warning. - -## 0.4.1+1 - -* Include lifecycle dependency as a compileOnly one on Android to resolve - potential version conflicts with other transitive libraries. - -## 0.4.1 - -* Support the v2 Android embedding. -* Update to AndroidX. -* Migrate to using the new e2e test binding. -* Add a e2e test. - - -## 0.4.0+4 - -* Define clang module for iOS. - -## 0.4.0+3 - -* Update and migrate iOS example project. - -## 0.4.0+2 - -* Bump minimum Flutter version to 1.5.0. -* Add missing template type parameter to `invokeMethod` calls. -* Replace invokeMethod with invokeMapMethod wherever necessary. - -## 0.4.0+1 - -* Log a more detailed warning at build time about the previous AndroidX - migration. - -## 0.4.0 - -* **Breaking change**. Migrate from the deprecated original Android Support - Library to AndroidX. This shouldn't result in any functional changes, but it - requires any Android apps using this plugin to [also - migrate](https://developer.android.com/jetpack/androidx/migrate) if they're - using the original support library. - -## 0.3.0 - -* Added ability to get Android ID for Android devices - -## 0.2.1 - -* Updated Gradle tooling to match Android Studio 3.1.2. - -## 0.2.0 - -* **Breaking change**. Set SDK constraints to match the Flutter beta release. - -## 0.1.2 - -* Fixed Dart 2 type errors. - -## 0.1.1 - -* Simplified and upgraded Android project template to Android SDK 27. -* Updated package description. - -## 0.1.0 - -* **Breaking change**. Upgraded to Gradle 4.1 and Android Studio Gradle plugin - 3.0.1. Older Flutter projects need to upgrade their Gradle setup as well in - order to use this version of the plugin. Instructions can be found - [here](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1). - -## 0.0.5 - -* Added FLT prefix to iOS types - -## 0.0.4 - -* Fixed Java/Dart communication error with empty lists - -## 0.0.3 - -* Added support for utsname - -## 0.0.2 - -* Fixed broken type comparison -* Added "isPhysicalDevice" field, detecting emulators/simulators - -## 0.0.1 - -* Implements platform-specific device/OS properties diff --git a/packages/device_info/LICENSE b/packages/device_info/LICENSE deleted file mode 100644 index c89293372cf3..000000000000 --- a/packages/device_info/LICENSE +++ /dev/null @@ -1,27 +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/device_info/README.md b/packages/device_info/README.md deleted file mode 100644 index db846c39690c..000000000000 --- a/packages/device_info/README.md +++ /dev/null @@ -1,38 +0,0 @@ -# device_info - -Get current device information from within the Flutter application. - -**Please set your constraint to `device_info: '>=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 `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 - -# Usage - -Import `package:device_info/device_info.dart`, instantiate `DeviceInfoPlugin` -and use the Android and iOS getters to get platform-specific device -information. - -Example: - -```dart -import 'package:device_info/device_info.dart'; - -DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); -AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; -print('Running on ${androidInfo.model}'); // e.g. "Moto G (4)" - -IosDeviceInfo iosInfo = await deviceInfo.iosInfo; -print('Running on ${iosInfo.utsname.machine}'); // e.g. "iPod7,1" -``` - -You will find links to the API docs on the [pub page](https://pub.dartlang.org/packages/device_info). - -## 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/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/android/build.gradle b/packages/device_info/android/build.gradle deleted file mode 100644 index 58bdfd327631..000000000000 --- a/packages/device_info/android/build.gradle +++ /dev/null @@ -1,34 +0,0 @@ -group 'io.flutter.plugins.deviceinfo' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.6.1' - } -} - -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/device_info/android/gradle.properties b/packages/device_info/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/device_info/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/packages/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/MethodCallHandlerImpl.java b/packages/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/MethodCallHandlerImpl.java deleted file mode 100644 index 800ca6dcddb7..000000000000 --- a/packages/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/MethodCallHandlerImpl.java +++ /dev/null @@ -1,133 +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.deviceinfo; - -import android.annotation.SuppressLint; -import android.content.ContentResolver; -import android.content.pm.FeatureInfo; -import android.content.pm.PackageManager; -import android.os.Build; -import android.provider.Settings; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; - -/** - * The implementation of {@link MethodChannel.MethodCallHandler} for the plugin. Responsible for - * receiving method calls from method channel. - */ -class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { - - private final ContentResolver contentResolver; - private final PackageManager packageManager; - - /** Substitute for missing values. */ - private static final String[] EMPTY_STRING_LIST = new String[] {}; - - /** Constructs DeviceInfo. {@code contentResolver} and {@code packageManager} must not be null. */ - MethodCallHandlerImpl(ContentResolver contentResolver, PackageManager packageManager) { - this.contentResolver = contentResolver; - this.packageManager = packageManager; - } - - @Override - public void onMethodCall(MethodCall call, MethodChannel.Result result) { - if (call.method.equals("getAndroidDeviceInfo")) { - Map build = new HashMap<>(); - build.put("board", Build.BOARD); - build.put("bootloader", Build.BOOTLOADER); - build.put("brand", Build.BRAND); - build.put("device", Build.DEVICE); - build.put("display", Build.DISPLAY); - build.put("fingerprint", Build.FINGERPRINT); - build.put("hardware", Build.HARDWARE); - build.put("host", Build.HOST); - build.put("id", Build.ID); - build.put("manufacturer", Build.MANUFACTURER); - build.put("model", Build.MODEL); - build.put("product", Build.PRODUCT); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - build.put("supported32BitAbis", Arrays.asList(Build.SUPPORTED_32_BIT_ABIS)); - build.put("supported64BitAbis", Arrays.asList(Build.SUPPORTED_64_BIT_ABIS)); - build.put("supportedAbis", Arrays.asList(Build.SUPPORTED_ABIS)); - } else { - build.put("supported32BitAbis", Arrays.asList(EMPTY_STRING_LIST)); - build.put("supported64BitAbis", Arrays.asList(EMPTY_STRING_LIST)); - build.put("supportedAbis", Arrays.asList(EMPTY_STRING_LIST)); - } - build.put("tags", Build.TAGS); - build.put("type", Build.TYPE); - build.put("isPhysicalDevice", !isEmulator()); - build.put("androidId", getAndroidId()); - - build.put("systemFeatures", Arrays.asList(getSystemFeatures())); - - Map version = new HashMap<>(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - version.put("baseOS", Build.VERSION.BASE_OS); - version.put("previewSdkInt", Build.VERSION.PREVIEW_SDK_INT); - version.put("securityPatch", Build.VERSION.SECURITY_PATCH); - } - version.put("codename", Build.VERSION.CODENAME); - version.put("incremental", Build.VERSION.INCREMENTAL); - version.put("release", Build.VERSION.RELEASE); - version.put("sdkInt", Build.VERSION.SDK_INT); - build.put("version", version); - - result.success(build); - } else { - result.notImplemented(); - } - } - - private String[] getSystemFeatures() { - FeatureInfo[] featureInfos = packageManager.getSystemAvailableFeatures(); - if (featureInfos == null) { - return EMPTY_STRING_LIST; - } - String[] features = new String[featureInfos.length]; - for (int i = 0; i < featureInfos.length; i++) { - features[i] = featureInfos[i].name; - } - return features; - } - - /** - * Returns the Android hardware device ID that is unique between the device + user and app - * signing. This key will change if the app is uninstalled or its data is cleared. Device factory - * reset will also result in a value change. - * - * @return The android ID - */ - @SuppressLint("HardwareIds") - private String getAndroidId() { - return Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID); - } - - /** - * A simple emulator-detection based on the flutter tools detection logic and a couple of legacy - * detection systems - */ - private boolean isEmulator() { - return (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) - || Build.FINGERPRINT.startsWith("generic") - || Build.FINGERPRINT.startsWith("unknown") - || Build.HARDWARE.contains("goldfish") - || Build.HARDWARE.contains("ranchu") - || Build.MODEL.contains("google_sdk") - || Build.MODEL.contains("Emulator") - || Build.MODEL.contains("Android SDK built for x86") - || Build.MANUFACTURER.contains("Genymotion") - || Build.PRODUCT.contains("sdk_google") - || Build.PRODUCT.contains("google_sdk") - || Build.PRODUCT.contains("sdk") - || Build.PRODUCT.contains("sdk_x86") - || Build.PRODUCT.contains("vbox86p") - || Build.PRODUCT.contains("emulator") - || Build.PRODUCT.contains("simulator"); - } -} diff --git a/packages/device_info/device_info/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 new file mode 100644 index 000000000000..97349d450cf1 --- /dev/null +++ b/packages/device_info/device_info/CHANGELOG.md @@ -0,0 +1,181 @@ +## NEXT + +* Remove references to the Android V1 embedding. +* Updated Android lint settings. + +## 2.0.2 + +* Update README to point to Plus Plugins version. + +## 2.0.1 + +* Migrate maven repository from jcenter to mavenCentral. + +## 2.0.0 + +* Migrate to null safety. +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 1.0.1 + +* Update Flutter SDK constraint. + +## 1.0.0 + +* Announce 1.0.0. + +## 0.4.2+10 + +* Update Dart SDK constraint in example. + +## 0.4.2+9 + +* Update android compileSdkVersion to 29. + +## 0.4.2+8 + +* Keep handling deprecated Android v1 classes for backward compatibility. + +## 0.4.2+7 + +* Port device_info plugin to use platform interface. + +## 0.4.2+6 + +* Moved everything from device_info to device_info/device_info + +## 0.4.2+5 + +* Update package:e2e reference to use the local version in the flutter/plugins + repository. + +## 0.4.2+4 + +Update lower bound of dart dependency to 2.1.0. + +## 0.4.2+3 + +* Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). + +## 0.4.2+2 + +* Fix CocoaPods podspec lint warnings. + +## 0.4.2+1 + +* Bump the minimum Flutter version to 1.12.13+hotfix.5. +* Remove deprecated API usage warning in AndroidIntentPlugin.java. +* Migrates the Android example to V2 embedding. +* Bumps AGP to 3.6.1. + +## 0.4.2 + +* Add systemFeatures to AndroidDeviceInfo. + +## 0.4.1+5 + +* Make the pedantic dev_dependency explicit. + +## 0.4.1+4 + +* Remove the deprecated `author:` field from pubspec.yaml +* Migrate the plugin to the pubspec platforms manifest. +* Require Flutter SDK 1.10.0 or greater. + +## 0.4.1+3 + +* Fix pedantic errors. Adds some missing documentation and fixes unawaited + futures in the tests. + +## 0.4.1+2 + +* Remove AndroidX warning. + +## 0.4.1+1 + +* Include lifecycle dependency as a compileOnly one on Android to resolve + potential version conflicts with other transitive libraries. + +## 0.4.1 + +* Support the v2 Android embedding. +* Update to AndroidX. +* Migrate to using the new e2e test binding. +* Add a e2e test. + + +## 0.4.0+4 + +* Define clang module for iOS. + +## 0.4.0+3 + +* Update and migrate iOS example project. + +## 0.4.0+2 + +* Bump minimum Flutter version to 1.5.0. +* Add missing template type parameter to `invokeMethod` calls. +* Replace invokeMethod with invokeMapMethod wherever necessary. + +## 0.4.0+1 + +* Log a more detailed warning at build time about the previous AndroidX + migration. + +## 0.4.0 + +* **Breaking change**. Migrate from the deprecated original Android Support + Library to AndroidX. This shouldn't result in any functional changes, but it + requires any Android apps using this plugin to [also + migrate](https://developer.android.com/jetpack/androidx/migrate) if they're + using the original support library. + +## 0.3.0 + +* Added ability to get Android ID for Android devices + +## 0.2.1 + +* Updated Gradle tooling to match Android Studio 3.1.2. + +## 0.2.0 + +* **Breaking change**. Set SDK constraints to match the Flutter beta release. + +## 0.1.2 + +* Fixed Dart 2 type errors. + +## 0.1.1 + +* Simplified and upgraded Android project template to Android SDK 27. +* Updated package description. + +## 0.1.0 + +* **Breaking change**. Upgraded to Gradle 4.1 and Android Studio Gradle plugin + 3.0.1. Older Flutter projects need to upgrade their Gradle setup as well in + order to use this version of the plugin. Instructions can be found + [here](https://github.com/flutter/flutter/wiki/Updating-Flutter-projects-to-Gradle-4.1-and-Android-Studio-Gradle-plugin-3.0.1). + +## 0.0.5 + +* Added FLT prefix to iOS types + +## 0.0.4 + +* Fixed Java/Dart communication error with empty lists + +## 0.0.3 + +* Added support for utsname + +## 0.0.2 + +* Fixed broken type comparison +* Added "isPhysicalDevice" field, detecting emulators/simulators + +## 0.0.1 + +* Implements platform-specific device/OS properties diff --git a/packages/device_info/device_info/LICENSE b/packages/device_info/device_info/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/device_info/device_info/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/device_info/device_info/README.md b/packages/device_info/device_info/README.md new file mode 100644 index 000000000000..34cf9bb2ac2b --- /dev/null +++ b/packages/device_info/device_info/README.md @@ -0,0 +1,46 @@ +# device_info + +--- + +## Deprecation Notice + +This plugin has been replaced by the [Flutter Community Plus +Plugins](https://plus.fluttercommunity.dev/) version, +[`device_info_plus`](https://pub.dev/packages/device_info_plus). +No further updates are planned to this plugin, and we encourage all users to +migrate to the Plus version. + +Critical fixes (e.g., for any security incidents) will be provided through the +end of 2021, at which point this package will be marked as discontinued. + +--- + +Get current device information from within the Flutter application. + +# Usage + +Import `package:device_info/device_info.dart`, instantiate `DeviceInfoPlugin` +and use the Android and iOS getters to get platform-specific device +information. + +Example: + +```dart +import 'package:device_info/device_info.dart'; + +DeviceInfoPlugin deviceInfo = DeviceInfoPlugin(); +AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo; +print('Running on ${androidInfo.model}'); // e.g. "Moto G (4)" + +IosDeviceInfo iosInfo = await deviceInfo.iosInfo; +print('Running on ${iosInfo.utsname.machine}'); // e.g. "iPod7,1" +``` + +You will find links to the API docs on the [pub page](https://pub.dev/packages/device_info). + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.dev/). + +For help on editing plugin code, view the [documentation](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#plugin). diff --git a/packages/device_info/device_info/android/build.gradle b/packages/device_info/device_info/android/build.gradle new file mode 100644 index 000000000000..ed89da419d4a --- /dev/null +++ b/packages/device_info/device_info/android/build.gradle @@ -0,0 +1,48 @@ +group 'io.flutter.plugins.deviceinfo' +version '1.0-SNAPSHOT' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.6.1' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 29 + + defaultConfig { + minSdkVersion 16 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + lintOptions { + disable 'InvalidPackage' + disable 'GradleDependency' + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/packages/device_info/android/settings.gradle b/packages/device_info/device_info/android/settings.gradle similarity index 100% rename from packages/device_info/android/settings.gradle rename to packages/device_info/device_info/android/settings.gradle diff --git a/packages/device_info/android/src/main/AndroidManifest.xml b/packages/device_info/device_info/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/device_info/android/src/main/AndroidManifest.xml rename to packages/device_info/device_info/android/src/main/AndroidManifest.xml diff --git a/packages/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 similarity index 87% rename from packages/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/DeviceInfoPlugin.java rename to packages/device_info/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/DeviceInfoPlugin.java index 8061959c2047..9b766d7f8381 100644 --- a/packages/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. @@ -8,7 +8,6 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.PluginRegistry.Registrar; /** DeviceInfoPlugin */ public class DeviceInfoPlugin implements FlutterPlugin { @@ -16,7 +15,8 @@ public class DeviceInfoPlugin implements FlutterPlugin { MethodChannel channel; /** Plugin registration. */ - public static void registerWith(Registrar registrar) { + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { DeviceInfoPlugin plugin = new DeviceInfoPlugin(); plugin.setupMethodChannel(registrar.messenger(), registrar.context()); } 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 new file mode 100644 index 000000000000..531e5db0c237 --- /dev/null +++ b/packages/device_info/device_info/android/src/main/java/io/flutter/plugins/deviceinfo/MethodCallHandlerImpl.java @@ -0,0 +1,133 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.deviceinfo; + +import android.annotation.SuppressLint; +import android.content.ContentResolver; +import android.content.pm.FeatureInfo; +import android.content.pm.PackageManager; +import android.os.Build; +import android.provider.Settings; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * The implementation of {@link MethodChannel.MethodCallHandler} for the plugin. Responsible for + * receiving method calls from method channel. + */ +class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { + + private final ContentResolver contentResolver; + private final PackageManager packageManager; + + /** Substitute for missing values. */ + private static final String[] EMPTY_STRING_LIST = new String[] {}; + + /** Constructs DeviceInfo. {@code contentResolver} and {@code packageManager} must not be null. */ + MethodCallHandlerImpl(ContentResolver contentResolver, PackageManager packageManager) { + this.contentResolver = contentResolver; + this.packageManager = packageManager; + } + + @Override + public void onMethodCall(MethodCall call, MethodChannel.Result result) { + if (call.method.equals("getAndroidDeviceInfo")) { + Map build = new HashMap<>(); + build.put("board", Build.BOARD); + build.put("bootloader", Build.BOOTLOADER); + build.put("brand", Build.BRAND); + build.put("device", Build.DEVICE); + build.put("display", Build.DISPLAY); + build.put("fingerprint", Build.FINGERPRINT); + build.put("hardware", Build.HARDWARE); + build.put("host", Build.HOST); + build.put("id", Build.ID); + build.put("manufacturer", Build.MANUFACTURER); + build.put("model", Build.MODEL); + build.put("product", Build.PRODUCT); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + build.put("supported32BitAbis", Arrays.asList(Build.SUPPORTED_32_BIT_ABIS)); + build.put("supported64BitAbis", Arrays.asList(Build.SUPPORTED_64_BIT_ABIS)); + build.put("supportedAbis", Arrays.asList(Build.SUPPORTED_ABIS)); + } else { + build.put("supported32BitAbis", Arrays.asList(EMPTY_STRING_LIST)); + build.put("supported64BitAbis", Arrays.asList(EMPTY_STRING_LIST)); + build.put("supportedAbis", Arrays.asList(EMPTY_STRING_LIST)); + } + build.put("tags", Build.TAGS); + build.put("type", Build.TYPE); + build.put("isPhysicalDevice", !isEmulator()); + build.put("androidId", getAndroidId()); + + build.put("systemFeatures", Arrays.asList(getSystemFeatures())); + + Map version = new HashMap<>(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + version.put("baseOS", Build.VERSION.BASE_OS); + version.put("previewSdkInt", Build.VERSION.PREVIEW_SDK_INT); + version.put("securityPatch", Build.VERSION.SECURITY_PATCH); + } + version.put("codename", Build.VERSION.CODENAME); + version.put("incremental", Build.VERSION.INCREMENTAL); + version.put("release", Build.VERSION.RELEASE); + version.put("sdkInt", Build.VERSION.SDK_INT); + build.put("version", version); + + result.success(build); + } else { + result.notImplemented(); + } + } + + private String[] getSystemFeatures() { + FeatureInfo[] featureInfos = packageManager.getSystemAvailableFeatures(); + if (featureInfos == null) { + return EMPTY_STRING_LIST; + } + String[] features = new String[featureInfos.length]; + for (int i = 0; i < featureInfos.length; i++) { + features[i] = featureInfos[i].name; + } + return features; + } + + /** + * Returns the Android hardware device ID that is unique between the device + user and app + * signing. This key will change if the app is uninstalled or its data is cleared. Device factory + * reset will also result in a value change. + * + * @return The android ID + */ + @SuppressLint("HardwareIds") + private String getAndroidId() { + return Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID); + } + + /** + * A simple emulator-detection based on the flutter tools detection logic and a couple of legacy + * detection systems + */ + private boolean isEmulator() { + return (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) + || Build.FINGERPRINT.startsWith("generic") + || Build.FINGERPRINT.startsWith("unknown") + || Build.HARDWARE.contains("goldfish") + || Build.HARDWARE.contains("ranchu") + || Build.MODEL.contains("google_sdk") + || Build.MODEL.contains("Emulator") + || Build.MODEL.contains("Android SDK built for x86") + || Build.MANUFACTURER.contains("Genymotion") + || Build.PRODUCT.contains("sdk_google") + || Build.PRODUCT.contains("google_sdk") + || Build.PRODUCT.contains("sdk") + || Build.PRODUCT.contains("sdk_x86") + || Build.PRODUCT.contains("vbox86p") + || Build.PRODUCT.contains("emulator") + || Build.PRODUCT.contains("simulator"); + } +} diff --git a/packages/device_info/device_info/example/README.md b/packages/device_info/device_info/example/README.md new file mode 100644 index 000000000000..ea47551011d0 --- /dev/null +++ b/packages/device_info/device_info/example/README.md @@ -0,0 +1,8 @@ +# device_info_example + +Demonstrates how to use the `device_info` plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.dev/). diff --git a/packages/device_info/device_info/example/android/app/build.gradle b/packages/device_info/device_info/example/android/app/build.gradle new file mode 100644 index 000000000000..eb0c628330be --- /dev/null +++ b/packages/device_info/device_info/example/android/app/build.gradle @@ -0,0 +1,60 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 29 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + applicationId "io.flutter.plugins.deviceinfoexample" + minSdkVersion 16 + targetSdkVersion 28 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies { + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' +} diff --git a/packages/connectivity/connectivity_macos/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/device_info/device_info/example/android/app/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/connectivity/connectivity_macos/example/android/app/gradle/wrapper/gradle-wrapper.properties rename to packages/device_info/device_info/example/android/app/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/device_info/device_info/example/android/app/src/main/AndroidManifest.xml b/packages/device_info/device_info/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..4268475986a3 --- /dev/null +++ b/packages/device_info/device_info/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/packages/connectivity/connectivity_macos/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/device_info/device_info/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from packages/connectivity/connectivity_macos/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/device_info/device_info/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/connectivity/connectivity_macos/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/device_info/device_info/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from packages/connectivity/connectivity_macos/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/device_info/device_info/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/connectivity/connectivity_macos/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/device_info/device_info/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from packages/connectivity/connectivity_macos/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/device_info/device_info/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/connectivity/connectivity_macos/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/device_info/device_info/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from packages/connectivity/connectivity_macos/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/device_info/device_info/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/connectivity/connectivity_macos/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/device_info/device_info/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from packages/connectivity/connectivity_macos/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/device_info/device_info/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/packages/device_info/device_info/example/android/build.gradle b/packages/device_info/device_info/example/android/build.gradle new file mode 100644 index 000000000000..3274eb601b9b --- /dev/null +++ b/packages/device_info/device_info/example/android/build.gradle @@ -0,0 +1,29 @@ +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.6.1' + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/packages/connectivity/connectivity_macos/android/gradle.properties b/packages/device_info/device_info/example/android/gradle.properties similarity index 100% rename from packages/connectivity/connectivity_macos/android/gradle.properties rename to packages/device_info/device_info/example/android/gradle.properties diff --git a/packages/device_info/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/device_info/device_info/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/device_info/example/android/gradle/wrapper/gradle-wrapper.properties rename to packages/device_info/device_info/example/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/device_info/example/android/settings.gradle b/packages/device_info/device_info/example/android/settings.gradle similarity index 100% rename from packages/device_info/example/android/settings.gradle rename to packages/device_info/device_info/example/android/settings.gradle 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 new file mode 100644 index 000000000000..953eb856d62a --- /dev/null +++ b/packages/device_info/device_info/example/integration_test/device_info_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 'dart:io'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:device_info/device_info.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + late IosDeviceInfo iosInfo; + late AndroidDeviceInfo androidInfo; + + setUpAll(() async { + final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); + if (Platform.isIOS) { + iosInfo = await deviceInfoPlugin.iosInfo; + } else if (Platform.isAndroid) { + androidInfo = await deviceInfoPlugin.androidInfo; + } + }); + + testWidgets('Can get non-null device model', (WidgetTester tester) async { + if (Platform.isIOS) { + expect(iosInfo.model, isNotNull); + } else if (Platform.isAndroid) { + expect(androidInfo.model, isNotNull); + } + }); +} diff --git a/packages/android_intent/example/ios/Flutter/AppFrameworkInfo.plist b/packages/device_info/device_info/example/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from packages/android_intent/example/ios/Flutter/AppFrameworkInfo.plist rename to packages/device_info/device_info/example/ios/Flutter/AppFrameworkInfo.plist diff --git a/packages/android_intent/example/ios/Flutter/Debug.xcconfig b/packages/device_info/device_info/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/android_intent/example/ios/Flutter/Debug.xcconfig rename to packages/device_info/device_info/example/ios/Flutter/Debug.xcconfig diff --git a/packages/android_intent/example/ios/Flutter/Release.xcconfig b/packages/device_info/device_info/example/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/android_intent/example/ios/Flutter/Release.xcconfig rename to packages/device_info/device_info/example/ios/Flutter/Release.xcconfig 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 new file mode 100644 index 000000000000..ca599db5b7ac --- /dev/null +++ b/packages/device_info/device_info/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,460 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 4C26954642C9965233939F98 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 17470FCDF9FA37CB94B63753 /* libPods-Runner.a */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 17470FCDF9FA37CB94B63753 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + C8043F3EE7ED1716F368CC90 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + E96AF818D5456D130681C78B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4C26954642C9965233939F98 /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 20A0DD43C00A880430740858 /* Pods */ = { + isa = PBXGroup; + children = ( + C8043F3EE7ED1716F368CC90 /* Pods-Runner.debug.xcconfig */, + E96AF818D5456D130681C78B /* Pods-Runner.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 20A0DD43C00A880430740858 /* Pods */, + EA17DAB2B097E79A4CABE344 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + EA17DAB2B097E79A4CABE344 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 17470FCDF9FA37CB94B63753 /* libPods-Runner.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + B8856E1B697C88C1C62EE937 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1100; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + B8856E1B697C88C1C62EE937 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.deviceInfoExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.deviceInfoExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/device_info/device_info/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/device_info/device_info/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/device_info/device_info/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/android_intent/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/device_info/device_info/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from packages/android_intent/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/device_info/device_info/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/packages/battery/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/device_info/device_info/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/battery/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/device_info/device_info/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/device_info/device_info/example/ios/Runner/AppDelegate.h b/packages/device_info/device_info/example/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..0681d288bb70 --- /dev/null +++ b/packages/device_info/device_info/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/device_info/device_info/example/ios/Runner/AppDelegate.m b/packages/device_info/device_info/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..30b87969f44a --- /dev/null +++ b/packages/device_info/device_info/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/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from packages/android_intent/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/device_info/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/packages/battery/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/device_info/device_info/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from packages/battery/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/device_info/device_info/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/battery/example/ios/Runner/Base.lproj/Main.storyboard b/packages/device_info/device_info/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/battery/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/device_info/device_info/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/packages/device_info/example/ios/Runner/Info.plist b/packages/device_info/device_info/example/ios/Runner/Info.plist similarity index 100% rename from packages/device_info/example/ios/Runner/Info.plist rename to packages/device_info/device_info/example/ios/Runner/Info.plist diff --git a/packages/device_info/device_info/example/ios/Runner/main.m b/packages/device_info/device_info/example/ios/Runner/main.m new file mode 100644 index 000000000000..f97b9ef5c8a1 --- /dev/null +++ b/packages/device_info/device_info/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/device_info/device_info/example/lib/main.dart b/packages/device_info/device_info/example/lib/main.dart new file mode 100644 index 000000000000..44e3fb4ee2f7 --- /dev/null +++ b/packages/device_info/device_info/example/lib/main.dart @@ -0,0 +1,146 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; + +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:device_info/device_info.dart'; + +void main() { + runZonedGuarded(() { + runApp(MyApp()); + }, (dynamic error, dynamic stack) { + print(error); + print(stack); + }); +} + +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State { + static final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); + Map _deviceData = {}; + + @override + void initState() { + super.initState(); + initPlatformState(); + } + + Future initPlatformState() async { + Map deviceData = {}; + + try { + if (Platform.isAndroid) { + deviceData = _readAndroidBuildData(await deviceInfoPlugin.androidInfo); + } else if (Platform.isIOS) { + deviceData = _readIosDeviceInfo(await deviceInfoPlugin.iosInfo); + } + } on PlatformException { + deviceData = { + 'Error:': 'Failed to get platform version.' + }; + } + + if (!mounted) return; + + setState(() { + _deviceData = deviceData; + }); + } + + Map _readAndroidBuildData(AndroidDeviceInfo build) { + return { + 'version.securityPatch': build.version.securityPatch, + 'version.sdkInt': build.version.sdkInt, + 'version.release': build.version.release, + 'version.previewSdkInt': build.version.previewSdkInt, + 'version.incremental': build.version.incremental, + 'version.codename': build.version.codename, + 'version.baseOS': build.version.baseOS, + 'board': build.board, + 'bootloader': build.bootloader, + 'brand': build.brand, + 'device': build.device, + 'display': build.display, + 'fingerprint': build.fingerprint, + 'hardware': build.hardware, + 'host': build.host, + 'id': build.id, + 'manufacturer': build.manufacturer, + 'model': build.model, + 'product': build.product, + 'supported32BitAbis': build.supported32BitAbis, + 'supported64BitAbis': build.supported64BitAbis, + 'supportedAbis': build.supportedAbis, + 'tags': build.tags, + 'type': build.type, + 'isPhysicalDevice': build.isPhysicalDevice, + 'androidId': build.androidId, + 'systemFeatures': build.systemFeatures, + }; + } + + Map _readIosDeviceInfo(IosDeviceInfo data) { + return { + 'name': data.name, + 'systemName': data.systemName, + 'systemVersion': data.systemVersion, + 'model': data.model, + 'localizedModel': data.localizedModel, + 'identifierForVendor': data.identifierForVendor, + 'isPhysicalDevice': data.isPhysicalDevice, + 'utsname.sysname:': data.utsname.sysname, + 'utsname.nodename:': data.utsname.nodename, + 'utsname.release:': data.utsname.release, + 'utsname.version:': data.utsname.version, + 'utsname.machine:': data.utsname.machine, + }; + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: Text( + Platform.isAndroid ? 'Android Device Info' : 'iOS Device Info'), + ), + body: ListView( + children: _deviceData.keys.map((String property) { + return Row( + children: [ + Container( + padding: const EdgeInsets.all(10.0), + child: Text( + property, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + ), + Expanded( + child: Container( + padding: const EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0), + child: Text( + '${_deviceData[property]}', + maxLines: 10, + overflow: TextOverflow.ellipsis, + ), + )), + ], + ); + }).toList(), + ), + ), + ); + } +} diff --git a/packages/device_info/device_info/example/pubspec.yaml b/packages/device_info/device_info/example/pubspec.yaml new file mode 100644 index 000000000000..c4e84f60de5e --- /dev/null +++ b/packages/device_info/device_info/example/pubspec.yaml @@ -0,0 +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: + 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 new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/device_info/device_info/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/battery/ios/Assets/.gitkeep b/packages/device_info/device_info/ios/Assets/.gitkeep similarity index 100% rename from packages/battery/ios/Assets/.gitkeep rename to packages/device_info/device_info/ios/Assets/.gitkeep diff --git a/packages/device_info/ios/Classes/FLTDeviceInfoPlugin.h b/packages/device_info/device_info/ios/Classes/FLTDeviceInfoPlugin.h similarity index 76% rename from packages/device_info/ios/Classes/FLTDeviceInfoPlugin.h rename to packages/device_info/device_info/ios/Classes/FLTDeviceInfoPlugin.h index b5e95ed10e84..511b5b893fcb 100644 --- a/packages/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/ios/Classes/FLTDeviceInfoPlugin.m b/packages/device_info/device_info/ios/Classes/FLTDeviceInfoPlugin.m similarity index 96% rename from packages/device_info/ios/Classes/FLTDeviceInfoPlugin.m rename to packages/device_info/device_info/ios/Classes/FLTDeviceInfoPlugin.m index 423896061ac7..3d4d25ad9c6e 100644 --- a/packages/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/ios/device_info.podspec b/packages/device_info/device_info/ios/device_info.podspec similarity index 100% rename from packages/device_info/ios/device_info.podspec rename to packages/device_info/device_info/ios/device_info.podspec diff --git a/packages/device_info/device_info/lib/device_info.dart b/packages/device_info/device_info/lib/device_info.dart new file mode 100644 index 000000000000..1153ac6f7da6 --- /dev/null +++ b/packages/device_info/device_info/lib/device_info.dart @@ -0,0 +1,35 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'package:device_info_platform_interface/device_info_platform_interface.dart'; + +export 'package:device_info_platform_interface/device_info_platform_interface.dart' + show AndroidBuildVersion, AndroidDeviceInfo, IosDeviceInfo, IosUtsname; + +/// Provides device and operating system information. +class DeviceInfoPlugin { + /// No work is done when instantiating the plugin. It's safe to call this + /// repeatedly or in performance-sensitive blocks. + DeviceInfoPlugin(); + + /// This information does not change from call to call. Cache it. + AndroidDeviceInfo? _cachedAndroidDeviceInfo; + + /// Information derived from `android.os.Build`. + /// + /// See: https://developer.android.com/reference/android/os/Build.html + Future get androidInfo async => + _cachedAndroidDeviceInfo ??= + await DeviceInfoPlatform.instance.androidInfo(); + + /// This information does not change from call to call. Cache it. + IosDeviceInfo? _cachedIosDeviceInfo; + + /// Information derived from `UIDevice`. + /// + /// See: https://developer.apple.com/documentation/uikit/uidevice + Future get iosInfo async => + _cachedIosDeviceInfo ??= await DeviceInfoPlatform.instance.iosInfo(); +} diff --git a/packages/device_info/device_info/pubspec.yaml b/packages/device_info/device_info/pubspec.yaml new file mode 100644 index 000000000000..c5830f401039 --- /dev/null +++ b/packages/device_info/device_info/pubspec.yaml @@ -0,0 +1,29 @@ +name: device_info +description: Flutter plugin providing detailed information about the device + (make, model, etc.), and Android or iOS version the app is running on. +repository: https://github.com/flutter/plugins/tree/master/packages/device_info/device_info +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+device_info%22 +version: 2.0.2 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.12.13+hotfix.5" + +flutter: + plugin: + platforms: + android: + package: io.flutter.plugins.deviceinfo + pluginClass: DeviceInfoPlugin + ios: + pluginClass: FLTDeviceInfoPlugin + +dependencies: + flutter: + sdk: flutter + device_info_platform_interface: ^2.0.0 +dev_dependencies: + test: ^1.16.3 + flutter_test: + sdk: flutter + pedantic: ^1.10.0 diff --git a/packages/device_info/device_info_android.iml b/packages/device_info/device_info_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/device_info/device_info_android.iml +++ /dev/null @@ -1,12 +0,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 new file mode 100644 index 000000000000..438d5bccad40 --- /dev/null +++ b/packages/device_info/device_info_platform_interface/CHANGELOG.md @@ -0,0 +1,21 @@ +## 2.0.1 + +* Update platform_plugin_interface version requirement. + +## 2.0.0 + +* Migrate to null safety. +* Make `baseOS`, `previewSdkInt`, and `securityPatch` nullable types. +* Remove default values for non-nullable types. + +## 1.0.2 + +- Update Flutter SDK constraint. + +## 1.0.1 + +- Documentation typo fixed. + +## 1.0.0 + +- Initial open-source release. diff --git a/packages/device_info/device_info_platform_interface/LICENSE b/packages/device_info/device_info_platform_interface/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/device_info/device_info_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/device_info/device_info_platform_interface/README.md b/packages/device_info/device_info_platform_interface/README.md new file mode 100644 index 000000000000..1391ffded5ee --- /dev/null +++ b/packages/device_info/device_info_platform_interface/README.md @@ -0,0 +1,26 @@ +# device_info_platform_interface + +A common platform interface for the [`device_info`][1] plugin. + +This interface allows platform-specific implementations of the `device_info` +plugin, as well as the plugin itself, to ensure they are supporting the +same interface. + +# Usage + +To implement a new platform-specific implementation of `device_info`, extend +[`DeviceInfoPlatform`][2] with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`DeviceInfoPlatform` by calling +`DeviceInfoPlatform.instance = MyPlatformDeviceInfo()`. + +# Note on breaking changes + +Strongly prefer non-breaking changes (such as adding a method to the interface) +over breaking changes for this package. + +See https://flutter.dev/go/platform-interface-breaking-changes for a discussion +on why a less-clean interface is preferable to a breaking change. + +[1]: ../device_info +[2]: lib/device_info_platform_interface.dart diff --git a/packages/device_info/device_info_platform_interface/lib/device_info_platform_interface.dart b/packages/device_info/device_info_platform_interface/lib/device_info_platform_interface.dart new file mode 100644 index 000000000000..a40363b2dcb6 --- /dev/null +++ b/packages/device_info/device_info_platform_interface/lib/device_info_platform_interface.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:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'method_channel/method_channel_device_info.dart'; +import 'model/android_device_info.dart'; +import 'model/ios_device_info.dart'; +export 'model/android_device_info.dart'; +export 'model/ios_device_info.dart'; + +/// The interface that implementations of device_info must implement. +/// +/// Platform implementations should extend this class rather than implement it as `device_info` +/// does not consider newly added methods to be breaking changes. Extending this class +/// (using `extends`) ensures that the subclass will get the default implementation, while +/// platform implementations that `implements` this interface will be broken by newly added +/// [DeviceInfoPlatform] methods. +abstract class DeviceInfoPlatform extends PlatformInterface { + /// Constructs a DeviceInfoPlatform. + DeviceInfoPlatform() : super(token: _token); + + static final Object _token = Object(); + + static DeviceInfoPlatform _instance = MethodChannelDeviceInfo(); + + /// The default instance of [DeviceInfoPlatform] to use. + /// + /// Defaults to [MethodChannelDeviceInfo]. + static DeviceInfoPlatform get instance => _instance; + + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [DeviceInfoPlatform] when they register themselves. + static set instance(DeviceInfoPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + /// Gets the Android device information. + Future androidInfo() { + throw UnimplementedError('androidInfo() has not been implemented.'); + } + + /// Gets the iOS device information. + Future iosInfo() { + throw UnimplementedError('iosInfo() has not been implemented.'); + } +} diff --git a/packages/device_info/device_info_platform_interface/lib/method_channel/method_channel_device_info.dart b/packages/device_info/device_info_platform_interface/lib/method_channel/method_channel_device_info.dart new file mode 100644 index 000000000000..3c19e57f66a8 --- /dev/null +++ b/packages/device_info/device_info_platform_interface/lib/method_channel/method_channel_device_info.dart @@ -0,0 +1,30 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart'; +import 'package:device_info_platform_interface/device_info_platform_interface.dart'; + +/// An implementation of [DeviceInfoPlatform] that uses method channels. +class MethodChannelDeviceInfo extends DeviceInfoPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + MethodChannel channel = MethodChannel('plugins.flutter.io/device_info'); + + // Method channel for Android devices + Future androidInfo() async { + return AndroidDeviceInfo.fromMap((await channel + .invokeMapMethod('getAndroidDeviceInfo')) ?? + {}); + } + + // Method channel for iOS devices + Future iosInfo() async { + return IosDeviceInfo.fromMap( + (await channel.invokeMapMethod('getIosDeviceInfo')) ?? + {}); + } +} diff --git a/packages/device_info/device_info_platform_interface/lib/model/android_device_info.dart b/packages/device_info/device_info_platform_interface/lib/model/android_device_info.dart new file mode 100644 index 000000000000..b61dc14a0420 --- /dev/null +++ b/packages/device_info/device_info_platform_interface/lib/model/android_device_info.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. + +/// Information derived from `android.os.Build`. +/// +/// See: https://developer.android.com/reference/android/os/Build.html +class AndroidDeviceInfo { + /// Android device Info class. + AndroidDeviceInfo({ + required this.version, + required this.board, + required this.bootloader, + required this.brand, + required this.device, + required this.display, + required this.fingerprint, + required this.hardware, + required this.host, + required this.id, + required this.manufacturer, + required this.model, + required this.product, + required List supported32BitAbis, + required List supported64BitAbis, + required List supportedAbis, + required this.tags, + required this.type, + required this.isPhysicalDevice, + required this.androidId, + required List systemFeatures, + }) : supported32BitAbis = List.unmodifiable(supported32BitAbis), + supported64BitAbis = List.unmodifiable(supported64BitAbis), + supportedAbis = List.unmodifiable(supportedAbis), + systemFeatures = List.unmodifiable(systemFeatures); + + /// Android operating system version values derived from `android.os.Build.VERSION`. + final AndroidBuildVersion version; + + /// The name of the underlying board, like "goldfish". + /// + /// The value is an empty String if it is not available. + final String board; + + /// The system bootloader version number. + /// + /// The value is an empty String if it is not available. + final String bootloader; + + /// The consumer-visible brand with which the product/hardware will be associated, if any. + /// + /// The value is an empty String if it is not available. + final String brand; + + /// The name of the industrial design. + /// + /// The value is an empty String if it is not available. + final String device; + + /// A build ID string meant for displaying to the user. + /// + /// The value is an empty String if it is not available. + final String display; + + /// A string that uniquely identifies this build. + /// + /// The value is an empty String if it is not available. + final String fingerprint; + + /// The name of the hardware (from the kernel command line or /proc). + /// + /// The value is an empty String if it is not available. + final String hardware; + + /// Hostname. + /// + /// The value is an empty String if it is not available. + final String host; + + /// Either a changelist number, or a label like "M4-rc20". + /// + /// The value is an empty String if it is not available. + final String id; + + /// The manufacturer of the product/hardware. + /// + /// The value is an empty String if it is not available. + final String manufacturer; + + /// The end-user-visible name for the end product. + /// + /// The value is an empty String if it is not available. + final String model; + + /// The name of the overall product. + /// + /// The value is an empty String if it is not available. + final String product; + + /// An ordered list of 32 bit ABIs supported by this device. + final List supported32BitAbis; + + /// An ordered list of 64 bit ABIs supported by this device. + final List supported64BitAbis; + + /// An ordered list of ABIs supported by this device. + final List supportedAbis; + + /// Comma-separated tags describing the build, like "unsigned,debug". + /// + /// The value is an empty String if it is not available. + final String tags; + + /// The type of build, like "user" or "eng". + /// + /// The value is an empty String if it is not available. + final String type; + + /// The value is `true` if the application is running on a physical device. + /// + /// The value is `false` when the application is running on a emulator, or the value is unavailable. + final bool isPhysicalDevice; + + /// The Android hardware device ID that is unique between the device + user and app signing. + /// + /// The value is an empty String if it is not available. + final String androidId; + + /// Describes what features are available on the current device. + /// + /// This can be used to check if the device has, for example, a front-facing + /// camera, or a touchscreen. However, in many cases this is not the best + /// API to use. For example, if you are interested in bluetooth, this API + /// can tell you if the device has a bluetooth radio, but it cannot tell you + /// if bluetooth is currently enabled, or if you have been granted the + /// necessary permissions to use it. Please *only* use this if there is no + /// other way to determine if a feature is supported. + /// + /// This data comes from Android's PackageManager.getSystemAvailableFeatures, + /// and many of the common feature strings to look for are available in + /// PackageManager's public documentation: + /// https://developer.android.com/reference/android/content/pm/PackageManager + final List systemFeatures; + + /// Deserializes from the message received from [_kChannel]. + static AndroidDeviceInfo fromMap(Map map) { + return AndroidDeviceInfo( + version: AndroidBuildVersion._fromMap(map['version'] != null + ? map['version'].cast() + : {}), + board: map['board'] ?? '', + bootloader: map['bootloader'] ?? '', + brand: map['brand'] ?? '', + device: map['device'] ?? '', + display: map['display'] ?? '', + fingerprint: map['fingerprint'] ?? '', + hardware: map['hardware'] ?? '', + host: map['host'] ?? '', + id: map['id'] ?? '', + manufacturer: map['manufacturer'] ?? '', + model: map['model'] ?? '', + product: map['product'] ?? '', + supported32BitAbis: _fromList(map['supported32BitAbis']), + supported64BitAbis: _fromList(map['supported64BitAbis']), + supportedAbis: _fromList(map['supportedAbis']), + tags: map['tags'] ?? '', + type: map['type'] ?? '', + isPhysicalDevice: map['isPhysicalDevice'] ?? false, + androidId: map['androidId'] ?? '', + systemFeatures: _fromList(map['systemFeatures']), + ); + } + + /// Deserializes message as List + static List _fromList(dynamic message) { + if (message == null) { + return []; + } + assert(message is List); + final List list = List.from(message) + ..removeWhere((value) => value == null); + return list.cast(); + } +} + +/// Version values of the current Android operating system build derived from +/// `android.os.Build.VERSION`. +/// +/// See: https://developer.android.com/reference/android/os/Build.VERSION.html +class AndroidBuildVersion { + AndroidBuildVersion._({ + this.baseOS, + this.previewSdkInt, + this.securityPatch, + required this.codename, + required this.incremental, + required this.release, + required this.sdkInt, + }); + + /// The base OS build the product is based on. + /// This is only available on Android 6.0 or above. + String? baseOS; + + /// The developer preview revision of a prerelease SDK. + /// This is only available on Android 6.0 or above. + int? previewSdkInt; + + /// The user-visible security patch level. + /// This is only available on Android 6.0 or above. + final String? securityPatch; + + /// The current development codename, or the string "REL" if this is a release build. + /// + /// The value is an empty String if it is not available. + final String codename; + + /// The internal value used by the underlying source control to represent this build. + /// + /// The value is an empty String if it is not available. + final String incremental; + + /// The user-visible version string. + /// + /// The value is an empty String if it is not available. + final String release; + + /// The user-visible SDK version of the framework. + /// + /// Possible values are defined in: https://developer.android.com/reference/android/os/Build.VERSION_CODES.html + /// + /// The value is -1 if it is unavailable. + final int sdkInt; + + /// Deserializes from the map message received from [_kChannel]. + static AndroidBuildVersion _fromMap(Map map) { + return AndroidBuildVersion._( + baseOS: map['baseOS'], + previewSdkInt: map['previewSdkInt'], + securityPatch: map['securityPatch'], + codename: map['codename'] ?? '', + incremental: map['incremental'] ?? '', + release: map['release'] ?? '', + sdkInt: map['sdkInt'] ?? -1, + ); + } +} diff --git a/packages/device_info/device_info_platform_interface/lib/model/ios_device_info.dart b/packages/device_info/device_info_platform_interface/lib/model/ios_device_info.dart new file mode 100644 index 000000000000..17c96a3e250a --- /dev/null +++ b/packages/device_info/device_info_platform_interface/lib/model/ios_device_info.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. + +/// Information derived from `UIDevice`. +/// +/// See: https://developer.apple.com/documentation/uikit/uidevice +class IosDeviceInfo { + /// IOS device info class. + IosDeviceInfo({ + required this.name, + required this.systemName, + required this.systemVersion, + required this.model, + required this.localizedModel, + required this.identifierForVendor, + required this.isPhysicalDevice, + required this.utsname, + }); + + /// Device name. + /// + /// The value is an empty String if it is not available. + final String name; + + /// The name of the current operating system. + /// + /// The value is an empty String if it is not available. + final String systemName; + + /// The current operating system version. + /// + /// The value is an empty String if it is not available. + final String systemVersion; + + /// Device model. + /// + /// The value is an empty String if it is not available. + final String model; + + /// Localized name of the device model. + /// + /// The value is an empty String if it is not available. + final String localizedModel; + + /// Unique UUID value identifying the current device. + /// + /// The value is an empty String if it is not available. + final String identifierForVendor; + + /// The value is `true` if the application is running on a physical device. + /// + /// The value is `false` when the application is running on a simulator, or the value is unavailable. + final bool isPhysicalDevice; + + /// Operating system information derived from `sys/utsname.h`. + /// + /// The value is an empty String if it is not available. + final IosUtsname utsname; + + /// Deserializes from the map message received from [_kChannel]. + static IosDeviceInfo fromMap(Map map) { + return IosDeviceInfo( + name: map['name'] ?? '', + systemName: map['systemName'] ?? '', + systemVersion: map['systemVersion'] ?? '', + model: map['model'] ?? '', + localizedModel: map['localizedModel'] ?? '', + identifierForVendor: map['identifierForVendor'] ?? '', + isPhysicalDevice: map['isPhysicalDevice'] != null + ? map['isPhysicalDevice'] == 'true' + : false, + utsname: IosUtsname._fromMap(map['utsname'] != null + ? map['utsname'].cast() + : {}), + ); + } +} + +/// Information derived from `utsname`. +/// See http://pubs.opengroup.org/onlinepubs/7908799/xsh/sysutsname.h.html for details. +class IosUtsname { + IosUtsname._({ + required this.sysname, + required this.nodename, + required this.release, + required this.version, + required this.machine, + }); + + /// Operating system name. + /// + /// The value is an empty String if it is not available. + final String sysname; + + /// Network node name. + /// + /// The value is an empty String if it is not available. + final String nodename; + + /// Release level. + /// + /// The value is an empty String if it is not available. + final String release; + + /// Version level. + /// + /// The value is an empty String if it is not available. + final String version; + + /// Hardware type (e.g. 'iPhone7,1' for iPhone 6 Plus). + /// + /// The value is an empty String if it is not available. + final String machine; + + /// Deserializes from the map message received from [_kChannel]. + static IosUtsname _fromMap(Map map) { + return IosUtsname._( + sysname: map['sysname'] ?? '', + nodename: map['nodename'] ?? '', + release: map['release'] ?? '', + version: map['version'] ?? '', + machine: map['machine'] ?? '', + ); + } +} diff --git a/packages/device_info/device_info_platform_interface/pubspec.yaml b/packages/device_info/device_info_platform_interface/pubspec.yaml new file mode 100644 index 000000000000..cf3e50f98422 --- /dev/null +++ b/packages/device_info/device_info_platform_interface/pubspec.yaml @@ -0,0 +1,23 @@ +name: device_info_platform_interface +description: A common platform interface for the device_info plugin. +repository: https://github.com/flutter/plugins/tree/master/packages/device_info/device_info_platform_interface +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+device_info%22 +# NOTE: We strongly prefer non-breaking changes, even at the expense of a +# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes +version: 2.0.1 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.9.1+hotfix.4" + +dependencies: + flutter: + sdk: flutter + meta: ^1.3.0 + plugin_platform_interface: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + test: ^1.16.3 + pedantic: ^1.10.0 diff --git a/packages/device_info/device_info_platform_interface/test/method_channel_device_info_test.dart b/packages/device_info/device_info_platform_interface/test/method_channel_device_info_test.dart new file mode 100644 index 000000000000..14ed7c0aefb4 --- /dev/null +++ b/packages/device_info/device_info_platform_interface/test/method_channel_device_info_test.dart @@ -0,0 +1,373 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:device_info_platform_interface/device_info_platform_interface.dart'; +import 'package:device_info_platform_interface/method_channel/method_channel_device_info.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group("$MethodChannelDeviceInfo", () { + late MethodChannelDeviceInfo methodChannelDeviceInfo; + + setUp(() async { + methodChannelDeviceInfo = MethodChannelDeviceInfo(); + + methodChannelDeviceInfo.channel + .setMockMethodCallHandler((MethodCall methodCall) async { + switch (methodCall.method) { + case 'getAndroidDeviceInfo': + return ({ + "version": { + "securityPatch": "2018-09-05", + "sdkInt": 28, + "release": "9", + "previewSdkInt": 0, + "incremental": "5124027", + "codename": "REL", + "baseOS": "", + }, + "board": "goldfish_x86_64", + "bootloader": "unknown", + "brand": "google", + "device": "generic_x86_64", + "display": "PSR1.180720.075", + "fingerprint": + "google/sdk_gphone_x86_64/generic_x86_64:9/PSR1.180720.075/5124027:user/release-keys", + "hardware": "ranchu", + "host": "abfarm730", + "id": "PSR1.180720.075", + "manufacturer": "Google", + "model": "Android SDK built for x86_64", + "product": "sdk_gphone_x86_64", + "supported32BitAbis": [ + "x86", + ], + "supported64BitAbis": [ + "x86_64", + ], + "supportedAbis": [ + "x86_64", + "x86", + ], + "tags": "release-keys", + "type": "user", + "isPhysicalDevice": false, + "androidId": "f47571f3b4648f45", + "systemFeatures": [ + "android.hardware.sensor.proximity", + "android.software.adoptable_storage", + "android.hardware.sensor.accelerometer", + "android.hardware.faketouch", + "android.software.backup", + "android.hardware.touchscreen", + ], + }); + case 'getIosDeviceInfo': + return ({ + "name": "iPhone 13", + "systemName": "iOS", + "systemVersion": "13.0", + "model": "iPhone", + "localizedModel": "iPhone", + "identifierForVendor": "88F59280-55AD-402C-B922-3203B4794C06", + "isPhysicalDevice": false, + "utsname": { + "sysname": "Darwin", + "nodename": "host", + "release": "19.6.0", + "version": + "Darwin Kernel Version 19.6.0: Thu Jun 18 20:49:00 PDT 2020; root:xnu-6153.141.1~1/RELEASE_X86_64", + "machine": "x86_64", + } + }); + default: + return null; + } + }); + }); + + test("androidInfo", () async { + final AndroidDeviceInfo result = + await methodChannelDeviceInfo.androidInfo(); + + expect(result.version.securityPatch, "2018-09-05"); + expect(result.version.sdkInt, 28); + expect(result.version.release, "9"); + expect(result.version.previewSdkInt, 0); + expect(result.version.incremental, "5124027"); + expect(result.version.codename, "REL"); + expect(result.board, "goldfish_x86_64"); + expect(result.bootloader, "unknown"); + expect(result.brand, "google"); + expect(result.device, "generic_x86_64"); + expect(result.display, "PSR1.180720.075"); + expect(result.fingerprint, + "google/sdk_gphone_x86_64/generic_x86_64:9/PSR1.180720.075/5124027:user/release-keys"); + expect(result.hardware, "ranchu"); + expect(result.host, "abfarm730"); + expect(result.id, "PSR1.180720.075"); + expect(result.manufacturer, "Google"); + expect(result.model, "Android SDK built for x86_64"); + expect(result.product, "sdk_gphone_x86_64"); + expect(result.supported32BitAbis, [ + "x86", + ]); + expect(result.supported64BitAbis, [ + "x86_64", + ]); + expect(result.supportedAbis, [ + "x86_64", + "x86", + ]); + expect(result.tags, "release-keys"); + expect(result.type, "user"); + expect(result.isPhysicalDevice, false); + expect(result.androidId, "f47571f3b4648f45"); + expect(result.systemFeatures, [ + "android.hardware.sensor.proximity", + "android.software.adoptable_storage", + "android.hardware.sensor.accelerometer", + "android.hardware.faketouch", + "android.software.backup", + "android.hardware.touchscreen", + ]); + }); + + test("iosInfo", () async { + final IosDeviceInfo result = await methodChannelDeviceInfo.iosInfo(); + expect(result.name, "iPhone 13"); + expect(result.systemName, "iOS"); + expect(result.systemVersion, "13.0"); + expect(result.model, "iPhone"); + expect(result.localizedModel, "iPhone"); + expect( + result.identifierForVendor, "88F59280-55AD-402C-B922-3203B4794C06"); + expect(result.isPhysicalDevice, false); + expect(result.utsname.sysname, "Darwin"); + expect(result.utsname.nodename, "host"); + expect(result.utsname.release, "19.6.0"); + expect(result.utsname.version, + "Darwin Kernel Version 19.6.0: Thu Jun 18 20:49:00 PDT 2020; root:xnu-6153.141.1~1/RELEASE_X86_64"); + expect(result.utsname.machine, "x86_64"); + }); + }); + + group( + "$MethodChannelDeviceInfo handles null value in the map returned from method channel", + () { + late MethodChannelDeviceInfo methodChannelDeviceInfo; + + setUp(() async { + methodChannelDeviceInfo = MethodChannelDeviceInfo(); + + methodChannelDeviceInfo.channel + .setMockMethodCallHandler((MethodCall methodCall) async { + switch (methodCall.method) { + case 'getAndroidDeviceInfo': + return ({ + "version": null, + "board": null, + "bootloader": null, + "brand": null, + "device": null, + "display": null, + "fingerprint": null, + "hardware": null, + "host": null, + "id": null, + "manufacturer": null, + "model": null, + "product": null, + "supported32BitAbis": null, + "supported64BitAbis": null, + "supportedAbis": null, + "tags": null, + "type": null, + "isPhysicalDevice": null, + "androidId": null, + "systemFeatures": null, + }); + case 'getIosDeviceInfo': + return ({ + "name": null, + "systemName": null, + "systemVersion": null, + "model": null, + "localizedModel": null, + "identifierForVendor": null, + "isPhysicalDevice": null, + "utsname": null, + }); + default: + return null; + } + }); + }); + + test("androidInfo hanels null", () async { + final AndroidDeviceInfo result = + await methodChannelDeviceInfo.androidInfo(); + + expect(result.version.securityPatch, null); + expect(result.version.sdkInt, -1); + expect(result.version.release, ''); + expect(result.version.previewSdkInt, null); + expect(result.version.incremental, ''); + expect(result.version.codename, ''); + expect(result.board, ''); + expect(result.bootloader, ''); + expect(result.brand, ''); + expect(result.device, ''); + expect(result.display, ''); + expect(result.fingerprint, ''); + expect(result.hardware, ''); + expect(result.host, ''); + expect(result.id, ''); + expect(result.manufacturer, ''); + expect(result.model, ''); + expect(result.product, ''); + expect(result.supported32BitAbis, []); + expect(result.supported64BitAbis, []); + expect(result.supportedAbis, []); + expect(result.tags, ''); + expect(result.type, ''); + expect(result.isPhysicalDevice, false); + expect(result.androidId, ''); + expect(result.systemFeatures, []); + }); + + test("iosInfo handles null", () async { + final IosDeviceInfo result = await methodChannelDeviceInfo.iosInfo(); + expect(result.name, ''); + expect(result.systemName, ''); + expect(result.systemVersion, ''); + expect(result.model, ''); + expect(result.localizedModel, ''); + expect(result.identifierForVendor, ''); + expect(result.isPhysicalDevice, false); + expect(result.utsname.sysname, ''); + expect(result.utsname.nodename, ''); + expect(result.utsname.release, ''); + expect(result.utsname.version, ''); + expect(result.utsname.machine, ''); + }); + }); + + group("$MethodChannelDeviceInfo handles method channel returns null", () { + late MethodChannelDeviceInfo methodChannelDeviceInfo; + + setUp(() async { + methodChannelDeviceInfo = MethodChannelDeviceInfo(); + + methodChannelDeviceInfo.channel + .setMockMethodCallHandler((MethodCall methodCall) async { + switch (methodCall.method) { + case 'getAndroidDeviceInfo': + return null; + case 'getIosDeviceInfo': + return null; + default: + return null; + } + }); + }); + + test("androidInfo handles null", () async { + final AndroidDeviceInfo result = + await methodChannelDeviceInfo.androidInfo(); + + expect(result.version.securityPatch, null); + expect(result.version.sdkInt, -1); + expect(result.version.release, ''); + expect(result.version.previewSdkInt, null); + expect(result.version.incremental, ''); + expect(result.version.codename, ''); + expect(result.board, ''); + expect(result.bootloader, ''); + expect(result.brand, ''); + expect(result.device, ''); + expect(result.display, ''); + expect(result.fingerprint, ''); + expect(result.hardware, ''); + expect(result.host, ''); + expect(result.id, ''); + expect(result.manufacturer, ''); + expect(result.model, ''); + expect(result.product, ''); + expect(result.supported32BitAbis, []); + expect(result.supported64BitAbis, []); + expect(result.supportedAbis, []); + expect(result.tags, ''); + expect(result.type, ''); + expect(result.isPhysicalDevice, false); + expect(result.androidId, ''); + expect(result.systemFeatures, []); + }); + + test("iosInfo handles null", () async { + final IosDeviceInfo result = await methodChannelDeviceInfo.iosInfo(); + expect(result.name, ''); + expect(result.systemName, ''); + expect(result.systemVersion, ''); + expect(result.model, ''); + expect(result.localizedModel, ''); + expect(result.identifierForVendor, ''); + expect(result.isPhysicalDevice, false); + expect(result.utsname.sysname, ''); + expect(result.utsname.nodename, ''); + expect(result.utsname.release, ''); + expect(result.utsname.version, ''); + expect(result.utsname.machine, ''); + }); + }); + + group("$MethodChannelDeviceInfo android handles null values in list", () { + late MethodChannelDeviceInfo methodChannelDeviceInfo; + + setUp(() async { + methodChannelDeviceInfo = MethodChannelDeviceInfo(); + + methodChannelDeviceInfo.channel + .setMockMethodCallHandler((MethodCall methodCall) async { + switch (methodCall.method) { + case 'getAndroidDeviceInfo': + return ({ + "supported32BitAbis": ["x86"], + "supported64BitAbis": ["x86_64"], + "supportedAbis": ["x86_64", "x86"], + "systemFeatures": [ + "android.hardware.sensor.proximity", + "android.software.adoptable_storage", + "android.hardware.sensor.accelerometer", + "android.hardware.faketouch", + "android.software.backup", + "android.hardware.touchscreen", + ], + }); + default: + return null; + } + }); + }); + + test("androidInfo hanels null in list", () async { + final AndroidDeviceInfo result = + await methodChannelDeviceInfo.androidInfo(); + expect(result.supported32BitAbis, ['x86']); + expect(result.supported64BitAbis, ['x86_64']); + expect(result.supportedAbis, ['x86_64', 'x86']); + expect(result.systemFeatures, [ + "android.hardware.sensor.proximity", + "android.software.adoptable_storage", + "android.hardware.sensor.accelerometer", + "android.hardware.faketouch", + "android.software.backup", + "android.hardware.touchscreen" + ]); + }); + }); +} diff --git a/packages/device_info/example/README.md b/packages/device_info/example/README.md deleted file mode 100644 index 36ca6cc0600b..000000000000 --- a/packages/device_info/example/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# device_info_example - -Demonstrates how to use the `device_info` plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](http://flutter.io/). diff --git a/packages/device_info/example/android.iml b/packages/device_info/example/android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/device_info/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/device_info/example/android/app/build.gradle b/packages/device_info/example/android/app/build.gradle deleted file mode 100644 index 43d6d0a1a8c5..000000000000 --- a/packages/device_info/example/android/app/build.gradle +++ /dev/null @@ -1,60 +0,0 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -apply plugin: 'com.android.application' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -android { - compileSdkVersion 28 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - applicationId "io.flutter.plugins.deviceinfoexample" - minSdkVersion 16 - targetSdkVersion 28 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} - -dependencies { - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' -} diff --git a/packages/device_info/example/android/app/src/main/AndroidManifest.xml b/packages/device_info/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index f9f91fa39dae..000000000000 --- a/packages/device_info/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/packages/device_info/example/android/app/src/main/java/io/flutter/plugins/deviceinfoexample/EmbeddingV1Activity.java b/packages/device_info/example/android/app/src/main/java/io/flutter/plugins/deviceinfoexample/EmbeddingV1Activity.java deleted file mode 100644 index 48678a7e6ad1..000000000000 --- a/packages/device_info/example/android/app/src/main/java/io/flutter/plugins/deviceinfoexample/EmbeddingV1Activity.java +++ /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. - -package io.flutter.plugins.deviceinfoexample; - -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.deviceinfo.DeviceInfoPlugin; - -public class EmbeddingV1Activity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - DeviceInfoPlugin.registerWith(registrarFor("io.flutter.plugins.deviceinfo.DeviceInfoPlugin")); - } -} diff --git a/packages/device_info/example/android/app/src/main/java/io/flutter/plugins/deviceinfoexample/EmbeddingV1ActivityTest.java b/packages/device_info/example/android/app/src/main/java/io/flutter/plugins/deviceinfoexample/EmbeddingV1ActivityTest.java deleted file mode 100644 index 2bec9fb8e254..000000000000 --- a/packages/device_info/example/android/app/src/main/java/io/flutter/plugins/deviceinfoexample/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.flutter.plugins.deviceinfoexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/device_info/example/android/build.gradle b/packages/device_info/example/android/build.gradle deleted file mode 100644 index 83f114c21e31..000000000000 --- a/packages/device_info/example/android/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.6.1' - } -} - -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/device_info/example/device_info_example.iml b/packages/device_info/example/device_info_example.iml deleted file mode 100644 index 9d5dae19540c..000000000000 --- a/packages/device_info/example/device_info_example.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/device_info/example/device_info_example_android.iml b/packages/device_info/example/device_info_example_android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/device_info/example/device_info_example_android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/device_info/example/ios/Flutter/AppFrameworkInfo.plist b/packages/device_info/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6c2de8086bcd..000000000000 --- a/packages/device_info/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,30 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - UIRequiredDeviceCapabilities - - arm64 - - MinimumOSVersion - 8.0 - - diff --git a/packages/device_info/example/ios/Runner.xcodeproj/project.pbxproj b/packages/device_info/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 8f80e80abb09..000000000000 --- a/packages/device_info/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, ); }; }; - 4C26954642C9965233939F98 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 17470FCDF9FA37CB94B63753 /* 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 = ""; }; - 17470FCDF9FA37CB94B63753 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 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 = ""; }; - 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 = ""; }; - C8043F3EE7ED1716F368CC90 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - E96AF818D5456D130681C78B /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - 4C26954642C9965233939F98 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 20A0DD43C00A880430740858 /* Pods */ = { - isa = PBXGroup; - children = ( - C8043F3EE7ED1716F368CC90 /* Pods-Runner.debug.xcconfig */, - E96AF818D5456D130681C78B /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 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 */, - 20A0DD43C00A880430740858 /* Pods */, - EA17DAB2B097E79A4CABE344 /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - EA17DAB2B097E79A4CABE344 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 17470FCDF9FA37CB94B63753 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - B8856E1B697C88C1C62EE937 /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 2C129809545EBEEB9253436A /* [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 */ - 2C129809545EBEEB9253436A /* [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"; - }; - B8856E1B697C88C1C62EE937 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, - 97C146F31CF9000F007C117D /* main.m in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 97C147031CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - 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.deviceInfoExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.deviceInfoExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/packages/device_info/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/device_info/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a16ed0f..000000000000 --- a/packages/device_info/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/packages/device_info/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/device_info/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 3bb3697ef41c..000000000000 --- a/packages/device_info/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/device_info/example/ios/Runner/AppDelegate.h b/packages/device_info/example/ios/Runner/AppDelegate.h deleted file mode 100644 index d9e18e990f2e..000000000000 --- a/packages/device_info/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/device_info/example/ios/Runner/AppDelegate.m b/packages/device_info/example/ios/Runner/AppDelegate.m deleted file mode 100644 index f08675707182..000000000000 --- a/packages/device_info/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/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/device_info/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d22f10b2ab63..000000000000 --- a/packages/device_info/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/device_info/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/device_info/example/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index ebf48f603974..000000000000 --- a/packages/device_info/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/device_info/example/ios/Runner/main.m b/packages/device_info/example/ios/Runner/main.m deleted file mode 100644 index bec320c0bee0..000000000000 --- a/packages/device_info/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/device_info/example/lib/main.dart b/packages/device_info/example/lib/main.dart deleted file mode 100644 index 1c1064aa09ee..000000000000 --- a/packages/device_info/example/lib/main.dart +++ /dev/null @@ -1,146 +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 'dart:async'; - -import 'dart:io'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:device_info/device_info.dart'; - -void main() { - runZoned(() { - runApp(MyApp()); - }, onError: (dynamic error, dynamic stack) { - print(error); - print(stack); - }); -} - -class MyApp extends StatefulWidget { - @override - _MyAppState createState() => _MyAppState(); -} - -class _MyAppState extends State { - static final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); - Map _deviceData = {}; - - @override - void initState() { - super.initState(); - initPlatformState(); - } - - Future initPlatformState() async { - Map deviceData; - - try { - if (Platform.isAndroid) { - deviceData = _readAndroidBuildData(await deviceInfoPlugin.androidInfo); - } else if (Platform.isIOS) { - deviceData = _readIosDeviceInfo(await deviceInfoPlugin.iosInfo); - } - } on PlatformException { - deviceData = { - 'Error:': 'Failed to get platform version.' - }; - } - - if (!mounted) return; - - setState(() { - _deviceData = deviceData; - }); - } - - Map _readAndroidBuildData(AndroidDeviceInfo build) { - return { - 'version.securityPatch': build.version.securityPatch, - 'version.sdkInt': build.version.sdkInt, - 'version.release': build.version.release, - 'version.previewSdkInt': build.version.previewSdkInt, - 'version.incremental': build.version.incremental, - 'version.codename': build.version.codename, - 'version.baseOS': build.version.baseOS, - 'board': build.board, - 'bootloader': build.bootloader, - 'brand': build.brand, - 'device': build.device, - 'display': build.display, - 'fingerprint': build.fingerprint, - 'hardware': build.hardware, - 'host': build.host, - 'id': build.id, - 'manufacturer': build.manufacturer, - 'model': build.model, - 'product': build.product, - 'supported32BitAbis': build.supported32BitAbis, - 'supported64BitAbis': build.supported64BitAbis, - 'supportedAbis': build.supportedAbis, - 'tags': build.tags, - 'type': build.type, - 'isPhysicalDevice': build.isPhysicalDevice, - 'androidId': build.androidId, - 'systemFeatures': build.systemFeatures, - }; - } - - Map _readIosDeviceInfo(IosDeviceInfo data) { - return { - 'name': data.name, - 'systemName': data.systemName, - 'systemVersion': data.systemVersion, - 'model': data.model, - 'localizedModel': data.localizedModel, - 'identifierForVendor': data.identifierForVendor, - 'isPhysicalDevice': data.isPhysicalDevice, - 'utsname.sysname:': data.utsname.sysname, - 'utsname.nodename:': data.utsname.nodename, - 'utsname.release:': data.utsname.release, - 'utsname.version:': data.utsname.version, - 'utsname.machine:': data.utsname.machine, - }; - } - - @override - Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar( - title: Text( - Platform.isAndroid ? 'Android Device Info' : 'iOS Device Info'), - ), - body: ListView( - children: _deviceData.keys.map((String property) { - return Row( - children: [ - Container( - padding: const EdgeInsets.all(10.0), - child: Text( - property, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - ), - Expanded( - child: Container( - padding: const EdgeInsets.fromLTRB(0.0, 10.0, 0.0, 10.0), - child: Text( - '${_deviceData[property]}', - maxLines: 10, - overflow: TextOverflow.ellipsis, - ), - )), - ], - ); - }).toList(), - ), - ), - ); - } -} diff --git a/packages/device_info/example/pubspec.yaml b/packages/device_info/example/pubspec.yaml deleted file mode 100644 index dc63d8b66126..000000000000 --- a/packages/device_info/example/pubspec.yaml +++ /dev/null @@ -1,17 +0,0 @@ -name: device_info_example -description: Demonstrates how to use the device_info plugin. - -dependencies: - flutter: - sdk: flutter - device_info: - path: ../ - -dev_dependencies: - flutter_driver: - sdk: flutter - e2e: ^0.2.0 - pedantic: ^1.8.0 - -flutter: - uses-material-design: true diff --git a/packages/device_info/example/test_driver/device_info_e2e.dart b/packages/device_info/example/test_driver/device_info_e2e.dart deleted file mode 100644 index db8a73f579c1..000000000000 --- a/packages/device_info/example/test_driver/device_info_e2e.dart +++ /dev/null @@ -1,32 +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:io'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:device_info/device_info.dart'; -import 'package:e2e/e2e.dart'; - -void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); - - IosDeviceInfo iosInfo; - AndroidDeviceInfo androidInfo; - - setUpAll(() async { - final DeviceInfoPlugin deviceInfoPlugin = DeviceInfoPlugin(); - if (Platform.isIOS) { - iosInfo = await deviceInfoPlugin.iosInfo; - } else if (Platform.isAndroid) { - androidInfo = await deviceInfoPlugin.androidInfo; - } - }); - - testWidgets('Can get non-null device model', (WidgetTester tester) async { - if (Platform.isIOS) { - expect(iosInfo.model, isNotNull); - } else if (Platform.isAndroid) { - expect(androidInfo.model, isNotNull); - } - }); -} diff --git a/packages/device_info/example/test_driver/device_info_e2e_test.dart b/packages/device_info/example/test_driver/device_info_e2e_test.dart deleted file mode 100644 index f3aa9e218d82..000000000000 --- a/packages/device_info/example/test_driver/device_info_e2e_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 'dart:async'; -import 'dart:io'; -import 'package:flutter_driver/flutter_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); -} diff --git a/packages/device_info/lib/device_info.dart b/packages/device_info/lib/device_info.dart deleted file mode 100644 index 25b7d46cdb11..000000000000 --- a/packages/device_info/lib/device_info.dart +++ /dev/null @@ -1,324 +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'; - -/// Provides device and operating system information. -class DeviceInfoPlugin { - /// No work is done when instantiating the plugin. It's safe to call this - /// repeatedly or in performance-sensitive blocks. - DeviceInfoPlugin(); - - /// Channel used to communicate to native code. - static const MethodChannel channel = - MethodChannel('plugins.flutter.io/device_info'); - - /// This information does not change from call to call. Cache it. - AndroidDeviceInfo _cachedAndroidDeviceInfo; - - /// Information derived from `android.os.Build`. - /// - /// See: https://developer.android.com/reference/android/os/Build.html - Future get androidInfo async => - _cachedAndroidDeviceInfo ??= AndroidDeviceInfo._fromMap(await channel - .invokeMapMethod('getAndroidDeviceInfo')); - - /// This information does not change from call to call. Cache it. - IosDeviceInfo _cachedIosDeviceInfo; - - /// Information derived from `UIDevice`. - /// - /// See: https://developer.apple.com/documentation/uikit/uidevice - Future get iosInfo async => - _cachedIosDeviceInfo ??= IosDeviceInfo._fromMap( - await channel.invokeMapMethod('getIosDeviceInfo')); -} - -/// Information derived from `android.os.Build`. -/// -/// See: https://developer.android.com/reference/android/os/Build.html -class AndroidDeviceInfo { - AndroidDeviceInfo._({ - this.version, - this.board, - this.bootloader, - this.brand, - this.device, - this.display, - this.fingerprint, - this.hardware, - this.host, - this.id, - this.manufacturer, - this.model, - this.product, - List supported32BitAbis, - List supported64BitAbis, - List supportedAbis, - this.tags, - this.type, - this.isPhysicalDevice, - this.androidId, - List systemFeatures, - }) : supported32BitAbis = List.unmodifiable(supported32BitAbis), - supported64BitAbis = List.unmodifiable(supported64BitAbis), - supportedAbis = List.unmodifiable(supportedAbis), - systemFeatures = List.unmodifiable(systemFeatures); - - /// Android operating system version values derived from `android.os.Build.VERSION`. - final AndroidBuildVersion version; - - /// The name of the underlying board, like "goldfish". - final String board; - - /// The system bootloader version number. - final String bootloader; - - /// The consumer-visible brand with which the product/hardware will be associated, if any. - final String brand; - - /// The name of the industrial design. - final String device; - - /// A build ID string meant for displaying to the user. - final String display; - - /// A string that uniquely identifies this build. - final String fingerprint; - - /// The name of the hardware (from the kernel command line or /proc). - final String hardware; - - /// Hostname. - final String host; - - /// Either a changelist number, or a label like "M4-rc20". - final String id; - - /// The manufacturer of the product/hardware. - final String manufacturer; - - /// The end-user-visible name for the end product. - final String model; - - /// The name of the overall product. - final String product; - - /// An ordered list of 32 bit ABIs supported by this device. - final List supported32BitAbis; - - /// An ordered list of 64 bit ABIs supported by this device. - final List supported64BitAbis; - - /// An ordered list of ABIs supported by this device. - final List supportedAbis; - - /// Comma-separated tags describing the build, like "unsigned,debug". - final String tags; - - /// The type of build, like "user" or "eng". - final String type; - - /// `false` if the application is running in an emulator, `true` otherwise. - final bool isPhysicalDevice; - - /// The Android hardware device ID that is unique between the device + user and app signing. - final String androidId; - - /// Describes what features are available on the current device. - /// - /// This can be used to check if the device has, for example, a front-facing - /// camera, or a touchscreen. However, in many cases this is not the best - /// API to use. For example, if you are interested in bluetooth, this API - /// can tell you if the device has a bluetooth radio, but it cannot tell you - /// if bluetooth is currently enabled, or if you have been granted the - /// necessary permissions to use it. Please *only* use this if there is no - /// other way to determine if a feature is supported. - /// - /// This data comes from Android's PackageManager.getSystemAvailableFeatures, - /// and many of the common feature strings to look for are available in - /// PackageManager's public documentation: - /// https://developer.android.com/reference/android/content/pm/PackageManager - final List systemFeatures; - - /// Deserializes from the message received from [_kChannel]. - static AndroidDeviceInfo _fromMap(Map map) { - return AndroidDeviceInfo._( - version: - AndroidBuildVersion._fromMap(map['version']?.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']), - ); - } - - /// Deserializes message as List - static List _fromList(dynamic message) { - final List list = message; - return List.from(list); - } -} - -/// Version values of the current Android operating system build derived from -/// `android.os.Build.VERSION`. -/// -/// See: https://developer.android.com/reference/android/os/Build.VERSION.html -class AndroidBuildVersion { - AndroidBuildVersion._({ - this.baseOS, - this.codename, - this.incremental, - this.previewSdkInt, - this.release, - this.sdkInt, - this.securityPatch, - }); - - /// The base OS build the product is based on. - final String baseOS; - - /// The current development codename, or the string "REL" if this is a release build. - final String codename; - - /// The internal value used by the underlying source control to represent this build. - final String incremental; - - /// The developer preview revision of a prerelease SDK. - final int previewSdkInt; - - /// The user-visible version string. - 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 - 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'], - ); - } -} - -/// Information derived from `UIDevice`. -/// -/// See: https://developer.apple.com/documentation/uikit/uidevice -class IosDeviceInfo { - IosDeviceInfo._({ - this.name, - this.systemName, - this.systemVersion, - this.model, - this.localizedModel, - this.identifierForVendor, - this.isPhysicalDevice, - this.utsname, - }); - - /// Device name. - final String name; - - /// The name of the current operating system. - final String systemName; - - /// The current operating system version. - final String systemVersion; - - /// Device model. - final String model; - - /// Localized name of the device model. - final String localizedModel; - - /// Unique UUID value identifying the current device. - final String identifierForVendor; - - /// `false` if the application is running in a simulator, `true` otherwise. - final bool isPhysicalDevice; - - /// Operating system information derived from `sys/utsname.h`. - 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()), - ); - } -} - -/// Information derived from `utsname`. -/// 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, - }); - - /// Operating system name. - final String sysname; - - /// Network node name. - final String nodename; - - /// Release level. - final String release; - - /// Version level. - final String version; - - /// Hardware type (e.g. 'iPhone7,1' for iPhone 6 Plus). - final String machine; - - /// Deserializes from the map message received from [_kChannel]. - static IosUtsname _fromMap(Map map) { - return IosUtsname._( - sysname: map['sysname'], - nodename: map['nodename'], - release: map['release'], - version: map['version'], - machine: map['machine'], - ); - } -} diff --git a/packages/device_info/pubspec.yaml b/packages/device_info/pubspec.yaml deleted file mode 100644 index 8ad768a3c031..000000000000 --- a/packages/device_info/pubspec.yaml +++ /dev/null @@ -1,32 +0,0 @@ -name: device_info -description: Flutter plugin providing detailed information about the device - (make, model, etc.), and Android or iOS version the app is running on. -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+4 - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.deviceinfo - pluginClass: DeviceInfoPlugin - ios: - pluginClass: FLTDeviceInfoPlugin - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - test: ^1.3.0 - flutter_test: - sdk: flutter - e2e: ^0.2.0 - 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/e2e/CHANGELOG.md b/packages/e2e/CHANGELOG.md deleted file mode 100644 index 43572b4a8762..000000000000 --- a/packages/e2e/CHANGELOG.md +++ /dev/null @@ -1,132 +0,0 @@ -## 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 - `E2EWidgetsFlutterBinding`. -* 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/e2e/LICENSE b/packages/e2e/LICENSE deleted file mode 100644 index 0c382ce171cc..000000000000 --- a/packages/e2e/LICENSE +++ /dev/null @@ -1,27 +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/e2e/README.md b/packages/e2e/README.md index 84a9ba87e2a3..e86126e4cc56 100644 --- a/packages/e2e/README.md +++ b/packages/e2e/README.md @@ -1,190 +1,3 @@ -# e2e +# e2e (deprecated) -This package enables self-driving testing of Flutter code on devices and emulators. -It adapts flutter_test results into a format that is compatible with `flutter drive` -and native Android instrumentation testing. - -iOS support is not available yet, but is planned in the future. - -## Usage - -Add a dependency on the `e2e` package in the -`dev_dependencies` section of pubspec.yaml. For plugins, do this in the -pubspec.yaml of the example app. - -Invoke `E2EWidgetsFlutterBinding.ensureInitialized()` at the start -of a test file, e.g. - -```dart -import 'package:e2e/e2e.dart'; - -void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); - testWidgets("failing test example", (WidgetTester tester) async { - expect(2 + 2, equals(5)); - }); - exit(result == 'pass' ? 0 : 1); -} -``` - -## Test locations - -It is recommended to put e2e tests in the `test/` folder of the app or package. -For example apps, if the e2e test references example app code, it should go in -`example/test/`. It is also acceptable to put e2e tests in `test_driver/` folder -so that they're alongside the runner app (see below). - -## Using Flutter driver to run tests - -`E2EWidgetsTestBinding` supports launching the on-device tests with `flutter drive`. -Note that the tests don't use the `FlutterDriver` API, they use `testWidgets` instead. - -Put the a file named `_e2e_test.dart` in the app' `test_driver` directory: - -```dart -import 'dart:async'; - -import 'package:e2e/e2e_driver.dart' as e2e; - -Future main() async => e2e.main(); - -``` - -To run a example app test with Flutter driver: - -``` -cd example -flutter drive test/_e2e.dart -``` - -To test plugin APIs using Flutter driver: - -``` -cd example -flutter drive --driver=test_driver/_test.dart test/_e2e.dart -``` - -You can run tests on web in release or profile mode. - -First you need to make sure you have downloaded the driver for the browser. - -``` -cd example -flutter drive -v --target=test_driver/dart -d web-server --release --browser-name=chrome -``` - -## Android device testing - -Create an instrumentation test file in your application's -**android/app/src/androidTest/java/com/example/myapp/** directory (replacing -com, example, and myapp with values from your app's package name). You can name -this test file MainActivityTest.java or another name of your choice. - -```java -package com.example.myapp; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterTestRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -public class MainActivityTest { - @Rule - public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class, true, false); -} -``` - -Update your application's **myapp/android/app/build.gradle** to make sure it -uses androidx's version of AndroidJUnitRunner and has androidx libraries as a -dependency. - -``` -android { - ... - defaultConfig { - ... - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } -} - -dependencies { - testImplementation 'junit:junit:4.12' - - // https://developer.android.com/jetpack/androidx/releases/test/#1.2.0 - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' -} -``` - -To e2e test on a local Android device (emulated or physical): - -``` -./gradlew app:connectedAndroidTest -Ptarget=`pwd`/../test_driver/_e2e.dart -``` - -## Firebase Test Lab - -If this is your first time testing with Firebase Test Lab, you'll need to follow -the guides in the [Firebase test lab -documentation](https://firebase.google.com/docs/test-lab/?gclid=EAIaIQobChMIs5qVwqW25QIV8iCtBh3DrwyUEAAYASAAEgLFU_D_BwE) -to set up a project. - -To run an e2e test on Android devices using Firebase Test Lab, use gradle commands to build an -instrumentation test for Android. - -``` -pushd android -./gradlew app:assembleAndroidTest -./gradlew app:assembleDebug -Ptarget=.dart -popd -``` - -Upload the build apks Firebase Test Lab, making sure to replace , -, , and with your values. - -``` -gcloud auth activate-service-account --key-file= -gcloud --quiet config set project -gcloud firebase test android run --type instrumentation \ - --app build/app/outputs/apk/debug/app-debug.apk \ - --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk\ - --timeout 2m \ - --results-bucket= \ - --results-dir= -``` - -You can pass additional parameters on the command line, such as the -devices you want to test on. See -[gcloud firebase test android run](https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run). - -## iOS device testing - -You need to change `iOS/Podfile` to avoid test target statically linking to the plugins. One way is to -link all of the plugins dynamically: - -``` -target 'Runner' do - use_frameworks! - ... -end -``` - -To e2e test on your iOS device (simulator or real), rebuild your iOS targets with Flutter tool. - -``` -flutter build ios -t test_driver/_e2e.dart (--simulator) -``` - -Open Xcode project (by default, it's `ios/Runner.xcodeproj`). Create a test target -(navigating `File > New > Target...` and set up the values) and a test file `RunnerTests.m` and -change the code. You can change `RunnerTests.m` to the name of your choice. - -```objective-c -#import -#import - -E2E_IOS_RUNNER(RunnerTests) -``` - -Now you can start RunnerTests to kick out e2e tests! +This package has been moved to [integration_test](https://github.com/flutter/plugins/tree/master/packages/integration_test). diff --git a/packages/e2e/android/.gitignore b/packages/e2e/android/.gitignore deleted file mode 100644 index c6cbe562a427..000000000000 --- a/packages/e2e/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/e2e/android/build.gradle b/packages/e2e/android/build.gradle deleted file mode 100644 index c91d4721d3ac..000000000000 --- a/packages/e2e/android/build.gradle +++ /dev/null @@ -1,42 +0,0 @@ -group 'com.example.e2e' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.2.1' - } -} - -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' - } -} diff --git a/packages/e2e/android/gradle.properties b/packages/e2e/android/gradle.properties deleted file mode 100644 index 2bd6f4fda009..000000000000 --- a/packages/e2e/android/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M - diff --git a/packages/e2e/android/settings.gradle b/packages/e2e/android/settings.gradle deleted file mode 100644 index e5d17d080b60..000000000000 --- a/packages/e2e/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'e2e' diff --git a/packages/e2e/android/src/main/AndroidManifest.xml b/packages/e2e/android/src/main/AndroidManifest.xml deleted file mode 100644 index 33fdf86052ab..000000000000 --- a/packages/e2e/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/packages/e2e/android/src/main/java/dev/flutter/plugins/e2e/E2EPlugin.java b/packages/e2e/android/src/main/java/dev/flutter/plugins/e2e/E2EPlugin.java deleted file mode 100644 index f79f8f14595f..000000000000 --- a/packages/e2e/android/src/main/java/dev/flutter/plugins/e2e/E2EPlugin.java +++ /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. - -package dev.flutter.plugins.e2e; - -import android.content.Context; -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugin.common.PluginRegistry.Registrar; -import java.util.Map; -import java.util.concurrent.CompletableFuture; - -/** E2EPlugin */ -public class E2EPlugin implements MethodCallHandler, FlutterPlugin { - private MethodChannel methodChannel; - - public static CompletableFuture> testResults = new CompletableFuture<>(); - - private static final String CHANNEL = "plugins.flutter.io/e2e"; - - /** Plugin registration. */ - public static void registerWith(Registrar registrar) { - final E2EPlugin instance = new E2EPlugin(); - instance.onAttachedToEngine(registrar.context(), registrar.messenger()); - } - - @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { - onAttachedToEngine(binding.getApplicationContext(), binding.getBinaryMessenger()); - } - - private void onAttachedToEngine(Context applicationContext, BinaryMessenger messenger) { - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/e2e"); - 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"); - testResults.complete(results); - result.success(null); - } else { - result.notImplemented(); - } - } -} diff --git a/packages/e2e/android/src/main/java/dev/flutter/plugins/e2e/FlutterTestRunner.java b/packages/e2e/android/src/main/java/dev/flutter/plugins/e2e/FlutterTestRunner.java deleted file mode 100644 index 78f0c3c5bac2..000000000000 --- a/packages/e2e/android/src/main/java/dev/flutter/plugins/e2e/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.e2e; - -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 = E2EPlugin.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("failed")) { - Exception dummyException = new Exception(outcome); - notifier.fireTestFailure(new Failure(d, dummyException)); - } - notifier.fireTestFinished(d); - } - } -} diff --git a/packages/e2e/e2e_macos/LICENSE b/packages/e2e/e2e_macos/LICENSE deleted file mode 100644 index 0c382ce171cc..000000000000 --- a/packages/e2e/e2e_macos/LICENSE +++ /dev/null @@ -1,27 +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/e2e/e2e_macos/android/build.gradle b/packages/e2e/e2e_macos/android/build.gradle deleted file mode 100644 index 2796c5b87731..000000000000 --- a/packages/e2e/e2e_macos/android/build.gradle +++ /dev/null @@ -1,37 +0,0 @@ -group 'com.example.e2e' -version '1.0' - -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.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' - } -} diff --git a/packages/e2e/e2e_macos/android/gradle/wrapper/gradle-wrapper.properties b/packages/e2e/e2e_macos/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 019065d1d650..000000000000 --- a/packages/e2e/e2e_macos/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/e2e/e2e_macos/android/settings.gradle b/packages/e2e/e2e_macos/android/settings.gradle deleted file mode 100644 index e5d17d080b60..000000000000 --- a/packages/e2e/e2e_macos/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'e2e' diff --git a/packages/e2e/e2e_macos/android/src/main/AndroidManifest.xml b/packages/e2e/e2e_macos/android/src/main/AndroidManifest.xml deleted file mode 100644 index 3b85b192fa32..000000000000 --- a/packages/e2e/e2e_macos/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/packages/e2e/e2e_macos/android/src/main/java/com/example/e2e/E2ePlugin.java b/packages/e2e/e2e_macos/android/src/main/java/com/example/e2e/E2ePlugin.java deleted file mode 100644 index aa4842180499..000000000000 --- a/packages/e2e/e2e_macos/android/src/main/java/com/example/e2e/E2ePlugin.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.example.e2e; - -import androidx.annotation.NonNull; -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugin.common.PluginRegistry.Registrar; - -/** E2ePlugin */ -public class E2ePlugin implements FlutterPlugin, MethodCallHandler { - @Override - public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {} - - public static void registerWith(Registrar registrar) {} - - @Override - public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {} - - @Override - public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {} -} diff --git a/packages/e2e/e2e_macos/ios/e2e_macos.podspec b/packages/e2e/e2e_macos/ios/e2e_macos.podspec deleted file mode 100644 index 561933b15d08..000000000000 --- a/packages/e2e/e2e_macos/ios/e2e_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 = 'e2e_macos' - s.version = '0.0.1' - s.summary = 'No-op implementation of the e2e desktop plugin to avoid build issues on iOS' - s.description = <<-DESC - No-op implementation of e2e 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/e2e/e2e_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/e2e/e2e_macos/macos/Classes/E2EPlugin.swift b/packages/e2e/e2e_macos/macos/Classes/E2EPlugin.swift deleted file mode 100644 index c3a80a9bcdbd..000000000000 --- a/packages/e2e/e2e_macos/macos/Classes/E2EPlugin.swift +++ /dev/null @@ -1,22 +0,0 @@ -import FlutterMacOS - -public class E2EPlugin: NSObject, FlutterPlugin { - - public static func register(with registrar: FlutterPluginRegistrar) { - let channel = FlutterMethodChannel( - name: "plugins.flutter.io/e2e", - binaryMessenger: registrar.messenger) - - let instance = E2EPlugin() - 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/e2e/e2e_macos/macos/e2e_macos.podspec b/packages/e2e/e2e_macos/macos/e2e_macos.podspec deleted file mode 100644 index c92addd8ba07..000000000000 --- a/packages/e2e/e2e_macos/macos/e2e_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 = 'e2e_macos' - s.version = '0.0.1' - s.summary = 'Adapter for e2e 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/e2e/e2e_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/e2e' } - s.source_files = 'Classes/**/*' - s.dependency 'FlutterMacOS' - - s.platform = :osx, '10.11' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } -end - diff --git a/packages/e2e/e2e_macos/pubspec.yaml b/packages/e2e/e2e_macos/pubspec.yaml deleted file mode 100644 index 62dea6390ba4..000000000000 --- a/packages/e2e/e2e_macos/pubspec.yaml +++ /dev/null @@ -1,21 +0,0 @@ -name: e2e_macos -description: Desktop implementation of e2e plugin -version: 0.0.1 -author: Flutter Team -homepage: https://github.com/flutter/plugins/tree/master/packages/e2e/e2e_macos - -flutter: - plugin: - platforms: - macos: - pluginClass: E2EPlugin - -environment: - sdk: ">=2.1.0 <3.0.0" - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - pedantic: ^1.8.0 diff --git a/packages/e2e/example/.gitignore b/packages/e2e/example/.gitignore deleted file mode 100644 index 2ddde2a5e36f..000000000000 --- a/packages/e2e/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/e2e/example/.metadata b/packages/e2e/example/.metadata deleted file mode 100644 index 76f3d14cd0f9..000000000000 --- a/packages/e2e/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/e2e/example/README.md b/packages/e2e/example/README.md deleted file mode 100644 index 64a5e8780bc2..000000000000 --- a/packages/e2e/example/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# e2e_example - -Demonstrates how to use the e2e plugin. - -## Getting Started - -This project is a starting point for a Flutter application. - -A few resources to get you started if this is your first Flutter project: - -- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) - -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. diff --git a/packages/e2e/example/android/app/build.gradle b/packages/e2e/example/android/app/build.gradle deleted file mode 100644 index 527ed2dd38e0..000000000000 --- a/packages/e2e/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.e2e_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/e2e/example/android/app/src/androidTest/java/com/example/e2e_example/EmbedderV1ActivityTest.java b/packages/e2e/example/android/app/src/androidTest/java/com/example/e2e_example/EmbedderV1ActivityTest.java deleted file mode 100644 index eedde293eb6c..000000000000 --- a/packages/e2e/example/android/app/src/androidTest/java/com/example/e2e_example/EmbedderV1ActivityTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.e2e_example; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.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/e2e/example/android/app/src/androidTest/java/com/example/e2e_example/MainActivityTest.java b/packages/e2e/example/android/app/src/androidTest/java/com/example/e2e_example/MainActivityTest.java deleted file mode 100644 index 93b1f923ee05..000000000000 --- a/packages/e2e/example/android/app/src/androidTest/java/com/example/e2e_example/MainActivityTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.e2e_example; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterTestRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterTestRunner.class) -public class MainActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(MainActivity.class, true, false); -} diff --git a/packages/e2e/example/android/app/src/androidTest/java/com/example/e2e_example/MainActivityWithPermissionTest.java b/packages/e2e/example/android/app/src/androidTest/java/com/example/e2e_example/MainActivityWithPermissionTest.java deleted file mode 100644 index 90218e8724a9..000000000000 --- a/packages/e2e/example/android/app/src/androidTest/java/com/example/e2e_example/MainActivityWithPermissionTest.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.example.e2e_example; - -import android.Manifest.permission; -import androidx.test.rule.ActivityTestRule; -import androidx.test.rule.GrantPermissionRule; -import dev.flutter.plugins.e2e.FlutterTestRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -/** - * Demonstrates how a E2E 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 MainActivityWithPermissionTest { - - @Rule - public GrantPermissionRule permissionRule = - GrantPermissionRule.grant(permission.ACCESS_COARSE_LOCATION); - - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(MainActivity.class, true, false); -} diff --git a/packages/e2e/example/android/app/src/debug/AndroidManifest.xml b/packages/e2e/example/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 5d4aea26b1dd..000000000000 --- a/packages/e2e/example/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/packages/e2e/example/android/app/src/main/AndroidManifest.xml b/packages/e2e/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index ae1a11dbcb34..000000000000 --- a/packages/e2e/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - - - - diff --git a/packages/e2e/example/android/app/src/main/java/com/example/e2e_example/EmbedderV1Activity.java b/packages/e2e/example/android/app/src/main/java/com/example/e2e_example/EmbedderV1Activity.java deleted file mode 100644 index 4056569eed8c..000000000000 --- a/packages/e2e/example/android/app/src/main/java/com/example/e2e_example/EmbedderV1Activity.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 com.example.e2e_example; - -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; - -public class EmbedderV1Activity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); - } -} diff --git a/packages/e2e/example/android/app/src/main/java/com/example/e2e_example/MainActivity.java b/packages/e2e/example/android/app/src/main/java/com/example/e2e_example/MainActivity.java deleted file mode 100644 index a868db8d9d1c..000000000000 --- a/packages/e2e/example/android/app/src/main/java/com/example/e2e_example/MainActivity.java +++ /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. - -package com.example.e2e_example; - -import dev.flutter.plugins.e2e.E2EPlugin; -import io.flutter.embedding.android.FlutterActivity; -import io.flutter.embedding.engine.FlutterEngine; - -public class MainActivity extends FlutterActivity { - @Override - public void configureFlutterEngine(FlutterEngine flutterEngine) { - flutterEngine.getPlugins().add(new E2EPlugin()); - } -} diff --git a/packages/e2e/example/android/app/src/profile/AndroidManifest.xml b/packages/e2e/example/android/app/src/profile/AndroidManifest.xml deleted file mode 100644 index 5d4aea26b1dd..000000000000 --- a/packages/e2e/example/android/app/src/profile/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/packages/e2e/example/android/build.gradle b/packages/e2e/example/android/build.gradle deleted file mode 100644 index bb8a303898ca..000000000000 --- a/packages/e2e/example/android/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.2.1' - } -} - -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/e2e/example/android/gradle.properties b/packages/e2e/example/android/gradle.properties deleted file mode 100644 index 755300e3a0b5..000000000000 --- a/packages/e2e/example/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M - -android.useAndroidX=true -android.enableJetifier=true diff --git a/packages/e2e/example/ios/Flutter/AppFrameworkInfo.plist b/packages/e2e/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6b4c0f78a785..000000000000 --- a/packages/e2e/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/e2e/example/ios/Runner.xcodeproj/project.pbxproj b/packages/e2e/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index b96fa2fb2618..000000000000 --- a/packages/e2e/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/e2e/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/e2e/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 72fa1469f5e1..000000000000 --- a/packages/e2e/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/e2e/example/ios/Runner/AppDelegate.h b/packages/e2e/example/ios/Runner/AppDelegate.h deleted file mode 100644 index 36e21bbf9cf4..000000000000 --- a/packages/e2e/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,6 +0,0 @@ -#import -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/e2e/example/ios/Runner/AppDelegate.m b/packages/e2e/example/ios/Runner/AppDelegate.m deleted file mode 100644 index 59a72e90be12..000000000000 --- a/packages/e2e/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/e2e/example/ios/Runner/Info.plist b/packages/e2e/example/ios/Runner/Info.plist deleted file mode 100644 index 62f6fbb5c02c..000000000000 --- a/packages/e2e/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 - e2e_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/e2e/example/ios/Runner/main.m b/packages/e2e/example/ios/Runner/main.m deleted file mode 100644 index dff6597e4513..000000000000 --- a/packages/e2e/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/e2e/example/ios/RunnerTests/RunnerTests.m b/packages/e2e/example/ios/RunnerTests/RunnerTests.m deleted file mode 100644 index 9614c6598b15..000000000000 --- a/packages/e2e/example/ios/RunnerTests/RunnerTests.m +++ /dev/null @@ -1,4 +0,0 @@ -#import -#import - -E2E_IOS_RUNNER(RunnerTests) diff --git a/packages/e2e/example/lib/main.dart b/packages/e2e/example/lib/main.dart deleted file mode 100644 index 1f33324acd01..000000000000 --- a/packages/e2e/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/e2e/example/lib/my_app.dart b/packages/e2e/example/lib/my_app.dart deleted file mode 100644 index bfbdb860c76d..000000000000 --- a/packages/e2e/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/e2e/example/lib/my_web_app.dart b/packages/e2e/example/lib/my_web_app.dart deleted file mode 100644 index c2ced1af97ae..000000000000 --- a/packages/e2e/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/e2e/example/macos/Flutter/Flutter-Debug.xcconfig b/packages/e2e/example/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index 785633d3a86b..000000000000 --- a/packages/e2e/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/e2e/example/macos/Flutter/Flutter-Release.xcconfig b/packages/e2e/example/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index 5fba960c3af2..000000000000 --- a/packages/e2e/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/e2e/example/macos/Flutter/GeneratedPluginRegistrant.swift b/packages/e2e/example/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index 2b7c33cf9b20..000000000000 --- a/packages/e2e/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,12 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import e2e_macos - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - E2EPlugin.register(with: registry.registrar(forPlugin: "E2EPlugin")) -} diff --git a/packages/e2e/example/macos/Runner.xcodeproj/project.pbxproj b/packages/e2e/example/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 73b46daf7715..000000000000 --- a/packages/e2e/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 /* e2e_example_example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = e2e_example_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 /* e2e_example_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 /* e2e_example_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/e2e/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/e2e/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index b0cc6c6e2166..000000000000 --- a/packages/e2e/example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,101 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/e2e/example/macos/Runner.xcworkspace/contents.xcworkspacedata b/packages/e2e/example/macos/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a16ed0f..000000000000 --- a/packages/e2e/example/macos/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/packages/e2e/example/macos/Runner/AppDelegate.swift b/packages/e2e/example/macos/Runner/AppDelegate.swift deleted file mode 100644 index d53ef6437726..000000000000 --- a/packages/e2e/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/e2e/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/e2e/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index a2ec33f19f11..000000000000 --- a/packages/e2e/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/e2e/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/packages/e2e/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 3c4935a7ca84..000000000000 Binary files a/packages/e2e/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null differ diff --git a/packages/e2e/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/packages/e2e/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index ed4cc1642168..000000000000 Binary files a/packages/e2e/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null differ diff --git a/packages/e2e/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/packages/e2e/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index 483be6138973..000000000000 Binary files a/packages/e2e/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null differ diff --git a/packages/e2e/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/packages/e2e/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png deleted file mode 100644 index bcbf36df2f2a..000000000000 Binary files a/packages/e2e/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null differ diff --git a/packages/e2e/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/packages/e2e/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png deleted file mode 100644 index 9c0a65286476..000000000000 Binary files a/packages/e2e/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null differ diff --git a/packages/e2e/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/packages/e2e/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png deleted file mode 100644 index e71a726136a4..000000000000 Binary files a/packages/e2e/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null differ diff --git a/packages/e2e/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/packages/e2e/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 8a31fe2dd3f9..000000000000 Binary files a/packages/e2e/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ diff --git a/packages/e2e/example/macos/Runner/Base.lproj/MainMenu.xib b/packages/e2e/example/macos/Runner/Base.lproj/MainMenu.xib deleted file mode 100644 index 537341abf994..000000000000 --- a/packages/e2e/example/macos/Runner/Base.lproj/MainMenu.xib +++ /dev/null @@ -1,339 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/e2e/example/macos/Runner/Configs/AppInfo.xcconfig b/packages/e2e/example/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index 8a3359f80a49..000000000000 --- a/packages/e2e/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 = e2e_example_example - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.e2eExample - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2019 com.example. All rights reserved. diff --git a/packages/e2e/example/macos/Runner/Configs/Debug.xcconfig b/packages/e2e/example/macos/Runner/Configs/Debug.xcconfig deleted file mode 100644 index 36b0fd9464f4..000000000000 --- a/packages/e2e/example/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/packages/e2e/example/macos/Runner/Configs/Release.xcconfig b/packages/e2e/example/macos/Runner/Configs/Release.xcconfig deleted file mode 100644 index dff4f49561c8..000000000000 --- a/packages/e2e/example/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/packages/e2e/example/macos/Runner/Configs/Warnings.xcconfig b/packages/e2e/example/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100644 index 42bcbf4780b1..000000000000 --- a/packages/e2e/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/e2e/example/macos/Runner/DebugProfile.entitlements b/packages/e2e/example/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index dddb8a30c851..000000000000 --- a/packages/e2e/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/e2e/example/macos/Runner/Info.plist b/packages/e2e/example/macos/Runner/Info.plist deleted file mode 100644 index 4789daa6a443..000000000000 --- a/packages/e2e/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/e2e/example/macos/Runner/MainFlutterWindow.swift b/packages/e2e/example/macos/Runner/MainFlutterWindow.swift deleted file mode 100644 index 2722837ec918..000000000000 --- a/packages/e2e/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/e2e/example/macos/Runner/Release.entitlements b/packages/e2e/example/macos/Runner/Release.entitlements deleted file mode 100644 index 852fa1a4728a..000000000000 --- a/packages/e2e/example/macos/Runner/Release.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.app-sandbox - - - diff --git a/packages/e2e/example/pubspec.yaml b/packages/e2e/example/pubspec.yaml deleted file mode 100644 index 9ef6098477e3..000000000000 --- a/packages/e2e/example/pubspec.yaml +++ /dev/null @@ -1,31 +0,0 @@ -name: e2e_example -description: Demonstrates how to use the e2e 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 - e2e: - path: ../ - e2e_macos: - path: ../e2e_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/e2e/example/test_driver/example_e2e.dart b/packages/e2e/example/test_driver/example_e2e.dart deleted file mode 100644 index d97702d5d7cf..000000000000 --- a/packages/e2e/example/test_driver/example_e2e.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:e2e/e2e.dart'; - -import 'example_e2e_io.dart' if (dart.library.html) 'example_e2e_web.dart' - as tests; - -void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); - tests.main(); -} diff --git a/packages/e2e/example/test_driver/example_e2e_io.dart b/packages/e2e/example/test_driver/example_e2e_io.dart deleted file mode 100644 index 9766f568b654..000000000000 --- a/packages/e2e/example/test_driver/example_e2e_io.dart +++ /dev/null @@ -1,34 +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:e2e/e2e.dart'; - -import 'package:e2e_example/main.dart' as app; - -void main() { - E2EWidgetsFlutterBinding.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 version is retrieved. - expect( - find.byWidgetPredicate( - (Widget widget) => - widget is Text && - widget.data.startsWith('Platform: ${Platform.operatingSystem}'), - ), - findsOneWidget, - ); - }); -} diff --git a/packages/e2e/example/test_driver/example_e2e_test.dart b/packages/e2e/example/test_driver/example_e2e_test.dart deleted file mode 100644 index 983c3863dea5..000000000000 --- a/packages/e2e/example/test_driver/example_e2e_test.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'dart:async'; - -import 'package:e2e/e2e_driver.dart' as e2e; - -Future main() async => e2e.main(); diff --git a/packages/e2e/example/test_driver/example_e2e_web.dart b/packages/e2e/example/test_driver/example_e2e_web.dart deleted file mode 100644 index 24c3f2cbb2a4..000000000000 --- a/packages/e2e/example/test_driver/example_e2e_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:e2e/e2e.dart'; - -import 'package:e2e_example/main.dart' as app; - -void main() { - E2EWidgetsFlutterBinding.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/e2e/example/test_driver/failure.dart b/packages/e2e/example/test_driver/failure.dart deleted file mode 100644 index ddeeb800bb06..000000000000 --- a/packages/e2e/example/test_driver/failure.dart +++ /dev/null @@ -1,39 +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:e2e/e2e.dart'; - -import 'package:e2e_example/main.dart' as app; - -// Tests the failure behavior of the E2EWidgetsFlutterBinding -// -// This test fails intentionally! It should be run using a test runner that -// expects failure. -void main() { - E2EWidgetsFlutterBinding.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/e2e/example/test_driver/failure_test.dart b/packages/e2e/example/test_driver/failure_test.dart deleted file mode 100644 index a828df6242d7..000000000000 --- a/packages/e2e/example/test_driver/failure_test.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'dart:async'; - -import 'package:e2e/common.dart' as common; -import 'package:flutter_driver/flutter_driver.dart'; -import 'package:test/test.dart'; - -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/e2e/example/web/index.html b/packages/e2e/example/web/index.html deleted file mode 100644 index 96629657328f..000000000000 --- a/packages/e2e/example/web/index.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - - - - - - example - - - - - - - - diff --git a/packages/e2e/example/web/manifest.json b/packages/e2e/example/web/manifest.json deleted file mode 100644 index c63800102369..000000000000 --- a/packages/e2e/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/e2e/ios/.gitignore b/packages/e2e/ios/.gitignore deleted file mode 100644 index aa479fd3ce8a..000000000000 --- a/packages/e2e/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/e2e/ios/Classes/E2EIosTest.h b/packages/e2e/ios/Classes/E2EIosTest.h deleted file mode 100644 index 1d7651459419..000000000000 --- a/packages/e2e/ios/Classes/E2EIosTest.h +++ /dev/null @@ -1,22 +0,0 @@ -#import - -@interface E2EIosTest : NSObject - -- (BOOL)testE2E:(NSString **)testResult; - -@end - -#define E2E_IOS_RUNNER(__test_class) \ - @interface __test_class : XCTestCase \ - @end \ - \ - @implementation __test_class \ - \ - -(void)testE2E { \ - NSString *testResult; \ - E2EIosTest *e2eIosTest = [[E2EIosTest alloc] init]; \ - BOOL testPass = [e2eIosTest testE2E:&testResult]; \ - XCTAssertTrue(testPass, @"%@", testResult); \ - } \ - \ - @end diff --git a/packages/e2e/ios/Classes/E2EIosTest.m b/packages/e2e/ios/Classes/E2EIosTest.m deleted file mode 100644 index b788780d87e7..000000000000 --- a/packages/e2e/ios/Classes/E2EIosTest.m +++ /dev/null @@ -1,43 +0,0 @@ -#import "E2EIosTest.h" -#import "E2EPlugin.h" - -@implementation E2EIosTest - -- (BOOL)testE2E:(NSString **)testResult { - E2EPlugin *e2ePlugin = [E2EPlugin instance]; - UIViewController *rootViewController = - [[[[UIApplication sharedApplication] delegate] window] rootViewController]; - if (![rootViewController isKindOfClass:[FlutterViewController class]]) { - NSLog(@"expected FlutterViewController as rootViewController."); - return NO; - } - FlutterViewController *flutterViewController = (FlutterViewController *)rootViewController; - [e2ePlugin setupChannels:flutterViewController.engine.binaryMessenger]; - while (!e2ePlugin.testResults) { - CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.f, NO); - } - NSDictionary *testResults = e2ePlugin.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); - [failedTests addObject:test]; - } - } - NSLog(@"================== Test Results End ===================="); - BOOL testPass = failedTests.count == 0; - if (!testPass && testResult) { - *testResult = - [NSString stringWithFormat:@"Detected failed E2E test(s) %@ among %@", - failedTests.description, testResults.allKeys.description]; - } - return testPass; -} - -@end diff --git a/packages/e2e/ios/Classes/E2EPlugin.h b/packages/e2e/ios/Classes/E2EPlugin.h deleted file mode 100644 index e1a99f1b5a8e..000000000000 --- a/packages/e2e/ios/Classes/E2EPlugin.h +++ /dev/null @@ -1,23 +0,0 @@ -#import - -NS_ASSUME_NONNULL_BEGIN - -/** A Flutter plugin that's responsible for communicating the test results back to iOS XCTest. */ -@interface E2EPlugin : NSObject - -/** - * Test results that are sent from Dart when E2E test completes. Before the completion, it is - * @c nil. - */ -@property(nonatomic, readonly, nullable) NSDictionary *testResults; - -/** Fetches the singleton instance of the plugin. */ -+ (E2EPlugin *)instance; - -- (void)setupChannels:(id)binaryMessenger; - -- (instancetype)init NS_UNAVAILABLE; - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/e2e/ios/Classes/E2EPlugin.m b/packages/e2e/ios/Classes/E2EPlugin.m deleted file mode 100644 index e025c2917201..000000000000 --- a/packages/e2e/ios/Classes/E2EPlugin.m +++ /dev/null @@ -1,53 +0,0 @@ -#import "E2EPlugin.h" - -static NSString *const kE2EPluginChannel = @"plugins.flutter.io/e2e"; -static NSString *const kMethodTestFinished = @"allTestsFinished"; - -@interface E2EPlugin () - -@property(nonatomic, readwrite) NSDictionary *testResults; - -@end - -@implementation E2EPlugin { - NSDictionary *_testResults; -} - -+ (E2EPlugin *)instance { - static dispatch_once_t onceToken; - static E2EPlugin *sInstance; - dispatch_once(&onceToken, ^{ - sInstance = [[E2EPlugin 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 E2EPlugin is allocated when the bundle is reloaded. - // See also: https://github.com/flutter/plugins/pull/2465 -} - -- (void)setupChannels:(id)binaryMessenger { - FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:kE2EPluginChannel - 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/e2e/ios/e2e.podspec b/packages/e2e/ios/e2e.podspec deleted file mode 100644 index 0a6a6915607d..000000000000 --- a/packages/e2e/ios/e2e.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 = 'e2e' - s.version = '0.0.1' - s.summary = 'Adapter for e2e 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/e2e' - 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/e2e' } - 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/e2e/lib/_extension_io.dart b/packages/e2e/lib/_extension_io.dart deleted file mode 100644 index a8e66fca1900..000000000000 --- a/packages/e2e/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/e2e/lib/_extension_web.dart b/packages/e2e/lib/_extension_web.dart deleted file mode 100644 index 8bf5950ef74a..000000000000 --- a/packages/e2e/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/e2e/lib/common.dart b/packages/e2e/lib/common.dart deleted file mode 100644 index 39efcc867704..000000000000 --- a/packages/e2e/lib/common.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 'dart:convert'; - -/// An object sent from e2e back to the Flutter Driver in response to -/// `request_data` command. -class Response { - final List _failureDetails; - - final bool _allTestsPassed; - - /// Constructor to use for positive response. - Response.allTestsPassed() - : this._allTestsPassed = true, - this._failureDetails = null; - - /// Constructor for failure response. - Response.someTestsFailed(this._failureDetails) : this._allTestsPassed = false; - - /// 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(), - }); - - /// Deserializes the result from JSON. - static Response fromJson(String source) { - Map responseJson = json.decode(source); - if (responseJson['result'] == 'true') { - return Response.allTestsPassed(); - } else { - return Response.someTestsFailed( - _failureDetailsFromJson(responseJson['failureDetails'])); - } - } - - /// Method for formating 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.toString()); - }); - - 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. - @override - String toString() { - return json.encode({ - 'methodName': methodName, - 'details': details, - }); - } - - /// 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']); - } -} diff --git a/packages/e2e/lib/e2e.dart b/packages/e2e/lib/e2e.dart deleted file mode 100644 index 799b77e6ec3c..000000000000 --- a/packages/e2e/lib/e2e.dart +++ /dev/null @@ -1,114 +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 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; - -import 'common.dart'; -import '_extension_io.dart' if (dart.library.html) '_extension_web.dart'; - -/// A subclass of [LiveTestWidgetsFlutterBinding] that reports tests results -/// on a channel to adapt them to native instrumentation test format. -class E2EWidgetsFlutterBinding extends LiveTestWidgetsFlutterBinding { - /// Sets up a listener to report that the tests are finished when everything is - /// torn down. - E2EWidgetsFlutterBinding() { - // TODO(jackson): Report test results as they arrive - tearDownAll(() async { - try { - // For web integration tests we are not using the - // `plugins.flutter.io/e2e`. Mark the tests as complete before invoking - // the channel. - if (kIsWeb) { - if (!_allTestsPassed.isCompleted) _allTestsPassed.complete(true); - } - await _channel.invokeMethod( - 'allTestsFinished', {'results': _results}); - } on MissingPluginException { - print('Warning: E2E test plugin was not detected.'); - } - if (!_allTestsPassed.isCompleted) _allTestsPassed.complete(true); - }); - } - - final Completer _allTestsPassed = Completer(); - - /// Stores failure details. - /// - /// Failed test method's names used as key. - final List _failureMethodsDetails = List(); - - /// Similar to [WidgetsFlutterBinding.ensureInitialized]. - /// - /// Returns an instance of the [E2EWidgetsFlutterBinding], creating and - /// initializing it if necessary. - static WidgetsBinding ensureInitialized() { - if (WidgetsBinding.instance == null) { - E2EWidgetsFlutterBinding(); - } - assert(WidgetsBinding.instance is E2EWidgetsFlutterBinding); - return WidgetsBinding.instance; - } - - static const MethodChannel _channel = MethodChannel('plugins.flutter.io/e2e'); - - static Map _results = {}; - - // Emulates the Flutter driver extension, returning 'pass' or 'fail'. - @override - void initServiceExtensions() { - super.initServiceExtensions(); - Future> callback(Map params) async { - final String command = params['command']; - Map response; - switch (command) { - case 'request_data': - final bool allTestsPassed = await _allTestsPassed.future; - response = { - 'message': allTestsPassed - ? Response.allTestsPassed().toJson() - : Response.someTestsFailed(_failureMethodsDetails).toJson(), - }; - break; - case 'get_health': - response = {'status': 'ok'}; - break; - default: - throw UnimplementedError('$command is not implemented'); - } - return { - 'isError': false, - 'response': response, - }; - } - - if (kIsWeb) { - registerWebServiceExtension(callback); - } - - registerServiceExtension(name: 'driver', callback: callback); - } - - @override - Future runTest(Future testBody(), VoidCallback invariantTester, - {String description = '', Duration timeout}) async { - // TODO(jackson): Report the results individually instead of all at once - // See https://github.com/flutter/flutter/issues/38985 - final TestExceptionReporter valueBeforeTest = reportTestException; - reportTestException = - (FlutterErrorDetails details, String testDescription) { - _results[description] = 'failed'; - _failureMethodsDetails.add(Failure(testDescription, details.toString())); - if (!_allTestsPassed.isCompleted) _allTestsPassed.complete(false); - valueBeforeTest(details, testDescription); - }; - await super.runTest(testBody, invariantTester, - description: description, timeout: timeout); - _results[description] ??= 'success'; - } -} diff --git a/packages/e2e/lib/e2e_driver.dart b/packages/e2e/lib/e2e_driver.dart deleted file mode 100644 index 2e43c5a36e55..000000000000 --- a/packages/e2e/lib/e2e_driver.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:e2e/common.dart' as e2e; -import 'package:flutter_driver/flutter_driver.dart'; - -Future main() async { - final FlutterDriver driver = await FlutterDriver.connect(); - final String jsonResult = - await driver.requestData(null, timeout: const Duration(minutes: 1)); - final e2e.Response response = e2e.Response.fromJson(jsonResult); - 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/e2e/pubspec.yaml b/packages/e2e/pubspec.yaml deleted file mode 100644 index 1857e2ddfb91..000000000000 --- a/packages/e2e/pubspec.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: e2e -description: Runs tests that use the flutter_test API as integration tests. -version: 0.4.2 -homepage: https://github.com/flutter/plugins/tree/master/packages/e2e - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" - -dependencies: - flutter: - sdk: flutter - flutter_driver: - sdk: flutter - flutter_test: - sdk: flutter - -dev_dependencies: - pedantic: ^1.8.0 - -flutter: - plugin: - platforms: - android: - package: dev.flutter.plugins.e2e - pluginClass: E2EPlugin - ios: - pluginClass: E2EPlugin 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 bc9d93e3bd6d..88976c88b668 100644 --- a/packages/espresso/CHANGELOG.md +++ b/packages/espresso/CHANGELOG.md @@ -1,3 +1,41 @@ +## 0.1.0+4 + +* Updated Android lint settings. +* Updated package description. + +## 0.1.0+3 + +* Remove references to the Android v1 embedding. + +## 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. + ## 0.0.1+5 * Replace deprecated `getFlutterEngine` call on Android. diff --git a/packages/espresso/LICENSE b/packages/espresso/LICENSE index 0c382ce171cc..c6823b81eb84 100644 --- a/packages/espresso/LICENSE +++ b/packages/espresso/LICENSE @@ -1,27 +1,25 @@ -// 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. +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/espresso/android/build.gradle b/packages/espresso/android/build.gradle index 4af1d3e8b67f..da0cd2ebfee8 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 @@ -30,6 +30,21 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' + baseline file("lint-baseline.xml") + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } } } diff --git a/packages/espresso/android/lint-baseline.xml b/packages/espresso/android/lint-baseline.xml new file mode 100644 index 000000000000..19b349f044bf --- /dev/null +++ b/packages/espresso/android/lint-baseline.xml @@ -0,0 +1,389 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 a2ada5c530f5..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; @@ -6,7 +10,6 @@ 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.Registrar; /** EspressoPlugin */ public class EspressoPlugin implements FlutterPlugin, MethodCallHandler { @@ -26,7 +29,8 @@ public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBindin // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called // depending on the user's project. onAttachedToEngine or registerWith must both be defined // in the same class. - public static void registerWith(Registrar registrar) { + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { final MethodChannel channel = new MethodChannel(registrar.messenger(), "espresso"); channel.setMethodCallHandler(new EspressoPlugin()); } 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/AndroidManifest.xml b/packages/espresso/example/android/app/src/main/AndroidManifest.xml index b82df920d3bc..366373e997dc 100644 --- a/packages/espresso/example/android/app/src/main/AndroidManifest.xml +++ b/packages/espresso/example/android/app/src/main/AndroidManifest.xml @@ -1,12 +1,6 @@ - - - - - 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/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/Assets/.gitkeep b/packages/espresso/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/espresso/ios/Classes/EspressoPlugin.h b/packages/espresso/ios/Classes/EspressoPlugin.h deleted file mode 100644 index 5f9761591f72..000000000000 --- a/packages/espresso/ios/Classes/EspressoPlugin.h +++ /dev/null @@ -1,4 +0,0 @@ -#import - -@interface EspressoPlugin : NSObject -@end diff --git a/packages/espresso/ios/Classes/EspressoPlugin.m b/packages/espresso/ios/Classes/EspressoPlugin.m deleted file mode 100644 index cb4ef8072cae..000000000000 --- a/packages/espresso/ios/Classes/EspressoPlugin.m +++ /dev/null @@ -1,15 +0,0 @@ -#import "EspressoPlugin.h" - -@implementation EspressoPlugin -+ (void)registerWithRegistrar:(NSObject*)registrar { - FlutterMethodChannel* channel = - [FlutterMethodChannel methodChannelWithName:@"espresso" - binaryMessenger:[registrar messenger]]; - EspressoPlugin* instance = [[EspressoPlugin alloc] init]; - [registrar addMethodCallDelegate:instance channel:channel]; -} - -- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { - result(FlutterMethodNotImplemented); -} -@end diff --git a/packages/espresso/ios/espresso.podspec b/packages/espresso/ios/espresso.podspec deleted file mode 100644 index c9b2d106fd92..000000000000 --- a/packages/espresso/ios/espresso.podspec +++ /dev/null @@ -1,25 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. -# Run `pod lib lint espresso.podspec' to validate before publishing. -# -Pod::Spec.new do |s| - s.name = 'espresso' - s.version = '0.0.1' - s.summary = 'Flutter Espresso' - s.description = <<-DESC -Provides bindings for Espresso tests of Flutter apps. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/espresso' } - s.documentation_url = 'https://pub.dev/packages/espresso' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - s.platform = :ios, '8.0' - - # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported. - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } -end diff --git a/packages/espresso/pubspec.yaml b/packages/espresso/pubspec.yaml index e84ef5e5b8d1..c0f3b00d556c 100644 --- a/packages/espresso/pubspec.yaml +++ b/packages/espresso/pubspec.yaml @@ -1,11 +1,20 @@ name: espresso description: Java classes for testing Flutter apps using Espresso. -version: 0.0.1+5 -homepage: https://github.com/flutter/plugins/espresso + Allows driving Flutter widgets from a native Espresso test. +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+4 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 +23,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/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..f34ed78d4e7f --- /dev/null +++ b/packages/file_selector/file_selector/CHANGELOG.md @@ -0,0 +1,28 @@ +## 0.8.2+1 + +* Minor code cleanup for new analysis rules. +* Updated package description. + +## 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..b3ed9d0eeaca --- /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 { + Future _getDirectoryPath(BuildContext context) async { + const 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: (BuildContext context) => TextDisplay(directoryPath), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Open a text file'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: Colors.blue, + onPrimary: Colors.white, + ), + child: const Text('Press to ask user to choose a directory'), + onPressed: () => _getDirectoryPath(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog +class TextDisplay extends StatelessWidget { + /// Default Constructor + const TextDisplay(this.directoryPath); + + /// Directory path + final String directoryPath; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Selected Directory'), + content: Scrollbar( + child: SingleChildScrollView( + child: Text(directoryPath), + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () => Navigator.pop(context), + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector/example/lib/home_page.dart b/packages/file_selector/file_selector/example/lib/home_page.dart new file mode 100644 index 000000000000..c598cbdf2611 --- /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: const Text('File Selector Demo Home Page'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: style, + child: const Text('Open a text file'), + onPressed: () => Navigator.pushNamed(context, '/open/text'), + ), + const SizedBox(height: 10), + ElevatedButton( + style: style, + child: const Text('Open an image'), + onPressed: () => Navigator.pushNamed(context, '/open/image'), + ), + const SizedBox(height: 10), + ElevatedButton( + style: style, + child: const Text('Open multiple images'), + onPressed: () => Navigator.pushNamed(context, '/open/images'), + ), + const SizedBox(height: 10), + ElevatedButton( + style: style, + child: const Text('Save a file'), + onPressed: () => Navigator.pushNamed(context, '/save/text'), + ), + const SizedBox(height: 10), + ElevatedButton( + style: style, + child: const Text('Open a get directory dialog'), + onPressed: () => Navigator.pushNamed(context, '/directory'), + ), + ], + ), + ), + ); + } +} diff --git a/packages/file_selector/file_selector/example/lib/main.dart b/packages/file_selector/file_selector/example/lib/main.dart new file mode 100644 index 000000000000..14ce3f593f33 --- /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:example/get_directory_page.dart'; +import 'package:example/home_page.dart'; +import 'package:example/open_image_page.dart'; +import 'package:example/open_multiple_images_page.dart'; +import 'package:example/open_text_page.dart'; +import 'package:example/save_text_page.dart'; +import 'package:flutter/material.dart'; + +void main() { + runApp(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': (BuildContext context) => OpenImagePage(), + '/open/images': (BuildContext context) => OpenMultipleImagesPage(), + '/open/text': (BuildContext context) => OpenTextPage(), + '/save/text': (BuildContext context) => SaveTextPage(), + '/directory': (BuildContext context) => GetDirectoryPage(), + }, + ); + } +} diff --git a/packages/file_selector/file_selector/example/lib/open_image_page.dart b/packages/file_selector/file_selector/example/lib/open_image_page.dart new file mode 100644 index 000000000000..0abdba6eb72d --- /dev/null +++ b/packages/file_selector/file_selector/example/lib/open_image_page.dart @@ -0,0 +1,87 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:file_selector/file_selector.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// Screen that shows an example of openFiles +class OpenImagePage extends StatelessWidget { + Future _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: (BuildContext context) => ImageDisplay(fileName, filePath), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Open an image'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: Colors.blue, + onPrimary: Colors.white, + ), + child: const 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 { + /// Default Constructor + const ImageDisplay(this.fileName, this.filePath); + + /// Image's name + final String fileName; + + /// Image's path + final String filePath; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(fileName), + // On web the filePath is a blob url + // while on other platforms it is a system path. + content: kIsWeb ? Image.network(filePath) : Image.file(File(filePath)), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector/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..9a1101214aaa --- /dev/null +++ b/packages/file_selector/file_selector/example/lib/open_multiple_images_page.dart @@ -0,0 +1,97 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:file_selector/file_selector.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +/// Screen that shows an example of openFiles +class OpenMultipleImagesPage extends StatelessWidget { + Future _openImageFile(BuildContext context) async { + final XTypeGroup jpgsTypeGroup = XTypeGroup( + label: 'JPEGs', + extensions: ['jpg', 'jpeg'], + ); + final XTypeGroup pngTypeGroup = XTypeGroup( + label: 'PNGs', + extensions: ['png'], + ); + final List files = await openFiles(acceptedTypeGroups: [ + jpgsTypeGroup, + pngTypeGroup, + ]); + if (files.isEmpty) { + // Operation was canceled by the user. + return; + } + await showDialog( + context: context, + builder: (BuildContext context) => MultipleImagesDisplay(files), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Open multiple images'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: Colors.blue, + onPrimary: Colors.white, + ), + child: const Text('Press to open multiple images (png, jpg)'), + onPressed: () => _openImageFile(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog +class MultipleImagesDisplay extends StatelessWidget { + /// Default Constructor + const MultipleImagesDisplay(this.files); + + /// The files containing the images + final List files; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Gallery'), + // On web the filePath is a blob url + // while on other platforms it is a system path. + content: Center( + child: Row( + children: [ + ...files.map( + (XFile file) => Flexible( + child: kIsWeb + ? Image.network(file.path) + : Image.file(File(file.path))), + ) + ], + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector/example/lib/open_text_page.dart b/packages/file_selector/file_selector/example/lib/open_text_page.dart new file mode 100644 index 000000000000..652e8596cf81 --- /dev/null +++ b/packages/file_selector/file_selector/example/lib/open_text_page.dart @@ -0,0 +1,83 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file_selector/file_selector.dart'; +import 'package:flutter/material.dart'; + +/// Screen that shows an example of openFile +class OpenTextPage extends StatelessWidget { + Future _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: (BuildContext context) => TextDisplay(fileName, fileContent), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Open a text file'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: Colors.blue, + onPrimary: Colors.white, + ), + child: const Text('Press to open a text file (json, txt)'), + onPressed: () => _openTextFile(context), + ), + ], + ), + ), + ); + } +} + +/// Widget that displays a text file in a dialog +class TextDisplay extends StatelessWidget { + /// Default Constructor + const TextDisplay(this.fileName, this.fileContent); + + /// File's name + final String fileName; + + /// File to display + final String fileContent; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(fileName), + content: Scrollbar( + child: SingleChildScrollView( + child: Text(fileContent), + ), + ), + actions: [ + TextButton( + child: const Text('Close'), + onPressed: () => Navigator.pop(context), + ), + ], + ); + } +} diff --git a/packages/file_selector/file_selector/example/lib/save_text_page.dart b/packages/file_selector/file_selector/example/lib/save_text_page.dart new file mode 100644 index 000000000000..108ef89b0248 --- /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(); + + Future _saveFile() async { + final 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); + const String fileMimeType = 'text/plain'; + final XFile textFile = + XFile.fromData(fileData, mimeType: fileMimeType, name: fileName); + await textFile.saveTo(path); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Save text into a file'), + ), + body: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 300, + child: TextField( + minLines: 1, + maxLines: 12, + controller: _nameController, + decoration: const InputDecoration( + hintText: '(Optional) Suggest File Name', + ), + ), + ), + Container( + width: 300, + child: TextField( + minLines: 1, + maxLines: 12, + controller: _contentController, + decoration: const InputDecoration( + hintText: 'Enter File Contents', + ), + ), + ), + const SizedBox(height: 10), + ElevatedButton( + style: ElevatedButton.styleFrom( + primary: Colors.blue, + onPrimary: Colors.white, + ), + child: const 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..531f4790afd0 --- /dev/null +++ b/packages/file_selector/file_selector/example/pubspec.yaml @@ -0,0 +1,26 @@ +name: example +description: A new Flutter project. +publish_to: none + +version: 1.0.0+1 + +environment: + sdk: ">=2.12.0 <3.0.0" + +dependencies: + 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: ../ + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true diff --git a/packages/file_selector/file_selector/example/web/favicon.png b/packages/file_selector/file_selector/example/web/favicon.png new file mode 100644 index 000000000000..8aaa46ac1ae2 Binary files /dev/null and b/packages/file_selector/file_selector/example/web/favicon.png differ diff --git a/packages/file_selector/file_selector/example/web/icons/Icon-192.png b/packages/file_selector/file_selector/example/web/icons/Icon-192.png new file mode 100644 index 000000000000..b749bfef0747 Binary files /dev/null and b/packages/file_selector/file_selector/example/web/icons/Icon-192.png differ diff --git a/packages/file_selector/file_selector/example/web/icons/Icon-512.png b/packages/file_selector/file_selector/example/web/icons/Icon-512.png new file mode 100644 index 000000000000..88cfd48dff11 Binary files /dev/null and b/packages/file_selector/file_selector/example/web/icons/Icon-512.png differ diff --git a/packages/file_selector/file_selector/example/web/index.html b/packages/file_selector/file_selector/example/web/index.html new file mode 100644 index 000000000000..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..d69217f208ef --- /dev/null +++ b/packages/file_selector/file_selector/pubspec.yaml @@ -0,0 +1,29 @@ +name: file_selector +description: Flutter plugin for opening and saving files, or selecting + directories, using native file selection UI. +repository: https://github.com/flutter/plugins/tree/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+1 + +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..6ab0bd975036 --- /dev/null +++ b/packages/file_selector/file_selector/test/file_selector_test.dart @@ -0,0 +1,343 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file_selector/file_selector.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:test/fake.dart'; + +void main() { + late FakeFileSelector fakePlatformImplementation; + const String initialDirectory = '/home/flutteruser'; + const String confirmButtonText = 'Use this profile picture'; + const String suggestedName = 'suggested_name'; + final List acceptedTypeGroups = [ + 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 XFile expectedFile = XFile('path'); + + test('works', () async { + fakePlatformImplementation + ..setExpectations( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + acceptedTypeGroups: acceptedTypeGroups) + ..setFileResponse([expectedFile]); + + final XFile? file = await openFile( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + acceptedTypeGroups: acceptedTypeGroups, + ); + + expect(file, expectedFile); + }); + + test('works with no arguments', () async { + fakePlatformImplementation.setFileResponse([expectedFile]); + + final XFile? file = await openFile(); + + expect(file, expectedFile); + }); + + test('sets the initial directory', () async { + fakePlatformImplementation + ..setExpectations(initialDirectory: initialDirectory) + ..setFileResponse([expectedFile]); + + final XFile? file = await openFile(initialDirectory: initialDirectory); + expect(file, expectedFile); + }); + + test('sets the button confirmation label', () async { + fakePlatformImplementation + ..setExpectations(confirmButtonText: confirmButtonText) + ..setFileResponse([expectedFile]); + + final XFile? file = await openFile(confirmButtonText: confirmButtonText); + expect(file, expectedFile); + }); + + test('sets the accepted type groups', () async { + fakePlatformImplementation + ..setExpectations(acceptedTypeGroups: acceptedTypeGroups) + ..setFileResponse([expectedFile]); + + final XFile? file = + await openFile(acceptedTypeGroups: acceptedTypeGroups); + expect(file, expectedFile); + }); + }); + + group('openFiles', () { + final List expectedFiles = [XFile('path')]; + + test('works', () async { + fakePlatformImplementation + ..setExpectations( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + acceptedTypeGroups: acceptedTypeGroups) + ..setFileResponse(expectedFiles); + + final List files = await openFiles( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + acceptedTypeGroups: acceptedTypeGroups, + ); + + expect(files, expectedFiles); + }); + + test('works with no arguments', () async { + fakePlatformImplementation.setFileResponse(expectedFiles); + + final List files = await openFiles(); + + expect(files, expectedFiles); + }); + + test('sets the initial directory', () async { + fakePlatformImplementation + ..setExpectations(initialDirectory: initialDirectory) + ..setFileResponse(expectedFiles); + + final List files = + await openFiles(initialDirectory: initialDirectory); + expect(files, expectedFiles); + }); + + test('sets the button confirmation label', () async { + fakePlatformImplementation + ..setExpectations(confirmButtonText: confirmButtonText) + ..setFileResponse(expectedFiles); + + final List files = + await openFiles(confirmButtonText: confirmButtonText); + expect(files, expectedFiles); + }); + + test('sets the accepted type groups', () async { + fakePlatformImplementation + ..setExpectations(acceptedTypeGroups: acceptedTypeGroups) + ..setFileResponse(expectedFiles); + + final List files = + await openFiles(acceptedTypeGroups: acceptedTypeGroups); + expect(files, expectedFiles); + }); + }); + + group('getSavePath', () { + const String expectedSavePath = '/example/path'; + + test('works', () async { + fakePlatformImplementation + ..setExpectations( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + acceptedTypeGroups: acceptedTypeGroups, + suggestedName: suggestedName) + ..setPathResponse(expectedSavePath); + + final String? savePath = await getSavePath( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + acceptedTypeGroups: acceptedTypeGroups, + suggestedName: suggestedName, + ); + + expect(savePath, expectedSavePath); + }); + + test('works with no arguments', () async { + fakePlatformImplementation.setPathResponse(expectedSavePath); + + final String? savePath = await getSavePath(); + expect(savePath, expectedSavePath); + }); + + test('sets the initial directory', () async { + fakePlatformImplementation + ..setExpectations(initialDirectory: initialDirectory) + ..setPathResponse(expectedSavePath); + + final String? savePath = + await getSavePath(initialDirectory: initialDirectory); + expect(savePath, expectedSavePath); + }); + + test('sets the button confirmation label', () async { + fakePlatformImplementation + ..setExpectations(confirmButtonText: confirmButtonText) + ..setPathResponse(expectedSavePath); + + final String? savePath = + await getSavePath(confirmButtonText: confirmButtonText); + expect(savePath, expectedSavePath); + }); + + test('sets the accepted type groups', () async { + fakePlatformImplementation + ..setExpectations(acceptedTypeGroups: acceptedTypeGroups) + ..setPathResponse(expectedSavePath); + + final String? savePath = + await getSavePath(acceptedTypeGroups: acceptedTypeGroups); + expect(savePath, expectedSavePath); + }); + + test('sets the suggested name', () async { + fakePlatformImplementation + ..setExpectations(suggestedName: suggestedName) + ..setPathResponse(expectedSavePath); + + final String? savePath = await getSavePath(suggestedName: suggestedName); + expect(savePath, expectedSavePath); + }); + }); + + group('getDirectoryPath', () { + const String expectedDirectoryPath = '/example/path'; + + test('works', () async { + fakePlatformImplementation + ..setExpectations( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText) + ..setPathResponse(expectedDirectoryPath); + + final String? directoryPath = await getDirectoryPath( + initialDirectory: initialDirectory, + confirmButtonText: confirmButtonText, + ); + + expect(directoryPath, expectedDirectoryPath); + }); + + test('works with no arguments', () async { + fakePlatformImplementation.setPathResponse(expectedDirectoryPath); + + final String? directoryPath = await getDirectoryPath(); + expect(directoryPath, expectedDirectoryPath); + }); + + test('sets the initial directory', () async { + fakePlatformImplementation + ..setExpectations(initialDirectory: initialDirectory) + ..setPathResponse(expectedDirectoryPath); + + final String? directoryPath = + await getDirectoryPath(initialDirectory: initialDirectory); + expect(directoryPath, expectedDirectoryPath); + }); + + test('sets the button confirmation label', () async { + fakePlatformImplementation + ..setExpectations(confirmButtonText: confirmButtonText) + ..setPathResponse(expectedDirectoryPath); + + final String? 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, suggestedName); + return files?[0]; + } + + @override + Future> openFiles({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) async { + expect(acceptedTypeGroups, this.acceptedTypeGroups); + expect(initialDirectory, this.initialDirectory); + expect(suggestedName, 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 new file mode 100644 index 000000000000..95701504fed5 --- /dev/null +++ b/packages/file_selector/file_selector_platform_interface/CHANGELOG.md @@ -0,0 +1,35 @@ +## NEXT + +* Minor code cleanup for new analysis rules. + +## 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 new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/file_selector/file_selector_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/file_selector/file_selector_platform_interface/README.md b/packages/file_selector/file_selector_platform_interface/README.md new file mode 100644 index 000000000000..d750461f2133 --- /dev/null +++ b/packages/file_selector/file_selector_platform_interface/README.md @@ -0,0 +1,26 @@ +# file_selector_platform_interface + +A common platform interface for the `file_selector` plugin. + +This interface allows platform-specific implementations of the `file_selector` +plugin, as well as the plugin itself, to ensure they are supporting the +same interface. + +# Usage + +To implement a new platform-specific implementation of `file_selector`, extend +[`FileSelectorPlatform`][2] with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`FileSelectorPlatform` by calling +`FileSelectorPlatform.instance = MyPlatformFileSelector()`. + +# 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]: ../file_selector +[2]: lib/file_selector_platform_interface.dart 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 new file mode 100644 index 000000000000..5e9a9fefa0bc --- /dev/null +++ b/packages/file_selector/file_selector_platform_interface/lib/file_selector_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 '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 new file mode 100644 index 000000000000..28ec41db6dde --- /dev/null +++ b/packages/file_selector/file_selector_platform_interface/lib/src/method_channel/method_channel_file_selector.dart @@ -0,0 +1,96 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:cross_file/cross_file.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart'; + +const MethodChannel _channel = + MethodChannel('plugins.flutter.io/file_selector'); + +/// An implementation of [FileSelectorPlatform] that uses method channels. +class MethodChannelFileSelector extends FileSelectorPlatform { + /// The MethodChannel that is being used by this implementation of the plugin. + @visibleForTesting + MethodChannel get channel => _channel; + + /// Load a file from user's computer and return it as an XFile + @override + Future openFile({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) async { + final List? path = await _channel.invokeListMethod( + 'openFile', + { + 'acceptedTypeGroups': acceptedTypeGroups + ?.map((XTypeGroup group) => group.toJSON()) + .toList(), + 'initialDirectory': initialDirectory, + 'confirmButtonText': confirmButtonText, + 'multiple': false, + }, + ); + return path == null ? null : XFile(path.first); + } + + /// Load multiple files from user's computer and return it as an XFile + @override + Future> openFiles({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) async { + final List? pathList = await _channel.invokeListMethod( + 'openFile', + { + 'acceptedTypeGroups': acceptedTypeGroups + ?.map((XTypeGroup group) => group.toJSON()) + .toList(), + 'initialDirectory': initialDirectory, + 'confirmButtonText': confirmButtonText, + 'multiple': true, + }, + ); + return pathList?.map((String path) => XFile(path)).toList() ?? []; + } + + /// Gets the path from a save dialog + @override + Future getSavePath({ + List? acceptedTypeGroups, + String? initialDirectory, + String? suggestedName, + String? confirmButtonText, + }) async { + return _channel.invokeMethod( + 'getSavePath', + { + 'acceptedTypeGroups': acceptedTypeGroups + ?.map((XTypeGroup group) => group.toJSON()) + .toList(), + 'initialDirectory': initialDirectory, + 'suggestedName': suggestedName, + 'confirmButtonText': confirmButtonText, + }, + ); + } + + /// Gets a directory path from a dialog + @override + Future getDirectoryPath({ + String? initialDirectory, + String? confirmButtonText, + }) async { + return _channel.invokeMethod( + 'getDirectoryPath', + { + 'initialDirectory': initialDirectory, + 'confirmButtonText': confirmButtonText, + }, + ); + } +} diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/platform_interface/file_selector_interface.dart b/packages/file_selector/file_selector_platform_interface/lib/src/platform_interface/file_selector_interface.dart new file mode 100644 index 000000000000..54a6557c4428 --- /dev/null +++ b/packages/file_selector/file_selector_platform_interface/lib/src/platform_interface/file_selector_interface.dart @@ -0,0 +1,78 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:cross_file/cross_file.dart'; +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import '../method_channel/method_channel_file_selector.dart'; + +/// The interface that implementations of file_selector must implement. +/// +/// Platform implementations should extend this class rather than implement it as `file_selector` +/// 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 +/// [FileSelectorPlatform] methods. +abstract class FileSelectorPlatform extends PlatformInterface { + /// Constructs a FileSelectorPlatform. + FileSelectorPlatform() : super(token: _token); + + static final Object _token = Object(); + + static FileSelectorPlatform _instance = MethodChannelFileSelector(); + + /// The default instance of [FileSelectorPlatform] to use. + /// + /// Defaults to [MethodChannelFileSelector]. + static FileSelectorPlatform get instance => _instance; + + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [FileSelectorPlatform] when they register themselves. + static set instance(FileSelectorPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + /// Open file dialog for loading files and return a file path + /// 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, + }) { + throw UnimplementedError('openFiles() has not been implemented.'); + } + + /// Open file dialog for saving files and return a file path at which to save + /// 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 + /// 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 new file mode 100644 index 000000000000..9caee27c3e35 --- /dev/null +++ b/packages/file_selector/file_selector_platform_interface/lib/src/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 '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_type_group/x_type_group.dart b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_type_group/x_type_group.dart new file mode 100644 index 000000000000..2146131023e1 --- /dev/null +++ b/packages/file_selector/file_selector_platform_interface/lib/src/types/x_type_group/x_type_group.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. + +/// 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, + List? extensions, + this.mimeTypes, + this.macUTIs, + this.webWildCards, + }) : extensions = _removeLeadingDots(extensions); + + /// The 'name' or reference to this group of types + final String? label; + + /// The extensions for this group + final List? extensions; + + /// The MIME types for this group + final List? mimeTypes; + + /// The UTIs for this group + final List? macUTIs; + + /// The web wild cards for this group (ex: image/*, video/*) + final List? webWildCards; + + /// Converts this object into a JSON formatted object + Map toJSON() { + return { + 'label': label, + 'extensions': extensions, + 'mimeTypes': mimeTypes, + 'macUTIs': macUTIs, + 'webWildCards': webWildCards, + }; + } + + static List? _removeLeadingDots(List? exts) => exts + ?.map((String ext) => ext.startsWith('.') ? ext.substring(1) : ext) + .toList(); +} diff --git a/packages/file_selector/file_selector_platform_interface/lib/src/web_helpers/web_helpers.dart b/packages/file_selector/file_selector_platform_interface/lib/src/web_helpers/web_helpers.dart new file mode 100644 index 000000000000..bc7136f80bd6 --- /dev/null +++ b/packages/file_selector/file_selector_platform_interface/lib/src/web_helpers/web_helpers.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 'dart:html'; + +/// Create anchor element with download attribute +AnchorElement createAnchorElement(String href, String? suggestedName) { + final AnchorElement element = AnchorElement(href: href); + + if (suggestedName == null) { + element.download = 'download'; + } else { + element.download = suggestedName; + } + + return element; +} + +/// Add an element to a container and click it +void addElementToContainerAndClick(Element container, Element element) { + // Add the element and click it + // All previous elements will be removed before adding the new one + container.children.add(element); + element.click(); +} + +/// Initializes a DOM container where we can host elements. +Element ensureInitialized(String id) { + Element? target = querySelector('#$id'); + if (target == null) { + final Element targetElement = Element.tag('flt-x-file')..id = id; + + querySelector('body')!.children.add(targetElement); + target = targetElement; + } + return target; +} diff --git a/packages/file_selector/file_selector_platform_interface/pubspec.yaml b/packages/file_selector/file_selector_platform_interface/pubspec.yaml new file mode 100644 index 000000000000..ed0780537a80 --- /dev/null +++ b/packages/file_selector/file_selector_platform_interface/pubspec.yaml @@ -0,0 +1,25 @@ +name: file_selector_platform_interface +description: A common platform interface for the file_selector plugin. +repository: https://github.com/flutter/plugins/tree/master/packages/file_selector/file_selector_platform_interface +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+file_selector%22 +# NOTE: We strongly prefer non-breaking changes, even at the expense of a +# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes +version: 2.0.2 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +dependencies: + cross_file: ^0.3.0 + flutter: + sdk: flutter + http: ^0.13.0 + meta: ^1.3.0 + plugin_platform_interface: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + pedantic: ^1.10.0 + test: ^1.16.3 diff --git a/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart b/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_test.dart new file mode 100644 index 000000000000..91e78b452961 --- /dev/null +++ b/packages/file_selector/file_selector_platform_interface/test/file_selector_platform_interface_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:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:file_selector_platform_interface/src/method_channel/method_channel_file_selector.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + // Store the initial instance before any tests change it. + final FileSelectorPlatform initialInstance = FileSelectorPlatform.instance; + + group('$FileSelectorPlatform', () { + test('$MethodChannelFileSelector() is the default instance', () { + expect(initialInstance, isInstanceOf()); + }); + + test('Can be extended', () { + FileSelectorPlatform.instance = ExtendsFileSelectorPlatform(); + }); + }); +} + +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 new file mode 100644 index 000000000000..33f9fbf45a8b --- /dev/null +++ b/packages/file_selector/file_selector_platform_interface/test/method_channel_file_selector_test.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 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:file_selector_platform_interface/src/method_channel/method_channel_file_selector.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$MethodChannelFileSelector()', () { + final MethodChannelFileSelector plugin = MethodChannelFileSelector(); + + final List log = []; + + setUp(() { + plugin.channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return null; + }); + + log.clear(); + }); + + group('#openFile', () { + test('passes the accepted type groups correctly', () async { + final XTypeGroup group = XTypeGroup( + label: 'text', + extensions: ['txt'], + mimeTypes: ['text/plain'], + macUTIs: ['public.text'], + ); + + final XTypeGroup groupTwo = XTypeGroup( + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + macUTIs: ['public.image'], + webWildCards: ['image/*']); + + await plugin + .openFile(acceptedTypeGroups: [group, groupTwo]); + + expect( + log, + [ + isMethodCall('openFile', arguments: { + 'acceptedTypeGroups': >[ + group.toJSON(), + groupTwo.toJSON() + ], + 'initialDirectory': null, + 'confirmButtonText': null, + 'multiple': false, + }), + ], + ); + }); + test('passes initialDirectory correctly', () async { + await plugin.openFile(initialDirectory: '/example/directory'); + + expect( + log, + [ + isMethodCall('openFile', arguments: { + 'acceptedTypeGroups': null, + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + 'multiple': false, + }), + ], + ); + }); + test('passes confirmButtonText correctly', () async { + await plugin.openFile(confirmButtonText: 'Open File'); + + expect( + log, + [ + isMethodCall('openFile', arguments: { + 'acceptedTypeGroups': null, + 'initialDirectory': null, + 'confirmButtonText': 'Open File', + 'multiple': false, + }), + ], + ); + }); + }); + group('#openFiles', () { + test('passes the accepted type groups correctly', () async { + final XTypeGroup group = XTypeGroup( + label: 'text', + extensions: ['txt'], + mimeTypes: ['text/plain'], + macUTIs: ['public.text'], + ); + + final XTypeGroup groupTwo = XTypeGroup( + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + macUTIs: ['public.image'], + webWildCards: ['image/*']); + + await plugin + .openFiles(acceptedTypeGroups: [group, groupTwo]); + + expect( + log, + [ + isMethodCall('openFile', arguments: { + 'acceptedTypeGroups': >[ + group.toJSON(), + groupTwo.toJSON() + ], + 'initialDirectory': null, + 'confirmButtonText': null, + 'multiple': true, + }), + ], + ); + }); + test('passes initialDirectory correctly', () async { + await plugin.openFiles(initialDirectory: '/example/directory'); + + expect( + log, + [ + isMethodCall('openFile', arguments: { + 'acceptedTypeGroups': null, + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + 'multiple': true, + }), + ], + ); + }); + test('passes confirmButtonText correctly', () async { + await plugin.openFiles(confirmButtonText: 'Open File'); + + expect( + log, + [ + isMethodCall('openFile', arguments: { + 'acceptedTypeGroups': null, + 'initialDirectory': null, + 'confirmButtonText': 'Open File', + 'multiple': true, + }), + ], + ); + }); + }); + + group('#getSavePath', () { + test('passes the accepted type groups correctly', () async { + final XTypeGroup group = XTypeGroup( + label: 'text', + extensions: ['txt'], + mimeTypes: ['text/plain'], + macUTIs: ['public.text'], + ); + + final XTypeGroup groupTwo = XTypeGroup( + label: 'image', + extensions: ['jpg'], + mimeTypes: ['image/jpg'], + macUTIs: ['public.image'], + webWildCards: ['image/*']); + + await plugin + .getSavePath(acceptedTypeGroups: [group, groupTwo]); + + expect( + log, + [ + isMethodCall('getSavePath', arguments: { + 'acceptedTypeGroups': >[ + group.toJSON(), + groupTwo.toJSON() + ], + 'initialDirectory': null, + 'suggestedName': null, + 'confirmButtonText': null, + }), + ], + ); + }); + test('passes initialDirectory correctly', () async { + await plugin.getSavePath(initialDirectory: '/example/directory'); + + expect( + log, + [ + isMethodCall('getSavePath', arguments: { + 'acceptedTypeGroups': null, + 'initialDirectory': '/example/directory', + 'suggestedName': null, + 'confirmButtonText': null, + }), + ], + ); + }); + test('passes confirmButtonText correctly', () async { + await plugin.getSavePath(confirmButtonText: 'Open File'); + + expect( + log, + [ + isMethodCall('getSavePath', arguments: { + 'acceptedTypeGroups': null, + 'initialDirectory': null, + 'suggestedName': null, + 'confirmButtonText': 'Open File', + }), + ], + ); + }); + group('#getDirectoryPath', () { + test('passes initialDirectory correctly', () async { + await plugin.getDirectoryPath(initialDirectory: '/example/directory'); + + expect( + log, + [ + isMethodCall('getDirectoryPath', arguments: { + 'initialDirectory': '/example/directory', + 'confirmButtonText': null, + }), + ], + ); + }); + test('passes confirmButtonText correctly', () async { + await plugin.getDirectoryPath(confirmButtonText: 'Open File'); + + expect( + log, + [ + isMethodCall('getDirectoryPath', arguments: { + 'initialDirectory': null, + 'confirmButtonText': 'Open File', + }), + ], + ); + }); + }); + }); + }); +} diff --git a/packages/file_selector/file_selector_platform_interface/test/x_type_group_test.dart b/packages/file_selector/file_selector_platform_interface/test/x_type_group_test.dart new file mode 100644 index 000000000000..84f5ca1f0bd2 --- /dev/null +++ b/packages/file_selector/file_selector_platform_interface/test/x_type_group_test.dart @@ -0,0 +1,52 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('XTypeGroup', () { + test('toJSON() creates correct map', () { + const String label = 'test group'; + final List extensions = ['txt', 'jpg']; + final List mimeTypes = ['text/plain']; + final List macUTIs = ['public.plain-text']; + final List webWildCards = ['image/*']; + + final XTypeGroup group = XTypeGroup( + label: label, + extensions: extensions, + mimeTypes: mimeTypes, + macUTIs: macUTIs, + webWildCards: webWildCards, + ); + + final Map jsonMap = group.toJSON(); + expect(jsonMap['label'], label); + expect(jsonMap['extensions'], extensions); + expect(jsonMap['mimeTypes'], mimeTypes); + expect(jsonMap['macUTIs'], macUTIs); + expect(jsonMap['webWildCards'], webWildCards); + }); + + test('A wildcard group can be created', () { + final XTypeGroup group = XTypeGroup( + label: 'Any', + ); + + final Map 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 List extensions = ['.txt', '.jpg']; + final XTypeGroup 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..dabd7173868c --- /dev/null +++ b/packages/file_selector/file_selector_web/CHANGELOG.md @@ -0,0 +1,28 @@ +## NEXT + +* Minor code cleanup for new analysis rules. + +## 0.8.1+2 + +* Add `implements` to pubspec. + +# 0.8.1+1 + +- Updated installation instructions in README. + +# 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..026e5859e6f3 --- /dev/null +++ b/packages/file_selector/file_selector_web/README.md @@ -0,0 +1,11 @@ +# file\_selector\_web + +The web implementation of [`file_selector`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `file_selector` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/file_selector +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/file_selector/file_selector_web/example/README.md b/packages/file_selector/file_selector_web/example/README.md new file mode 100644 index 000000000000..8a6e74b107ea --- /dev/null +++ b/packages/file_selector/file_selector_web/example/README.md @@ -0,0 +1,9 @@ +# Testing + +This package uses `package:integration_test` to run its tests in a web browser. + +See [Plugin Tests > Web Tests](https://github.com/flutter/flutter/wiki/Plugin-Tests#web-tests) +in the Flutter wiki for instructions to setup and run the tests in this package. + +Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) +for more info. \ No newline at end of file diff --git a/packages/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..ee1af8cb62fd --- /dev/null +++ b/packages/file_selector/file_selector_web/example/integration_test/dom_helper_test.dart @@ -0,0 +1,115 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:html'; + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:file_selector_web/src/dom_helper.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + group('dom_helper', () { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + late DomHelper domHelper; + late FileUploadInputElement input; + + FileList? createFileList(List files) { + final DataTransfer 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 File mockFile1 = File(['123456'], 'file1.txt'); + final File mockFile2 = File([], 'file2.txt'); + + testWidgets('works', (_) async { + final Future> futureFiles = domHelper.getFiles( + input: input, + ); + + setFilesAndTriggerChange([mockFile1, mockFile2]); + + 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 { + const String accept = '.jpg,.png'; + const bool multiple = true; + bool wasClicked = false; + + //ignore: unawaited_futures + input.onClick.first.then((_) => wasClicked = true); + + final Future> 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..fe57d1d1e15d --- /dev/null +++ b/packages/file_selector/file_selector_web/example/integration_test/file_selector_web_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:html'; +import 'dart:typed_data'; + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:file_selector_web/file_selector_web.dart'; +import 'package:file_selector_web/src/dom_helper.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + group('FileSelectorWeb', () { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('openFile', () { + testWidgets('works', (WidgetTester _) async { + final XFile mockFile = createXFile('1001', 'identity.png'); + + final MockDomHelper mockDomHelper = MockDomHelper() + ..setFiles([mockFile]) + ..expectAccept('.jpg,.jpeg,image/png,image/*') + ..expectMultiple(false); + + final FileSelectorWeb plugin = + FileSelectorWeb(domHelper: mockDomHelper); + + final XTypeGroup typeGroup = XTypeGroup( + label: 'images', + extensions: ['jpg', 'jpeg'], + mimeTypes: ['image/png'], + webWildCards: ['image/*'], + ); + + final XFile 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 XFile mockFile1 = createXFile('123456', 'file1.txt'); + final XFile mockFile2 = createXFile('', 'file2.txt'); + + final MockDomHelper mockDomHelper = MockDomHelper() + ..setFiles([mockFile1, mockFile2]) + ..expectAccept('.txt') + ..expectMultiple(true); + + final FileSelectorWeb plugin = + FileSelectorWeb(domHelper: mockDomHelper); + + final XTypeGroup typeGroup = XTypeGroup( + label: 'files', + extensions: ['.txt'], + ); + + final List 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 FileSelectorWeb plugin = FileSelectorWeb(); + final Future 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 Uint8List data = Uint8List.fromList(content.codeUnits); + return XFile.fromData(data, name: name, lastModified: DateTime.now()); +} diff --git a/packages/file_selector/file_selector_web/example/lib/main.dart b/packages/file_selector/file_selector_web/example/lib/main.dart new file mode 100644 index 000000000000..341913a18490 --- /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 const Directionality( + textDirection: TextDirection.ltr, + child: Text('Testing... Look at the console output for results!'), + ); + } +} diff --git a/packages/file_selector/file_selector_web/example/pubspec.yaml b/packages/file_selector/file_selector_web/example/pubspec.yaml 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..8f4ca202593e --- /dev/null +++ b/packages/file_selector/file_selector_web/lib/file_selector_web.dart @@ -0,0 +1,79 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:file_selector_web/src/dom_helper.dart'; +import 'package:file_selector_web/src/utils.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:meta/meta.dart'; + +/// The web implementation of [FileSelectorPlatform]. +/// +/// This class implements the `package:file_selector` functionality for the web. +class FileSelectorWeb extends FileSelectorPlatform { + /// Default constructor, initializes _domHelper that we can use + /// to interact with the DOM. + /// overrides parameter allows for testing to override functions + FileSelectorWeb({@visibleForTesting DomHelper? domHelper}) + : _domHelper = domHelper ?? DomHelper(); + + final DomHelper _domHelper; + + /// Registers this class as the default instance of [FileSelectorPlatform]. + static void registerWith(Registrar registrar) { + FileSelectorPlatform.instance = FileSelectorWeb(); + } + + @override + Future openFile({ + List? acceptedTypeGroups, + String? initialDirectory, + String? confirmButtonText, + }) async { + final List 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 String accept = acceptedTypesToString(acceptedTypeGroups); + return _domHelper.getFiles( + accept: accept, + multiple: multiple, + ); + } +} diff --git a/packages/file_selector/file_selector_web/lib/src/dom_helper.dart b/packages/file_selector/file_selector_web/lib/src/dom_helper.dart new file mode 100644 index 000000000000..06c13d968484 --- /dev/null +++ b/packages/file_selector/file_selector_web/lib/src/dom_helper.dart @@ -0,0 +1,67 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:html'; + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart'; + +/// Class to manipulate the DOM with the intention of reading files from it. +class DomHelper { + /// Default constructor, initializes the container DOM element. + DomHelper() { + final Element body = querySelector('body')!; + body.children.add(_container); + } + + final Element _container = Element.tag('file-selector'); + + /// Sets the attributes and waits for a file to be selected. + Future> getFiles({ + String accept = '', + bool multiple = false, + @visibleForTesting FileUploadInputElement? input, + }) { + final Completer> completer = Completer>(); + final 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 event) { + final ErrorEvent error = event as ErrorEvent; + final PlatformException 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..6a534645fda6 --- /dev/null +++ b/packages/file_selector/file_selector_web/lib/src/utils.dart @@ -0,0 +1,40 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +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 XTypeGroup 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..bbad45bf2d6b --- /dev/null +++ b/packages/file_selector/file_selector_web/pubspec.yaml @@ -0,0 +1,30 @@ +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+2 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +flutter: + plugin: + implements: file_selector + 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..9bddfd2e6304 --- /dev/null +++ b/packages/file_selector/file_selector_web/test/utils_test.dart @@ -0,0 +1,58 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file_selector_platform_interface/file_selector_platform_interface.dart'; +import 'package:file_selector_web/src/utils.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('FileSelectorWeb utils', () { + group('acceptedTypesToString', () { + test('works', () { + final List acceptedTypes = [ + XTypeGroup(label: 'images', webWildCards: ['images/*']), + XTypeGroup(label: 'jpgs', extensions: ['jpg', 'jpeg']), + XTypeGroup(label: 'pngs', mimeTypes: ['image/png']), + ]; + final String accepts = acceptedTypesToString(acceptedTypes); + expect(accepts, 'images/*,.jpg,.jpeg,image/png'); + }); + + test('works with an empty list', () { + final List acceptedTypes = []; + final String accepts = acceptedTypesToString(acceptedTypes); + expect(accepts, ''); + }); + + test('works with extensions', () { + final List acceptedTypes = [ + XTypeGroup(label: 'jpgs', extensions: ['jpeg', 'jpg']), + XTypeGroup(label: 'pngs', extensions: ['png']), + ]; + final String accepts = acceptedTypesToString(acceptedTypes); + expect(accepts, '.jpeg,.jpg,.png'); + }); + + test('works with mime types', () { + final List acceptedTypes = [ + XTypeGroup( + label: 'jpgs', mimeTypes: ['image/jpeg', 'image/jpg']), + XTypeGroup(label: 'pngs', mimeTypes: ['image/png']), + ]; + final String accepts = acceptedTypesToString(acceptedTypes); + expect(accepts, 'image/jpeg,image/jpg,image/png'); + }); + + test('works with web wild cards', () { + final List acceptedTypes = [ + XTypeGroup(label: 'images', webWildCards: ['image/*']), + XTypeGroup(label: 'audios', webWildCards: ['audio/*']), + XTypeGroup(label: 'videos', webWildCards: ['video/*']), + ]; + final String 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 e51a1dd10f36..7e567d8cce5c 100644 --- a/packages/flutter_plugin_android_lifecycle/CHANGELOG.md +++ b/packages/flutter_plugin_android_lifecycle/CHANGELOG.md @@ -1,3 +1,44 @@ +## NEXT + +* Updated Android lint settings. + +## 2.0.3 + +* Remove references to the Android V1 embedding. + +## 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. + +## 1.0.10 + +* Update android compileSdkVersion to 29. + +## 1.0.9 + +* Let the no-op plugin implement the `FlutterPlugin` interface. + +## 1.0.8 + +* Post-v2 Android embedding cleanup. + ## 1.0.7 * Update Gradle version. Fixes https://github.com/flutter/flutter/issues/48724. diff --git a/packages/flutter_plugin_android_lifecycle/LICENSE b/packages/flutter_plugin_android_lifecycle/LICENSE index 0c382ce171cc..c6823b81eb84 100644 --- a/packages/flutter_plugin_android_lifecycle/LICENSE +++ b/packages/flutter_plugin_android_lifecycle/LICENSE @@ -1,27 +1,25 @@ -// 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. +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/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/android/build.gradle b/packages/flutter_plugin_android_lifecycle/android/build.gradle index 77c8415cd69d..5a584b4e366f 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,26 +15,41 @@ buildscript { rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } apply plugin: 'com.android.library' android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { minSdkVersion 16 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'proguard.txt' } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } dependencies { implementation "androidx.annotation:annotation:1.1.0" } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } } dependencies { diff --git a/packages/flutter_plugin_android_lifecycle/android/gradle.properties b/packages/flutter_plugin_android_lifecycle/android/gradle.properties deleted file mode 100644 index 38c8d4544ff1..000000000000 --- a/packages/flutter_plugin_android_lifecycle/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true -android.useAndroidX=true -android.enableJetifier=true diff --git a/packages/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 7abf1d67667c..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,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. // package io.flutter.plugins.flutter_plugin_android_lifecycle; -import io.flutter.plugin.common.PluginRegistry.Registrar; +import androidx.annotation.NonNull; +import io.flutter.embedding.engine.plugins.FlutterPlugin; /** * Plugin class that exists because the Flutter tool expects such a class to exist for every Android @@ -12,8 +13,19 @@ * *

      DO NOT USE THIS CLASS. */ -public class FlutterAndroidLifecyclePlugin { - public static void registerWith(Registrar registrar) { +public class FlutterAndroidLifecyclePlugin implements FlutterPlugin { + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { + // no-op + } + + @Override + public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) { + // no-op + } + + @Override + public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { // no-op } } 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/build.gradle b/packages/flutter_plugin_android_lifecycle/example/android/app/build.gradle index b2f6139734bd..da10d611c704 100644 --- a/packages/flutter_plugin_android_lifecycle/example/android/app/build.gradle +++ b/packages/flutter_plugin_android_lifecycle/example/android/app/build.gradle @@ -25,7 +25,7 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 28 + compileSdkVersion 29 lintOptions { disable 'InvalidPackage' diff --git a/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/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 deleted file mode 100644 index a5ad2176b4d0..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/android/app/src/androidTest/java/io/flutter/plugins/flutter_plugin_android_lifecycle/EmbeddingV1ActivityTest.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.flutter_plugin_android_lifecycle_example; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} 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 05057c1a697e..25999995691d 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,15 +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. package io.flutter.plugins.flutter_plugin_android_lifecycle_example; import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; -@RunWith(FlutterRunner.class) +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) public class MainActivityTest { @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); } diff --git a/packages/flutter_plugin_android_lifecycle/example/android/app/src/main/AndroidManifest.xml b/packages/flutter_plugin_android_lifecycle/example/android/app/src/main/AndroidManifest.xml index eddf84b10d4c..d00868f25cbf 100644 --- a/packages/flutter_plugin_android_lifecycle/example/android/app/src/main/AndroidManifest.xml +++ b/packages/flutter_plugin_android_lifecycle/example/android/app/src/main/AndroidManifest.xml @@ -1,12 +1,6 @@ - - - + 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 deleted file mode 100644 index a18c01b779ff..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/android/app/src/main/java/io/flutter/plugins/flutter_plugin_android_lifecycle_example/EmbeddingV1Activity.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.flutter.plugins.flutter_plugin_android_lifecycle_example; - -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; - -public class EmbeddingV1Activity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); - } -} 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 2ea33493eb3c..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. @@ -6,7 +6,7 @@ import android.util.Log; import androidx.lifecycle.Lifecycle; -import dev.flutter.plugins.e2e.E2EPlugin; +import dev.flutter.plugins.integration_test.IntegrationTestPlugin; import io.flutter.embedding.android.FlutterActivity; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.plugins.FlutterPlugin; @@ -20,7 +20,7 @@ public class MainActivity extends FlutterActivity { @Override public void configureFlutterEngine(FlutterEngine flutterEngine) { flutterEngine.getPlugins().add(new TestPlugin()); - flutterEngine.getPlugins().add(new E2EPlugin()); + flutterEngine.getPlugins().add(new IntegrationTestPlugin()); } private static class TestPlugin implements FlutterPlugin, ActivityAware { 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 new file mode 100644 index 000000000000..1d329a02f93b --- /dev/null +++ b/packages/flutter_plugin_android_lifecycle/example/integration_test/flutter_plugin_android_lifecycle_test.dart @@ -0,0 +1,15 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_plugin_android_lifecycle_example/main.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('loads', (WidgetTester tester) async { + await tester.pumpWidget(MyApp()); + }); +} 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-1024x1024@1x.png b/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index dc9ada4725e9..000000000000 Binary files a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/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..3ef6794dfad2 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'; @@ -12,7 +16,7 @@ class MyApp extends StatelessWidget { appBar: AppBar( title: const Text('Sample flutter_plugin_android_lifecycle usage'), ), - body: Center( + body: const Center( child: Text( 'This plugin only provides Android Lifecycle API\n for other Android plugins.')), ), diff --git a/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml b/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml index 379e0b804f4e..0c88cd2c5531 100644 --- a/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml +++ b/packages/flutter_plugin_android_lifecycle/example/pubspec.yaml @@ -1,22 +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 - e2e: "^0.2.1" + 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: flutter_test: sdk: flutter + integration_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/example/test_driver/flutter_plugin_android_lifecycle_e2e.dart b/packages/flutter_plugin_android_lifecycle/example/test_driver/flutter_plugin_android_lifecycle_e2e.dart deleted file mode 100644 index 5abead07d132..000000000000 --- a/packages/flutter_plugin_android_lifecycle/example/test_driver/flutter_plugin_android_lifecycle_e2e.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:e2e/e2e.dart'; -import 'package:flutter_plugin_android_lifecycle_example/main.dart'; - -void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('loads', (WidgetTester tester) async { - await tester.pumpWidget(MyApp()); - }); -} 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 a9191be477fa..0fc128d03e17 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.7 -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.3 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 df4d9162be7e..0c95abda319b 100644 --- a/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md +++ b/packages/google_maps_flutter/google_maps_flutter/CHANGELOG.md @@ -1,3 +1,166 @@ +## 2.0.11 + +* Add additional marker drag events. + +## 2.0.10 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. + +## 2.0.9 + +* Fix Android `NullPointerException` caused by the `GoogleMapController` being disposed before `GoogleMap` was ready. + +## 2.0.8 + +* Mark iOS arm64 simulators as unsupported. + +## 2.0.7 + +* Add iOS unit and UI integration test targets. +* Exclude arm64 simulators in example app. +* Remove references to the Android V1 embedding. + +## 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. + +## 1.0.1 + +* Fix headline in the readme. + +## 1.0.0 - Out of developer preview 🎉. + +* Bump the minimal Flutter SDK to 1.22 where platform views are out of developer preview and performing better on iOS. Flutter 1.22 no longer requires adding the `io.flutter.embedded_views_preview` to `Info.plist` in iOS. + +## 0.5.33 + +* Keep handling deprecated Android v1 classes for backward compatibility. + +## 0.5.32 + +* Fix typo in google_maps_flutter/example/map_ui.dart. + +## 0.5.31 + +* Geodesic Polyline support for iOS + +## 0.5.30 + +* Add a `dispose` method to the controller to let the native side know that we're done with said controller. +* Call `controller.dispose()` from the `dispose` method of the `GoogleMap` widget. + +## 0.5.29+1 + +* (ios) Pin dependency on GoogleMaps pod to `< 3.10`, to address https://github.com/flutter/flutter/issues/63447 + +## 0.5.29 + +* Pass a constant `_web_only_mapCreationId` to `platform.buildView`, so web can return a cached widget DOM when flutter attempts to repaint there. +* Modify some examples slightly so they're more web-friendly. + +## 0.5.28+2 + +* Move test introduced in #2449 to its right location. + +## 0.5.28+1 + +* Android: Make sure map view only calls onDestroy once. +* Android: Fix a memory leak regression caused in `0.5.26+4`. + +## 0.5.28 + +* Android: Add liteModeEnabled option. + ## 0.5.27+3 * iOS: Update the gesture recognizer blocking policy to "WaitUntilTouchesEnded", which fixes the camera idle callback not triggered issue. @@ -385,4 +548,4 @@ ## 0.0.2 -* Initial developers preview release. \ No newline at end of file +* Initial developers preview release. diff --git a/packages/google_maps_flutter/google_maps_flutter/LICENSE b/packages/google_maps_flutter/google_maps_flutter/LICENSE index 8940a4be1b58..c6823b81eb84 100644 --- a/packages/google_maps_flutter/google_maps_flutter/LICENSE +++ b/packages/google_maps_flutter/google_maps_flutter/LICENSE @@ -1,27 +1,25 @@ -// 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. +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/google_maps_flutter/google_maps_flutter/README.md b/packages/google_maps_flutter/google_maps_flutter/README.md index 4f206721995a..99c04f3ae1df 100644 --- a/packages/google_maps_flutter/google_maps_flutter/README.md +++ b/packages/google_maps_flutter/google_maps_flutter/README.md @@ -1,26 +1,12 @@ -# Google Maps for Flutter (Developers Preview) +# 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. -## Developers Preview Status -The plugin relies on Flutter's new mechanism for embedding Android and iOS views. -As that mechanism is currently in a developers preview, this plugin should also be -considered a developers preview. - -Known issues are tagged with the [platform-views](https://github.com/flutter/flutter/labels/a%3A%20platform-views) and/or [maps](https://github.com/flutter/flutter/labels/p%3A%20maps) labels. - -To use this plugin on iOS you need to opt-in for the embedded views preview by -adding a boolean property to the app's `Info.plist` file, with the key `io.flutter.embedded_views_preview` -and the value `YES`. - -The API exposed by this plugin is not yet stable, and we expect some breaking changes to land soon. - - ## 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 @@ -35,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; } @@ -316,6 +348,10 @@ static void interpretGoogleMapOptions(Object o, GoogleMapOptionsSink sink) { if (zoomGesturesEnabled != null) { sink.setZoomGesturesEnabled(toBoolean(zoomGesturesEnabled)); } + final Object liteModeEnabled = data.get("liteModeEnabled"); + if (liteModeEnabled != null) { + sink.setLiteModeEnabled(toBoolean(liteModeEnabled)); + } final Object myLocationEnabled = data.get("myLocationEnabled"); if (myLocationEnabled != null) { sink.setMyLocationEnabled(toBoolean(myLocationEnabled)); @@ -373,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) { @@ -448,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"); @@ -556,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); @@ -611,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 af42aa901379..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; } @@ -128,6 +115,11 @@ public void setZoomGesturesEnabled(boolean zoomGesturesEnabled) { options.zoomGesturesEnabled(zoomGesturesEnabled); } + @Override + public void setLiteModeEnabled(boolean liteModeEnabled) { + options.liteMode(liteModeEnabled); + } + @Override public void setIndoorEnabled(boolean indoorEnabled) { this.indoorEnabled = indoorEnabled; @@ -177,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 6f59aa63688e..9b8810354b8f 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,10 +56,10 @@ final class GoogleMapController private static final String TAG = "GoogleMapController"; private final int id; - private final AtomicInteger activityState; private final MethodChannel methodChannel; - private final MapView mapView; - private GoogleMap googleMap; + private final GoogleMapOptions options; + @Nullable private MapView mapView; + @Nullable private GoogleMap googleMap; private boolean trackCameraPosition = false; private boolean myLocationEnabled = false; private boolean myLocationButtonEnabled = false; @@ -82,47 +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 @@ -131,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); } @@ -201,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 @@ -300,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; } @@ -329,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; } @@ -383,6 +327,11 @@ public void onSnapshotReady(Bitmap bitmap) { result.success(googleMap.getUiSettings().isZoomGesturesEnabled()); break; } + case "map#isLiteModeEnabled": + { + result.success(options.getLiteMode()); + break; + } case "map#isZoomControlsEnabled": { result.success(googleMap.getUiSettings().isZoomControlsEnabled()); @@ -441,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(); } @@ -494,10 +467,14 @@ public boolean onMarkerClick(Marker marker) { } @Override - public void onMarkerDragStart(Marker marker) {} + public void onMarkerDragStart(Marker marker) { + markersController.onMarkerDragStart(marker.getId(), marker.getPosition()); + } @Override - public void onMarkerDrag(Marker marker) {} + public void onMarkerDrag(Marker marker) { + markersController.onMarkerDrag(marker.getId(), marker.getPosition()); + } @Override public void onMarkerDragEnd(Marker marker) { @@ -527,10 +504,18 @@ public void dispose() { disposed = true; methodChannel.setMethodCallHandler(null); setGoogleMapListener(null); - getApplication().unregisterActivityLifecycleCallbacks(this); + destroyMapViewIfNecessary(); + Lifecycle lifecycle = lifecycleProvider.getLifecycle(); + if (lifecycle != null) { + lifecycle.removeObserver(this); + } } private void setGoogleMapListener(@Nullable GoogleMapListener listener) { + if (googleMap == null) { + Log.v(TAG, "Controller was disposed before GoogleMap was ready."); + return; + } googleMap.setOnCameraMoveStartedListener(listener); googleMap.setOnCameraMoveListener(listener); googleMap.setOnCameraIdleListener(listener); @@ -548,73 +533,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; - } - mapView.onDestroy(); - } - - // DefaultLifecycleObserver and OnSaveInstanceStateListener + // DefaultLifecycleObserver @Override public void onCreate(@NonNull LifecycleOwner owner) { @@ -658,10 +586,11 @@ public void onStop(@NonNull LifecycleOwner owner) { @Override public void onDestroy(@NonNull LifecycleOwner owner) { + owner.getLifecycle().removeObserver(this); if (disposed) { return; } - mapView.onDestroy(); + destroyMapViewIfNecessary(); } @Override @@ -749,6 +678,12 @@ public void setZoomGesturesEnabled(boolean zoomGesturesEnabled) { googleMap.getUiSettings().setZoomGesturesEnabled(zoomGesturesEnabled); } + /** This call will have no effect on already created map */ + @Override + public void setLiteModeEnabled(boolean liteModeEnabled) { + options.liteMode(liteModeEnabled); + } + @Override public void setMyLocationEnabled(boolean myLocationEnabled) { if (this.myLocationEnabled == myLocationEnabled) { @@ -784,7 +719,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(); } @@ -796,7 +732,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(); } @@ -808,7 +745,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(); } @@ -820,7 +758,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(); } @@ -830,6 +769,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()) { @@ -862,20 +813,12 @@ 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; } + mapView.onDestroy(); + mapView = null; } public void setIndoorEnabled(boolean indoorEnabled) { @@ -894,16 +837,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 efae01510537..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 { @@ -30,6 +32,8 @@ interface GoogleMapOptionsSink { void setZoomGesturesEnabled(boolean zoomGesturesEnabled); + void setLiteModeEnabled(boolean liteModeEnabled); + void setMyLocationEnabled(boolean myLocationEnabled); void setZoomControlsEnabled(boolean zoomControlsEnabled); @@ -49,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 9f9f378737df..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,22 +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 io.flutter.plugin.common.PluginRegistry.Registrar; -import java.util.concurrent.atomic.AtomicInteger; /** * Plugin for controlling a set of GoogleMap views to be shown as overlays on top of the Flutter @@ -24,37 +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"; - public static void registerWith(Registrar registrar) { - if (registrar.activity() == null) { + @SuppressWarnings("deprecation") + 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..47ffe9b857d6 100644 --- a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/MarkersController.java @@ -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. @@ -105,6 +105,28 @@ boolean onMarkerTap(String googleMarkerId) { return false; } + void onMarkerDragStart(String googleMarkerId, LatLng latLng) { + String markerId = googleMapsMarkerIdToDartMarkerId.get(googleMarkerId); + if (markerId == null) { + return; + } + final Map data = new HashMap<>(); + data.put("markerId", markerId); + data.put("position", Convert.latLngToJson(latLng)); + methodChannel.invokeMethod("marker#onDragStart", data); + } + + void onMarkerDrag(String googleMarkerId, LatLng latLng) { + String markerId = googleMapsMarkerIdToDartMarkerId.get(googleMarkerId); + if (markerId == null) { + return; + } + final Map data = new HashMap<>(); + data.put("markerId", markerId); + data.put("position", Convert.latLngToJson(latLng)); + methodChannel.invokeMethod("marker#onDrag", data); + } + void onMarkerDragEnd(String googleMarkerId, LatLng latLng) { String markerId = googleMapsMarkerIdToDartMarkerId.get(googleMarkerId); if (markerId == null) { diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/main/java/io/flutter/plugins/googlemaps/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/GoogleMapControllerTest.java b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java new file mode 100644 index 000000000000..6bda085caf46 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/GoogleMapControllerTest.java @@ -0,0 +1,61 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemaps; + +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.os.Build; +import androidx.activity.ComponentActivity; +import androidx.test.core.app.ApplicationProvider; +import com.google.android.gms.maps.GoogleMap; +import io.flutter.plugin.common.BinaryMessenger; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.Robolectric; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Build.VERSION_CODES.P) +public class GoogleMapControllerTest { + + private Context context; + private ComponentActivity activity; + private GoogleMapController googleMapController; + + @Mock BinaryMessenger mockMessenger; + @Mock GoogleMap mockGoogleMap; + + @Before + public void before() { + MockitoAnnotations.initMocks(this); + context = ApplicationProvider.getApplicationContext(); + activity = Robolectric.setupActivity(ComponentActivity.class); + googleMapController = + new GoogleMapController(0, context, mockMessenger, activity::getLifecycle, null); + googleMapController.init(); + } + + @Test + public void DisposeReleaseTheMap() throws InterruptedException { + googleMapController.onMapReady(mockGoogleMap); + assertTrue(googleMapController != null); + googleMapController.dispose(); + assertNull(googleMapController.getView()); + } + + @Test + public void OnDestroyReleaseTheMap() throws InterruptedException { + googleMapController.onMapReady(mockGoogleMap); + assertTrue(googleMapController != null); + googleMapController.onDestroy(activity); + assertNull(googleMapController.getView()); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.java new file mode 100644 index 000000000000..afe91d77e2be --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/MarkersControllerTest.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.googlemaps; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import com.google.android.gms.internal.maps.zzt; +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.Marker; +import com.google.android.gms.maps.model.MarkerOptions; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodCodec; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.junit.Test; +import org.mockito.Mockito; + +public class MarkersControllerTest { + + @Test + public void controller_OnMarkerDragStart() { + final MethodChannel methodChannel = + spy(new MethodChannel(mock(BinaryMessenger.class), "no-name", mock(MethodCodec.class))); + final MarkersController controller = new MarkersController(methodChannel); + final GoogleMap googleMap = mock(GoogleMap.class); + controller.setGoogleMap(googleMap); + + final zzt z = mock(zzt.class); + final Marker marker = new Marker(z); + + final String googleMarkerId = "abc123"; + + when(marker.getId()).thenReturn(googleMarkerId); + when(googleMap.addMarker(any(MarkerOptions.class))).thenReturn(marker); + + final LatLng latLng = new LatLng(1.1, 2.2); + final Map markerOptions = new HashMap(); + markerOptions.put("markerId", googleMarkerId); + + final List markers = Arrays.asList(markerOptions); + controller.addMarkers(markers); + controller.onMarkerDragStart(googleMarkerId, latLng); + + final List points = new ArrayList(); + points.add(latLng.latitude); + points.add(latLng.longitude); + + final Map data = new HashMap<>(); + data.put("markerId", googleMarkerId); + data.put("position", points); + Mockito.verify(methodChannel).invokeMethod("marker#onDragStart", data); + } + + @Test + public void controller_OnMarkerDragEnd() { + final MethodChannel methodChannel = + spy(new MethodChannel(mock(BinaryMessenger.class), "no-name", mock(MethodCodec.class))); + final MarkersController controller = new MarkersController(methodChannel); + final GoogleMap googleMap = mock(GoogleMap.class); + controller.setGoogleMap(googleMap); + + final zzt z = mock(zzt.class); + final Marker marker = new Marker(z); + + final String googleMarkerId = "abc123"; + + when(marker.getId()).thenReturn(googleMarkerId); + when(googleMap.addMarker(any(MarkerOptions.class))).thenReturn(marker); + + final LatLng latLng = new LatLng(1.1, 2.2); + final Map markerOptions = new HashMap(); + markerOptions.put("markerId", googleMarkerId); + + final List markers = Arrays.asList(markerOptions); + controller.addMarkers(markers); + controller.onMarkerDragEnd(googleMarkerId, latLng); + + final List points = new ArrayList(); + points.add(latLng.latitude); + points.add(latLng.longitude); + + final Map data = new HashMap<>(); + data.put("markerId", googleMarkerId); + data.put("position", points); + Mockito.verify(methodChannel).invokeMethod("marker#onDragEnd", data); + } + + @Test + public void controller_OnMarkerDrag() { + final MethodChannel methodChannel = + spy(new MethodChannel(mock(BinaryMessenger.class), "no-name", mock(MethodCodec.class))); + final MarkersController controller = new MarkersController(methodChannel); + final GoogleMap googleMap = mock(GoogleMap.class); + controller.setGoogleMap(googleMap); + + final zzt z = mock(zzt.class); + final Marker marker = new Marker(z); + + final String googleMarkerId = "abc123"; + + when(marker.getId()).thenReturn(googleMarkerId); + when(googleMap.addMarker(any(MarkerOptions.class))).thenReturn(marker); + + final LatLng latLng = new LatLng(1.1, 2.2); + final Map markerOptions = new HashMap(); + markerOptions.put("markerId", googleMarkerId); + + final List markers = Arrays.asList(markerOptions); + controller.addMarkers(markers); + controller.onMarkerDrag(googleMarkerId, latLng); + + final List points = new ArrayList(); + points.add(latLng.latitude); + points.add(latLng.longitude); + + final Map data = new HashMap<>(); + data.put("markerId", googleMarkerId); + data.put("position", points); + Mockito.verify(methodChannel).invokeMethod("marker#onDrag", data); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/android/src/test/java/io/flutter/plugins/googlemaps/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 16e93d936838..d850810db651 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,8 +33,9 @@ android { defaultConfig { applicationId "io.flutter.plugins.googlemapsexample" - minSdkVersion 16 + minSdkVersion 20 targetSdkVersion 28 + multiDexEnabled true versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -51,14 +52,22 @@ android { signingConfig signingConfigs.debug } } + + testOptions { + unitTests { + includeAndroidResources = true + } + } + + dependencies { + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + api 'androidx.test:core:1.2.0' + testImplementation 'com.google.android.gms:play-services-maps:17.0.0' + } } 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/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemaps/EmbeddingV1ActivityTest.java b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemaps/EmbeddingV1ActivityTest.java deleted file mode 100644 index ff39d1ddf55d..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemaps/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.flutter.plugins.googlemaps; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import io.flutter.plugins.googlemapsexample.*; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} 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 deleted file mode 100644 index 525d2da8d665..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemaps/MainActivityTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.flutter.plugins.googlemaps; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import io.flutter.embedding.android.FlutterActivity; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class MainActivityTest { - @Rule - public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); -} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/GoogleMapsTest.java b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/GoogleMapsTest.java new file mode 100644 index 000000000000..40552ddf7be1 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/GoogleMapsTest.java @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemapsexample; + +import static org.junit.Assert.assertTrue; + +import androidx.test.core.app.ActivityScenario; +import io.flutter.plugins.googlemaps.GoogleMapsPlugin; +import org.junit.Test; + +public class GoogleMapsTest { + @Test + public void googleMapsPluginIsAdded() { + final ActivityScenario scenario = + ActivityScenario.launch(GoogleMapsTestActivity.class); + scenario.onActivity( + activity -> { + assertTrue(activity.engine.getPlugins().has(GoogleMapsPlugin.class)); + }); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/MainActivityTest.java b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/MainActivityTest.java new file mode 100644 index 000000000000..244a22b6c6c8 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/googlemapsexample/MainActivityTest.java @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemaps; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class MainActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/debug/AndroidManifest.xml b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..9c1f83d3cec5 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/debug/java/io/flutter/plugins/googlemapsexample/GoogleMapsTestActivity.java b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/debug/java/io/flutter/plugins/googlemapsexample/GoogleMapsTestActivity.java new file mode 100644 index 000000000000..e183a7c75c4e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/debug/java/io/flutter/plugins/googlemapsexample/GoogleMapsTestActivity.java @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlemapsexample; + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; + +// Makes the FlutterEngine accessible for testing. +public class GoogleMapsTestActivity extends FlutterActivity { + public FlutterEngine engine; + + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + engine = flutterEngine; + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/main/AndroidManifest.xml b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/main/AndroidManifest.xml index 0ff45c3cb3ac..815074bfad96 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/main/AndroidManifest.xml +++ b/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/main/AndroidManifest.xml @@ -4,10 +4,7 @@ - + @@ -28,13 +25,6 @@ - - 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 deleted file mode 100644 index 8d7b3054bf5f..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/example/android/app/src/main/java/io/flutter/plugins/googlemapsexample/EmbeddingV1Activity.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.flutter.plugins.googlemapsexample; - -import android.os.Bundle; -import dev.flutter.plugins.e2e.E2EPlugin; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.googlemaps.GoogleMapsPlugin; - -public class EmbeddingV1Activity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GoogleMapsPlugin.registerWith(registrarFor("io.flutter.plugins.googlemaps.GoogleMapsPlugin")); - E2EPlugin.registerWith(registrarFor("dev.flutter.plugins.e2e.E2EPlugin")); - } -} 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 new file mode 100644 index 000000000000..a4833fe8561d --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_map_inspector.dart @@ -0,0 +1,86 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:typed_data'; +import 'package:flutter/services.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +/// Inspect Google Maps state using the platform SDK. +/// +/// This class is primarily used for testing. The methods on this +/// class should call "getters" on the GoogleMap object or equivalent +/// on the platform side. +class GoogleMapInspector { + GoogleMapInspector(this._channel); + + final MethodChannel _channel; + + Future isCompassEnabled() async { + return await _channel.invokeMethod('map#isCompassEnabled'); + } + + Future isMapToolbarEnabled() async { + return await _channel.invokeMethod('map#isMapToolbarEnabled'); + } + + Future getMinMaxZoomLevels() async { + final List zoomLevels = + (await _channel.invokeMethod>('map#getMinMaxZoomLevels'))! + .cast(); + return MinMaxZoomPreference(zoomLevels[0], zoomLevels[1]); + } + + Future getZoomLevel() async { + final double? zoomLevel = + await _channel.invokeMethod('map#getZoomLevel'); + return zoomLevel; + } + + Future isZoomGesturesEnabled() async { + return await _channel.invokeMethod('map#isZoomGesturesEnabled'); + } + + Future isZoomControlsEnabled() async { + return await _channel.invokeMethod('map#isZoomControlsEnabled'); + } + + Future isLiteModeEnabled() async { + return await _channel.invokeMethod('map#isLiteModeEnabled'); + } + + Future isRotateGesturesEnabled() async { + return await _channel.invokeMethod('map#isRotateGesturesEnabled'); + } + + Future isTiltGesturesEnabled() async { + return await _channel.invokeMethod('map#isTiltGesturesEnabled'); + } + + Future isScrollGesturesEnabled() async { + return await _channel.invokeMethod('map#isScrollGesturesEnabled'); + } + + Future isMyLocationButtonEnabled() async { + return await _channel.invokeMethod('map#isMyLocationButtonEnabled'); + } + + Future isTrafficEnabled() async { + return await _channel.invokeMethod('map#isTrafficEnabled'); + } + + Future isBuildingsEnabled() async { + return await _channel.invokeMethod('map#isBuildingsEnabled'); + } + + Future takeSnapshot() async { + return await _channel.invokeMethod('map#takeSnapshot'); + } + + Future?> getTileOverlayInfo(String id) async { + return (await _channel.invokeMapMethod( + 'map#getTileOverlayInfo', { + 'tileOverlayId': id, + })); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart new file mode 100644 index 000000000000..8bafca15c344 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/integration_test/google_maps_test.dart @@ -0,0 +1,1232 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +import 'google_map_inspector.dart'; + +const LatLng _kInitialMapCenter = LatLng(0, 0); +const double _kInitialZoomLevel = 5; +const CameraPosition _kInitialCameraPosition = + CameraPosition(target: _kInitialMapCenter, zoom: _kInitialZoomLevel); + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('testCompassToggle', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer inspectorCompleter = + Completer(); + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + compassEnabled: false, + 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; + bool? compassEnabled = await inspector.isCompassEnabled(); + expect(compassEnabled, false); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + compassEnabled: true, + onMapCreated: (GoogleMapController controller) { + fail("OnMapCreated should get called only once."); + }, + ), + )); + + compassEnabled = await inspector.isCompassEnabled(); + expect(compassEnabled, true); + }); + + testWidgets('testMapToolbarToggle', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer inspectorCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + mapToolbarEnabled: false, + 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; + bool? mapToolbarEnabled = await inspector.isMapToolbarEnabled(); + expect(mapToolbarEnabled, false); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + mapToolbarEnabled: true, + onMapCreated: (GoogleMapController controller) { + fail("OnMapCreated should get called only once."); + }, + ), + )); + + mapToolbarEnabled = await inspector.isMapToolbarEnabled(); + expect(mapToolbarEnabled, Platform.isAndroid); + }); + + testWidgets('updateMinMaxZoomLevels', (WidgetTester tester) async { + // The behaviors of setting min max zoom level on iOS and Android are different. + // On iOS, when we get the min or max zoom level after setting the preference, the + // min and max will be exactly the same as the value we set; on Android however, + // the values we get do not equal to the value we set. + // + // Also, when we call zoomTo to set the zoom, on Android, it usually + // honors the preferences that we set and the zoom cannot pass beyond the boundary. + // On iOS, on the other hand, zoomTo seems to override the preferences. + // + // Thus we test iOS and Android a little differently here. + final Key key = GlobalKey(); + final Completer inspectorCompleter = + Completer(); + late GoogleMapController controller; + + const MinMaxZoomPreference initialZoomLevel = MinMaxZoomPreference(4, 8); + const MinMaxZoomPreference finalZoomLevel = MinMaxZoomPreference(6, 10); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + minMaxZoomPreference: initialZoomLevel, + onMapCreated: (GoogleMapController c) async { + final GoogleMapInspector inspector = + // ignore: invalid_use_of_visible_for_testing_member + GoogleMapInspector(c.channel!); + controller = c; + inspectorCompleter.complete(inspector); + }, + ), + )); + + final GoogleMapInspector inspector = await inspectorCompleter.future; + + if (Platform.isIOS) { + MinMaxZoomPreference zoomLevel = await inspector.getMinMaxZoomLevels(); + expect(zoomLevel, equals(initialZoomLevel)); + } else if (Platform.isAndroid) { + await controller.moveCamera(CameraUpdate.zoomTo(15)); + await tester.pumpAndSettle(); + double? zoomLevel = await inspector.getZoomLevel(); + expect(zoomLevel, equals(initialZoomLevel.maxZoom)); + + await controller.moveCamera(CameraUpdate.zoomTo(1)); + await tester.pumpAndSettle(); + zoomLevel = await inspector.getZoomLevel(); + expect(zoomLevel, equals(initialZoomLevel.minZoom)); + } + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + minMaxZoomPreference: finalZoomLevel, + onMapCreated: (GoogleMapController controller) { + fail("OnMapCreated should get called only once."); + }, + ), + )); + + if (Platform.isIOS) { + MinMaxZoomPreference zoomLevel = await inspector.getMinMaxZoomLevels(); + expect(zoomLevel, equals(finalZoomLevel)); + } else { + await controller.moveCamera(CameraUpdate.zoomTo(15)); + await tester.pumpAndSettle(); + double? zoomLevel = await inspector.getZoomLevel(); + expect(zoomLevel, equals(finalZoomLevel.maxZoom)); + + await controller.moveCamera(CameraUpdate.zoomTo(1)); + await tester.pumpAndSettle(); + zoomLevel = await inspector.getZoomLevel(); + expect(zoomLevel, equals(finalZoomLevel.minZoom)); + } + }); + + testWidgets('testZoomGesturesEnabled', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer inspectorCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + zoomGesturesEnabled: false, + 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; + bool? zoomGesturesEnabled = await inspector.isZoomGesturesEnabled(); + expect(zoomGesturesEnabled, false); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + zoomGesturesEnabled: true, + onMapCreated: (GoogleMapController controller) { + fail("OnMapCreated should get called only once."); + }, + ), + )); + + zoomGesturesEnabled = await inspector.isZoomGesturesEnabled(); + expect(zoomGesturesEnabled, true); + }); + + testWidgets('testZoomControlsEnabled', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer inspectorCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + 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; + bool? zoomControlsEnabled = await inspector.isZoomControlsEnabled(); + expect(zoomControlsEnabled, Platform.isIOS ? false : true); + + /// Zoom Controls functionality is not available on iOS at the moment. + if (Platform.isAndroid) { + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + zoomControlsEnabled: false, + onMapCreated: (GoogleMapController controller) { + fail("OnMapCreated should get called only once."); + }, + ), + )); + + zoomControlsEnabled = await inspector.isZoomControlsEnabled(); + expect(zoomControlsEnabled, false); + } + }); + + testWidgets('testLiteModeEnabled', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer inspectorCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + liteModeEnabled: false, + onMapCreated: (GoogleMapController controller) { + final GoogleMapInspector inspector = + // ignore: invalid_use_of_visible_for_testing_member + GoogleMapInspector(controller.channel!); + inspectorCompleter.complete(inspector); + }, + ), + )); + + final GoogleMapInspector inspector = await inspectorCompleter.future; + bool? liteModeEnabled = await inspector.isLiteModeEnabled(); + expect(liteModeEnabled, false); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + liteModeEnabled: true, + onMapCreated: (GoogleMapController controller) { + fail("OnMapCreated should get called only once."); + }, + ), + )); + + liteModeEnabled = await inspector.isLiteModeEnabled(); + expect(liteModeEnabled, true); + }, skip: !Platform.isAndroid); + + testWidgets('testRotateGesturesEnabled', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer inspectorCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + rotateGesturesEnabled: false, + 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; + bool? rotateGesturesEnabled = await inspector.isRotateGesturesEnabled(); + expect(rotateGesturesEnabled, false); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + rotateGesturesEnabled: true, + onMapCreated: (GoogleMapController controller) { + fail("OnMapCreated should get called only once."); + }, + ), + )); + + rotateGesturesEnabled = await inspector.isRotateGesturesEnabled(); + expect(rotateGesturesEnabled, true); + }); + + testWidgets('testTiltGesturesEnabled', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer inspectorCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + tiltGesturesEnabled: false, + 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; + bool? tiltGesturesEnabled = await inspector.isTiltGesturesEnabled(); + expect(tiltGesturesEnabled, false); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + tiltGesturesEnabled: true, + onMapCreated: (GoogleMapController controller) { + fail("OnMapCreated should get called only once."); + }, + ), + )); + + tiltGesturesEnabled = await inspector.isTiltGesturesEnabled(); + expect(tiltGesturesEnabled, true); + }); + + testWidgets('testScrollGesturesEnabled', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer inspectorCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + scrollGesturesEnabled: false, + 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; + bool? scrollGesturesEnabled = await inspector.isScrollGesturesEnabled(); + expect(scrollGesturesEnabled, false); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + scrollGesturesEnabled: true, + onMapCreated: (GoogleMapController controller) { + fail("OnMapCreated should get called only once."); + }, + ), + )); + + scrollGesturesEnabled = await inspector.isScrollGesturesEnabled(); + expect(scrollGesturesEnabled, true); + }); + + testWidgets('testInitialCenterLocationAtCenter', (WidgetTester tester) async { + await tester.binding.setSurfaceSize(const Size(800.0, 600.0)); + final Completer mapControllerCompleter = + Completer(); + final Key key = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (GoogleMapController controller) { + mapControllerCompleter.complete(controller); + }, + ), + ), + ); + final GoogleMapController mapController = + await mapControllerCompleter.future; + + await tester.pumpAndSettle(); + + // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and `mapControllerCompleter.complete(controller)` above should happen + // in `mapRendered`. + // https://github.com/flutter/flutter/issues/54758 + await Future.delayed(Duration(seconds: 1)); + + ScreenCoordinate coordinate = + await mapController.getScreenCoordinate(_kInitialCameraPosition.target); + Rect rect = tester.getRect(find.byKey(key)); + if (Platform.isIOS) { + // On iOS, the coordinate value from the GoogleMapSdk doesn't include the devicePixelRatio`. + // So we don't need to do the conversion like we did below for other platforms. + expect(coordinate.x, (rect.center.dx - rect.topLeft.dx).round()); + expect(coordinate.y, (rect.center.dy - rect.topLeft.dy).round()); + } else { + expect( + coordinate.x, + ((rect.center.dx - rect.topLeft.dx) * + tester.binding.window.devicePixelRatio) + .round()); + expect( + coordinate.y, + ((rect.center.dy - rect.topLeft.dy) * + tester.binding.window.devicePixelRatio) + .round()); + } + await tester.binding.setSurfaceSize(null); + }); + + testWidgets('testGetVisibleRegion', (WidgetTester tester) async { + final Key key = GlobalKey(); + final LatLngBounds zeroLatLngBounds = LatLngBounds( + southwest: const LatLng(0, 0), northeast: const LatLng(0, 0)); + + final Completer mapControllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (GoogleMapController controller) { + mapControllerCompleter.complete(controller); + }, + ), + )); + await tester.pumpAndSettle(); + + final GoogleMapController mapController = + await mapControllerCompleter.future; + + final LatLngBounds firstVisibleRegion = + await mapController.getVisibleRegion(); + + expect(firstVisibleRegion, isNotNull); + expect(firstVisibleRegion.southwest, isNotNull); + expect(firstVisibleRegion.northeast, isNotNull); + expect(firstVisibleRegion, isNot(zeroLatLngBounds)); + expect(firstVisibleRegion.contains(_kInitialMapCenter), isTrue); + + // Making a new `LatLngBounds` about (10, 10) distance south west to the `firstVisibleRegion`. + // The size of the `LatLngBounds` is 10 by 10. + final LatLng southWest = LatLng(firstVisibleRegion.southwest.latitude - 20, + firstVisibleRegion.southwest.longitude - 20); + final LatLng northEast = LatLng(firstVisibleRegion.southwest.latitude - 10, + firstVisibleRegion.southwest.longitude - 10); + final LatLng newCenter = LatLng( + (northEast.latitude + southWest.latitude) / 2, + (northEast.longitude + southWest.longitude) / 2, + ); + + expect(firstVisibleRegion.contains(northEast), isFalse); + expect(firstVisibleRegion.contains(southWest), isFalse); + + final LatLngBounds latLngBounds = + LatLngBounds(southwest: southWest, northeast: northEast); + + // TODO(iskakaushik): non-zero padding is needed for some device configurations + // https://github.com/flutter/flutter/issues/30575 + final double padding = 0; + await mapController + .moveCamera(CameraUpdate.newLatLngBounds(latLngBounds, padding)); + await tester.pumpAndSettle(const Duration(seconds: 3)); + + final LatLngBounds secondVisibleRegion = + await mapController.getVisibleRegion(); + + expect(secondVisibleRegion, isNotNull); + expect(secondVisibleRegion.southwest, isNotNull); + expect(secondVisibleRegion.northeast, isNotNull); + expect(secondVisibleRegion, isNot(zeroLatLngBounds)); + + expect(firstVisibleRegion, isNot(secondVisibleRegion)); + expect(secondVisibleRegion.contains(newCenter), isTrue); + }); + + testWidgets('testTraffic', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer inspectorCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + trafficEnabled: true, + 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; + bool? isTrafficEnabled = await inspector.isTrafficEnabled(); + expect(isTrafficEnabled, true); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + trafficEnabled: false, + onMapCreated: (GoogleMapController controller) { + fail("OnMapCreated should get called only once."); + }, + ), + )); + + isTrafficEnabled = await inspector.isTrafficEnabled(); + expect(isTrafficEnabled, false); + }); + + testWidgets('testBuildings', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer inspectorCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + buildingsEnabled: true, + onMapCreated: (GoogleMapController controller) { + final GoogleMapInspector inspector = + // ignore: invalid_use_of_visible_for_testing_member + GoogleMapInspector(controller.channel!); + inspectorCompleter.complete(inspector); + }, + ), + )); + + final GoogleMapInspector inspector = await inspectorCompleter.future; + final bool? isBuildingsEnabled = await inspector.isBuildingsEnabled(); + expect(isBuildingsEnabled, true); + }); + + // Location button tests are skipped in Android because we don't have location permission to test. + testWidgets('testMyLocationButtonToggle', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer inspectorCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + myLocationButtonEnabled: true, + myLocationEnabled: false, + onMapCreated: (GoogleMapController controller) { + final GoogleMapInspector inspector = + // ignore: invalid_use_of_visible_for_testing_member + GoogleMapInspector(controller.channel!); + inspectorCompleter.complete(inspector); + }, + ), + )); + + final GoogleMapInspector inspector = await inspectorCompleter.future; + bool? myLocationButtonEnabled = await inspector.isMyLocationButtonEnabled(); + expect(myLocationButtonEnabled, true); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + myLocationButtonEnabled: false, + myLocationEnabled: false, + onMapCreated: (GoogleMapController controller) { + fail("OnMapCreated should get called only once."); + }, + ), + )); + + myLocationButtonEnabled = await inspector.isMyLocationButtonEnabled(); + expect(myLocationButtonEnabled, false); + }, skip: Platform.isAndroid); + + testWidgets('testMyLocationButton initial value false', + (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer inspectorCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + myLocationButtonEnabled: false, + myLocationEnabled: false, + onMapCreated: (GoogleMapController controller) { + final GoogleMapInspector inspector = + // ignore: invalid_use_of_visible_for_testing_member + GoogleMapInspector(controller.channel!); + inspectorCompleter.complete(inspector); + }, + ), + )); + + final GoogleMapInspector inspector = await inspectorCompleter.future; + final bool? myLocationButtonEnabled = + await inspector.isMyLocationButtonEnabled(); + expect(myLocationButtonEnabled, false); + }, skip: Platform.isAndroid); + + testWidgets('testMyLocationButton initial value true', + (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer inspectorCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + myLocationButtonEnabled: true, + myLocationEnabled: false, + onMapCreated: (GoogleMapController controller) { + final GoogleMapInspector inspector = + // ignore: invalid_use_of_visible_for_testing_member + GoogleMapInspector(controller.channel!); + inspectorCompleter.complete(inspector); + }, + ), + )); + + final GoogleMapInspector inspector = await inspectorCompleter.future; + final bool? myLocationButtonEnabled = + await inspector.isMyLocationButtonEnabled(); + expect(myLocationButtonEnabled, true); + }, skip: Platform.isAndroid); + + testWidgets('testSetMapStyle valid Json String', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (GoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + )); + + final GoogleMapController controller = await controllerCompleter.future; + final String mapStyle = + '[{"elementType":"geometry","stylers":[{"color":"#242f3e"}]}]'; + await controller.setMapStyle(mapStyle); + }); + + testWidgets('testSetMapStyle invalid Json String', + (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (GoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + )); + + final GoogleMapController controller = await controllerCompleter.future; + + try { + await controller.setMapStyle('invalid_value'); + fail('expected MapStyleException'); + } on MapStyleException catch (e) { + expect(e.cause, isNotNull); + } + }); + + testWidgets('testSetMapStyle null string', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (GoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + )); + + final GoogleMapController controller = await controllerCompleter.future; + await controller.setMapStyle(null); + }); + + testWidgets('testGetLatLng', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (GoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + )); + + final GoogleMapController controller = await controllerCompleter.future; + + await tester.pumpAndSettle(); + // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and `mapControllerCompleter.complete(controller)` above should happen + // in `mapRendered`. + // https://github.com/flutter/flutter/issues/54758 + await Future.delayed(Duration(seconds: 1)); + + final LatLngBounds visibleRegion = await controller.getVisibleRegion(); + final LatLng topLeft = + await controller.getLatLng(const ScreenCoordinate(x: 0, y: 0)); + final LatLng northWest = LatLng( + visibleRegion.northeast.latitude, + visibleRegion.southwest.longitude, + ); + + expect(topLeft, northWest); + }); + + testWidgets('testGetZoomLevel', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (GoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + )); + + final GoogleMapController controller = await controllerCompleter.future; + + await tester.pumpAndSettle(); + // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and `mapControllerCompleter.complete(controller)` above should happen + // in `mapRendered`. + // https://github.com/flutter/flutter/issues/54758 + await Future.delayed(Duration(seconds: 1)); + + double zoom = await controller.getZoomLevel(); + expect(zoom, _kInitialZoomLevel); + + await controller.moveCamera(CameraUpdate.zoomTo(7)); + await tester.pumpAndSettle(); + zoom = await controller.getZoomLevel(); + expect(zoom, equals(7)); + }); + + testWidgets('testScreenCoordinate', (WidgetTester tester) async { + final Key key = GlobalKey(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + key: key, + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (GoogleMapController controller) { + controllerCompleter.complete(controller); + }, + ), + )); + final GoogleMapController controller = await controllerCompleter.future; + + await tester.pumpAndSettle(); + // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and `mapControllerCompleter.complete(controller)` above should happen + // in `mapRendered`. + // https://github.com/flutter/flutter/issues/54758 + await Future.delayed(Duration(seconds: 1)); + + final LatLngBounds visibleRegion = await controller.getVisibleRegion(); + final LatLng northWest = LatLng( + visibleRegion.northeast.latitude, + visibleRegion.southwest.longitude, + ); + final ScreenCoordinate topLeft = + await controller.getScreenCoordinate(northWest); + expect(topLeft, const ScreenCoordinate(x: 0, y: 0)); + }); + + testWidgets('testResizeWidget', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final GoogleMap map = GoogleMap( + initialCameraPosition: _kInitialCameraPosition, + onMapCreated: (GoogleMapController controller) async { + controllerCompleter.complete(controller); + }, + ); + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: MaterialApp( + home: Scaffold( + body: SizedBox(height: 100, width: 100, child: map))))); + final GoogleMapController controller = await controllerCompleter.future; + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: MaterialApp( + home: Scaffold( + body: SizedBox(height: 400, width: 400, child: map))))); + + await tester.pumpAndSettle(); + // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and `mapControllerCompleter.complete(controller)` above should happen + // in `mapRendered`. + // https://github.com/flutter/flutter/issues/54758 + await Future.delayed(Duration(seconds: 1)); + + // Simple call to make sure that the app hasn't crashed. + final LatLngBounds bounds1 = await controller.getVisibleRegion(); + final LatLngBounds bounds2 = await controller.getVisibleRegion(); + expect(bounds1, bounds2); + }); + + testWidgets('testToggleInfoWindow', (WidgetTester tester) async { + final Marker marker = Marker( + markerId: MarkerId("marker"), + infoWindow: InfoWindow(title: "InfoWindow")); + final Set markers = {marker}; + + Completer controllerCompleter = + Completer(); + + await tester.pumpWidget(Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), + markers: markers, + onMapCreated: (GoogleMapController googleMapController) { + controllerCompleter.complete(googleMapController); + }, + ), + )); + + GoogleMapController controller = await controllerCompleter.future; + + bool iwVisibleStatus = + await controller.isMarkerInfoWindowShown(marker.markerId); + expect(iwVisibleStatus, false); + + await controller.showMarkerInfoWindow(marker.markerId); + iwVisibleStatus = await controller.isMarkerInfoWindowShown(marker.markerId); + expect(iwVisibleStatus, true); + + await controller.hideMarkerInfoWindow(marker.markerId); + iwVisibleStatus = await controller.isMarkerInfoWindowShown(marker.markerId); + expect(iwVisibleStatus, false); + }); + + testWidgets("fromAssetImage", (WidgetTester tester) async { + double pixelRatio = 2; + final ImageConfiguration imageConfiguration = + ImageConfiguration(devicePixelRatio: pixelRatio); + final BitmapDescriptor mip = await BitmapDescriptor.fromAssetImage( + imageConfiguration, 'red_square.png'); + final BitmapDescriptor scaled = await BitmapDescriptor.fromAssetImage( + imageConfiguration, 'red_square.png', + mipmaps: false); + expect((mip.toJson() as List)[2], 1); + expect((scaled.toJson() as List)[2], 2); + }); + + testWidgets('testTakeSnapshot', (WidgetTester tester) async { + Completer inspectorCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: _kInitialCameraPosition, + 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; + 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/Flutter/AppFrameworkInfo.plist b/packages/google_maps_flutter/google_maps_flutter/example/ios/Flutter/AppFrameworkInfo.plist index 6c2de8086bcd..3a9c234f96d4 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/google_maps_flutter/google_maps_flutter/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 8.0 + 9.0 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..9686afaf3c99 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_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__)) + 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) + target.build_configurations.each do |build_configuration| + build_configuration.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64 i386' + end + end +end diff --git a/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/google_maps_flutter/google_maps_flutter/example/ios/Runner.xcodeproj/project.pbxproj index f6a2d6ec291a..fbb006aeded0 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; @@ -378,7 +535,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -388,7 +545,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -429,7 +585,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -443,6 +599,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -453,7 +610,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; @@ -464,6 +621,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -474,11 +632,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 +714,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/webview_flutter/example/ios/webview_flutter_exampleTests/Info.plist b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerTests/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/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/google_maps_flutter/google_maps_flutter/example/ios/RunnerUITests/Info.plist b/packages/google_maps_flutter/google_maps_flutter/example/ios/RunnerUITests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/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_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/drag_marker.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/drag_marker.dart new file mode 100644 index 000000000000..2c7929115247 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/drag_marker.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. + +// ignore_for_file: public_member_api_docs + +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; + +import 'page.dart'; + +class DragMarkerPage extends GoogleMapExampleAppPage { + DragMarkerPage() : super(const Icon(Icons.drag_handle), 'Drag marker'); + + @override + Widget build(BuildContext context) { + return const DragMarkerBody(); + } +} + +class DragMarkerBody extends StatefulWidget { + const DragMarkerBody(); + + @override + State createState() => DragMarkerBodyState(); +} + +typedef MarkerUpdateAction = Marker Function(Marker marker); + +class DragMarkerBodyState extends State { + DragMarkerBodyState(); + static const LatLng center = LatLng(-33.86711, 151.1947171); + + GoogleMapController? controller; + Map markers = {}; + MarkerId? selectedMarker; + int _markerIdCounter = 1; + LatLng? markerPosition; + + void _onMapCreated(GoogleMapController controller) { + this.controller = controller; + } + + void _onMarkerTapped(MarkerId markerId) { + final Marker? tappedMarker = markers[markerId]; + if (tappedMarker != null) { + setState(() { + if (markers.containsKey(selectedMarker)) { + final Marker resetOld = markers[selectedMarker]! + .copyWith(iconParam: BitmapDescriptor.defaultMarker); + markers[selectedMarker!] = resetOld; + } + selectedMarker = markerId; + final Marker newMarker = tappedMarker.copyWith( + iconParam: BitmapDescriptor.defaultMarkerWithHue( + BitmapDescriptor.hueGreen, + ), + ); + markers[markerId] = newMarker; + }); + } + } + + void _onMarkerDrag(MarkerId markerId, LatLng newPosition) async { + setState(() { + this.markerPosition = newPosition; + }); + } + + void _add() { + final int markerCount = markers.length; + + if (markerCount == 12) { + return; + } + + final String markerIdVal = 'marker_id_$_markerIdCounter'; + _markerIdCounter++; + final MarkerId markerId = MarkerId(markerIdVal); + + final Marker marker = Marker( + draggable: true, + markerId: markerId, + position: LatLng( + center.latitude + sin(_markerIdCounter * pi / 6.0) / 20.0, + center.longitude + cos(_markerIdCounter * pi / 6.0) / 20.0, + ), + infoWindow: InfoWindow(title: markerIdVal, snippet: '*'), + onTap: () => _onMarkerTapped(markerId), + onDrag: (LatLng position) => _onMarkerDrag(markerId, position), + ); + + setState(() { + markers[markerId] = marker; + }); + } + + void _remove() { + setState(() { + if (markers.containsKey(selectedMarker)) { + markers.remove(selectedMarker); + } + }); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Expanded( + child: Center( + child: GoogleMap( + onMapCreated: _onMapCreated, + initialCameraPosition: const CameraPosition( + target: center, + zoom: 11.0, + ), + markers: markers.values.toSet(), + ), + ), + ), + Container( + height: 30, + padding: EdgeInsets.only(left: 12, right: 12), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + markerPosition == null + ? Container() + : Expanded(child: Text("lat: ${markerPosition!.latitude}")), + markerPosition == null + ? Container() + : Expanded(child: Text("lng: ${markerPosition!.longitude}")), + ], + ), + ), + Row( + children: [ + TextButton( + child: const Text('add'), + onPressed: _add, + ), + TextButton( + child: const Text('remove'), + onPressed: _remove, + ), + ], + ), + ], + ); + } +} 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 new file mode 100644 index 000000000000..f6d6f54e135a --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/example/lib/lite_mode.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. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'page.dart'; + +const CameraPosition _kInitialPosition = + CameraPosition(target: LatLng(-33.852, 151.211), zoom: 11.0); + +class LiteModePage extends GoogleMapExampleAppPage { + LiteModePage() : super(const Icon(Icons.map), 'Lite mode'); + + @override + Widget build(BuildContext context) { + return const _LiteModeBody(); + } +} + +class _LiteModeBody extends StatelessWidget { + const _LiteModeBody(); + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 30.0), + child: Center( + child: SizedBox( + width: 300.0, + height: 300.0, + child: GoogleMap( + initialCameraPosition: _kInitialPosition, + liteModeEnabled: true, + ), + ), + ), + ), + ); + } +} 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 95d8c4503404..16f242c9e0ce 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,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. // ignore_for_file: public_member_api_docs import 'package:flutter/material.dart'; +import 'package:google_maps_flutter_example/drag_marker.dart'; +import 'package:google_maps_flutter_example/lite_mode.dart'; import 'animate_camera.dart'; import 'map_click.dart'; import 'map_coordinates.dart'; @@ -19,6 +21,7 @@ import 'place_polygon.dart'; import 'place_polyline.dart'; import 'scrolling_map.dart'; import 'snapshot.dart'; +import 'tile_overlay.dart'; final List _allPages = [ MapUiPage(), @@ -32,8 +35,11 @@ final List _allPages = [ PlacePolylinePage(), PlacePolygonPage(), PlaceCirclePage(), + DragMarkerPage(), 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 051d658ddff8..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,12 +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. // ignore_for_file: public_member_api_docs import 'package:flutter/material.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:flutter/services.dart' show rootBundle; +import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'page.dart'; @@ -56,7 +56,7 @@ class MapUiBodyState extends State { bool _myLocationEnabled = true; bool _myTrafficEnabled = false; bool _myLocationButtonEnabled = true; - GoogleMapController _controller; + late GoogleMapController _controller; bool _nightMode = false; @override @@ -70,7 +70,7 @@ class MapUiBodyState extends State { } Widget _compassToggler() { - return FlatButton( + return TextButton( child: Text('${_compassEnabled ? 'disable' : 'enable'} compass'), onPressed: () { setState(() { @@ -81,7 +81,7 @@ class MapUiBodyState extends State { } Widget _mapToolbarToggler() { - return FlatButton( + return TextButton( child: Text('${_mapToolbarEnabled ? 'disable' : 'enable'} map toolbar'), onPressed: () { setState(() { @@ -92,7 +92,7 @@ class MapUiBodyState extends State { } Widget _latLngBoundsToggler() { - return FlatButton( + return TextButton( child: Text( _cameraTargetBounds.bounds == null ? 'bound camera target' @@ -109,7 +109,7 @@ class MapUiBodyState extends State { } Widget _zoomBoundsToggler() { - return FlatButton( + return TextButton( child: Text(_minMaxZoomPreference.minZoom == null ? 'bound zoom' : 'release zoom'), @@ -126,7 +126,7 @@ class MapUiBodyState extends State { Widget _mapTypeCycler() { final MapType nextType = MapType.values[(_mapType.index + 1) % MapType.values.length]; - return FlatButton( + return TextButton( child: Text('change map type to $nextType'), onPressed: () { setState(() { @@ -137,7 +137,7 @@ class MapUiBodyState extends State { } Widget _rotateToggler() { - return FlatButton( + return TextButton( child: Text('${_rotateGesturesEnabled ? 'disable' : 'enable'} rotate'), onPressed: () { setState(() { @@ -148,7 +148,7 @@ class MapUiBodyState extends State { } Widget _scrollToggler() { - return FlatButton( + return TextButton( child: Text('${_scrollGesturesEnabled ? 'disable' : 'enable'} scroll'), onPressed: () { setState(() { @@ -159,7 +159,7 @@ class MapUiBodyState extends State { } Widget _tiltToggler() { - return FlatButton( + return TextButton( child: Text('${_tiltGesturesEnabled ? 'disable' : 'enable'} tilt'), onPressed: () { setState(() { @@ -170,7 +170,7 @@ class MapUiBodyState extends State { } Widget _zoomToggler() { - return FlatButton( + return TextButton( child: Text('${_zoomGesturesEnabled ? 'disable' : 'enable'} zoom'), onPressed: () { setState(() { @@ -181,7 +181,7 @@ class MapUiBodyState extends State { } Widget _zoomControlsToggler() { - return FlatButton( + return TextButton( child: Text('${_zoomControlsEnabled ? 'disable' : 'enable'} zoom controls'), onPressed: () { @@ -193,7 +193,7 @@ class MapUiBodyState extends State { } Widget _indoorViewToggler() { - return FlatButton( + return TextButton( child: Text('${_indoorViewEnabled ? 'disable' : 'enable'} indoor'), onPressed: () { setState(() { @@ -204,9 +204,9 @@ class MapUiBodyState extends State { } Widget _myLocationToggler() { - return FlatButton( + return TextButton( child: Text( - '${_myLocationButtonEnabled ? 'disable' : 'enable'} my location button'), + '${_myLocationEnabled ? 'disable' : 'enable'} my location marker'), onPressed: () { setState(() { _myLocationEnabled = !_myLocationEnabled; @@ -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 e0fcc427c1d6..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,23 +57,25 @@ 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 { if (_markerIcon == null) { final ImageConfiguration imageConfiguration = - createLocalImageConfiguration(context); + createLocalImageConfiguration(context, size: Size.square(48)); BitmapDescriptor.fromAssetImage( imageConfiguration, 'assets/red_square.png') .then(_updateBitmap); diff --git a/packages/google_maps_flutter/google_maps_flutter/example/lib/move_camera.dart b/packages/google_maps_flutter/google_maps_flutter/example/lib/move_camera.dart index 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..53f553eb67f8 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. @@ -7,6 +7,7 @@ import 'dart:async'; import 'dart:math'; import 'dart:ui'; +import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; @@ -29,15 +30,15 @@ class PlaceMarkerBody extends StatefulWidget { State createState() => PlaceMarkerBodyState(); } -typedef Marker MarkerUpdateAction(Marker marker); +typedef MarkerUpdateAction = Marker Function(Marker marker); class PlaceMarkerBodyState extends State { PlaceMarkerBodyState(); static final LatLng center = const LatLng(-33.86711, 151.1947171); - GoogleMapController controller; + GoogleMapController? controller; Map markers = {}; - MarkerId selectedMarker; + MarkerId? selectedMarker; int _markerIdCounter = 1; void _onMapCreated(GoogleMapController controller) { @@ -50,13 +51,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 +72,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 +128,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 +153,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 +177,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,84 +207,79 @@ 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, ); }); } -// 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 -// API before it makes it to stable I'm commenting out this sample for now -// TODO(amirh): uncomment this one the ImageStream API change makes it to stable. -// https://github.com/flutter/flutter/issues/33438 -// -// void _setMarkerIcon(BitmapDescriptor assetIcon) { -// if (selectedMarker == null) { -// return; -// } -// -// final Marker marker = markers[selectedMarker]; -// setState(() { -// markers[selectedMarker] = marker.copyWith( -// iconParam: assetIcon, -// ); -// }); -// } -// -// Future _getAssetIcon(BuildContext context) async { -// final Completer bitmapIcon = -// Completer(); -// final ImageConfiguration config = createLocalImageConfiguration(context); -// -// const AssetImage('assets/red_square.png') -// .resolve(config) -// .addListener(ImageStreamListener((ImageInfo image, bool sync) async { -// final ByteData bytes = -// await image.image.toByteData(format: ImageByteFormat.png); -// final BitmapDescriptor bitmap = -// BitmapDescriptor.fromBytes(bytes.buffer.asUint8List()); -// bitmapIcon.complete(bitmap); -// })); -// -// return await bitmapIcon.future; -// } + void _setMarkerIcon(MarkerId markerId, BitmapDescriptor assetIcon) { + final Marker marker = markers[markerId]!; + setState(() { + markers[markerId] = marker.copyWith( + iconParam: assetIcon, + ); + }); + } + + Future _getAssetIcon(BuildContext context) async { + final Completer bitmapIcon = + Completer(); + final ImageConfiguration config = createLocalImageConfiguration(context); + + const AssetImage('assets/red_square.png') + .resolve(config) + .addListener(ImageStreamListener((ImageInfo image, bool sync) async { + final ByteData? bytes = + await image.image.toByteData(format: ImageByteFormat.png); + if (bytes == null) { + bitmapIcon.completeError(Exception('Unable to encode icon')); + return; + } + final BitmapDescriptor bitmap = + BitmapDescriptor.fromBytes(bytes.buffer.asUint8List()); + bitmapIcon.complete(bitmap); + })); + + return await bitmapIcon.future; + } @override Widget build(BuildContext context) { + final MarkerId? selectedId = selectedMarker; return Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -297,9 +294,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,74 +307,92 @@ 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), + ), + TextButton( + child: const Text('set marker icon'), + onPressed: selectedId == null + ? null + : () { + _getAssetIcon(context).then( + (BitmapDescriptor icon) { + _setMarkerIcon(selectedId, icon); + }, + ); + }, ), - // 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 - // API before it makes it to stable I'm commenting out this sample for now - // TODO(amirh): uncomment this one the ImageStream API change makes it to stable. - // https://github.com/flutter/flutter/issues/33438 - // - // FlatButton( - // child: const Text('set marker icon'), - // onPressed: () { - // _getAssetIcon(context).then( - // (BitmapDescriptor icon) { - // _setMarkerIcon(icon); - // }, - // ); - // }, - // ), ], ), ], 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 0c9da634faa7..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,11 +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. // ignore_for_file: public_member_api_docs -import 'dart:io' show Platform; - +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; @@ -30,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; @@ -92,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; }); @@ -128,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], ); }); @@ -202,7 +201,9 @@ class PlacePolylineBodyState extends State { @override Widget build(BuildContext context) { - final bool iOSorNotSelected = Platform.isIOS || (selectedPolyline == null); + final bool isIOS = !kIsWeb && defaultTargetPlatform == TargetPlatform.iOS; + + final PolylineId? selectedId = selectedPolyline; return Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, @@ -214,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), @@ -231,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 c7b2c5ff6715..cd614c7e4384 100644 --- a/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml +++ b/packages/google_maps_flutter/google_maps_flutter/example/pubspec.yaml @@ -1,62 +1,34 @@ name: google_maps_flutter_example description: Demonstrates how to use the google_maps_flutter plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.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: + espresso: ^0.1.0+2 flutter_driver: sdk: flutter - test: ^1.6.0 - e2e: ^0.2.1 - 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 + integration_test: + 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/google_map_inspector.dart b/packages/google_maps_flutter/google_maps_flutter/example/test_driver/google_map_inspector.dart deleted file mode 100644 index 3583d4fd5b71..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/example/test_driver/google_map_inspector.dart +++ /dev/null @@ -1,75 +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:typed_data'; -import 'package:flutter/services.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; - -/// Inspect Google Maps state using the platform SDK. -/// -/// This class is primarily used for testing. The methods on this -/// class should call "getters" on the GoogleMap object or equivalent -/// on the platform side. -class GoogleMapInspector { - GoogleMapInspector(this._channel); - - final MethodChannel _channel; - - Future isCompassEnabled() async { - return await _channel.invokeMethod('map#isCompassEnabled'); - } - - Future isMapToolbarEnabled() async { - return await _channel.invokeMethod('map#isMapToolbarEnabled'); - } - - Future getMinMaxZoomLevels() async { - final List zoomLevels = - (await _channel.invokeMethod>('map#getMinMaxZoomLevels')) - .cast(); - return MinMaxZoomPreference(zoomLevels[0], zoomLevels[1]); - } - - Future getZoomLevel() async { - final double zoomLevel = - await _channel.invokeMethod('map#getZoomLevel'); - return zoomLevel; - } - - Future isZoomGesturesEnabled() async { - return await _channel.invokeMethod('map#isZoomGesturesEnabled'); - } - - Future isZoomControlsEnabled() async { - return await _channel.invokeMethod('map#isZoomControlsEnabled'); - } - - Future isRotateGesturesEnabled() async { - return await _channel.invokeMethod('map#isRotateGesturesEnabled'); - } - - Future isTiltGesturesEnabled() async { - return await _channel.invokeMethod('map#isTiltGesturesEnabled'); - } - - Future isScrollGesturesEnabled() async { - return await _channel.invokeMethod('map#isScrollGesturesEnabled'); - } - - Future isMyLocationButtonEnabled() async { - return await _channel.invokeMethod('map#isMyLocationButtonEnabled'); - } - - Future isTrafficEnabled() async { - return await _channel.invokeMethod('map#isTrafficEnabled'); - } - - Future isBuildingsEnabled() async { - return await _channel.invokeMethod('map#isBuildingsEnabled'); - } - - Future takeSnapshot() async { - return await _channel.invokeMethod('map#takeSnapshot'); - } -} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/test_driver/google_maps_e2e.dart b/packages/google_maps_flutter/google_maps_flutter/example/test_driver/google_maps_e2e.dart deleted file mode 100644 index 9620d6067c65..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/example/test_driver/google_maps_e2e.dart +++ /dev/null @@ -1,947 +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:io'; -import 'dart:typed_data'; - -import 'package:e2e/e2e.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; - -import 'google_map_inspector.dart'; - -const LatLng _kInitialMapCenter = LatLng(0, 0); -const double _kInitialZoomLevel = 5; -const CameraPosition _kInitialCameraPosition = - CameraPosition(target: _kInitialMapCenter, zoom: _kInitialZoomLevel); - -void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('testCompassToggle', (WidgetTester tester) async { - final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - compassEnabled: false, - 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; - bool compassEnabled = await inspector.isCompassEnabled(); - expect(compassEnabled, false); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - compassEnabled: true, - onMapCreated: (GoogleMapController controller) { - fail("OnMapCreated should get called only once."); - }, - ), - )); - - compassEnabled = await inspector.isCompassEnabled(); - expect(compassEnabled, true); - }); - - testWidgets('testMapToolbarToggle', (WidgetTester tester) async { - final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - mapToolbarEnabled: false, - 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; - bool mapToolbarEnabled = await inspector.isMapToolbarEnabled(); - expect(mapToolbarEnabled, false); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - mapToolbarEnabled: true, - onMapCreated: (GoogleMapController controller) { - fail("OnMapCreated should get called only once."); - }, - ), - )); - - mapToolbarEnabled = await inspector.isMapToolbarEnabled(); - expect(mapToolbarEnabled, Platform.isAndroid); - }); - - testWidgets('updateMinMaxZoomLevels', (WidgetTester tester) async { - // The behaviors of setting min max zoom level on iOS and Android are different. - // On iOS, when we get the min or max zoom level after setting the preference, the - // min and max will be exactly the same as the value we set; on Android however, - // the values we get do not equal to the value we set. - // - // Also, when we call zoomTo to set the zoom, on Android, it usually - // honors the preferences that we set and the zoom cannot pass beyond the boundary. - // On iOS, on the other hand, zoomTo seems to override the preferences. - // - // Thus we test iOS and Android a little differently here. - final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); - GoogleMapController controller; - - const MinMaxZoomPreference initialZoomLevel = MinMaxZoomPreference(4, 8); - const MinMaxZoomPreference finalZoomLevel = MinMaxZoomPreference(6, 10); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - minMaxZoomPreference: initialZoomLevel, - onMapCreated: (GoogleMapController c) async { - final GoogleMapInspector inspector = - // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(c.channel); - controller = c; - inspectorCompleter.complete(inspector); - }, - ), - )); - - final GoogleMapInspector inspector = await inspectorCompleter.future; - - if (Platform.isIOS) { - MinMaxZoomPreference zoomLevel = await inspector.getMinMaxZoomLevels(); - expect(zoomLevel, equals(initialZoomLevel)); - } else if (Platform.isAndroid) { - await controller.moveCamera(CameraUpdate.zoomTo(15)); - await tester.pumpAndSettle(); - double zoomLevel = await inspector.getZoomLevel(); - expect(zoomLevel, equals(initialZoomLevel.maxZoom)); - - await controller.moveCamera(CameraUpdate.zoomTo(1)); - await tester.pumpAndSettle(); - zoomLevel = await inspector.getZoomLevel(); - expect(zoomLevel, equals(initialZoomLevel.minZoom)); - } - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - minMaxZoomPreference: finalZoomLevel, - onMapCreated: (GoogleMapController controller) { - fail("OnMapCreated should get called only once."); - }, - ), - )); - - if (Platform.isIOS) { - MinMaxZoomPreference zoomLevel = await inspector.getMinMaxZoomLevels(); - expect(zoomLevel, equals(finalZoomLevel)); - } else { - await controller.moveCamera(CameraUpdate.zoomTo(15)); - await tester.pumpAndSettle(); - double zoomLevel = await inspector.getZoomLevel(); - expect(zoomLevel, equals(finalZoomLevel.maxZoom)); - - await controller.moveCamera(CameraUpdate.zoomTo(1)); - await tester.pumpAndSettle(); - zoomLevel = await inspector.getZoomLevel(); - expect(zoomLevel, equals(finalZoomLevel.minZoom)); - } - }); - - testWidgets('testZoomGesturesEnabled', (WidgetTester tester) async { - final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - zoomGesturesEnabled: false, - 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; - bool zoomGesturesEnabled = await inspector.isZoomGesturesEnabled(); - expect(zoomGesturesEnabled, false); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - zoomGesturesEnabled: true, - onMapCreated: (GoogleMapController controller) { - fail("OnMapCreated should get called only once."); - }, - ), - )); - - zoomGesturesEnabled = await inspector.isZoomGesturesEnabled(); - expect(zoomGesturesEnabled, true); - }); - - testWidgets('testZoomControlsEnabled', (WidgetTester tester) async { - final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - 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; - bool zoomControlsEnabled = await inspector.isZoomControlsEnabled(); - expect(zoomControlsEnabled, Platform.isIOS ? false : true); - - /// Zoom Controls functionality is not available on iOS at the moment. - if (Platform.isAndroid) { - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - zoomControlsEnabled: false, - onMapCreated: (GoogleMapController controller) { - fail("OnMapCreated should get called only once."); - }, - ), - )); - - zoomControlsEnabled = await inspector.isZoomControlsEnabled(); - expect(zoomControlsEnabled, false); - } - }); - - testWidgets('testRotateGesturesEnabled', (WidgetTester tester) async { - final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - rotateGesturesEnabled: false, - 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; - bool rotateGesturesEnabled = await inspector.isRotateGesturesEnabled(); - expect(rotateGesturesEnabled, false); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - rotateGesturesEnabled: true, - onMapCreated: (GoogleMapController controller) { - fail("OnMapCreated should get called only once."); - }, - ), - )); - - rotateGesturesEnabled = await inspector.isRotateGesturesEnabled(); - expect(rotateGesturesEnabled, true); - }); - - testWidgets('testTiltGesturesEnabled', (WidgetTester tester) async { - final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - tiltGesturesEnabled: false, - 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; - bool tiltGesturesEnabled = await inspector.isTiltGesturesEnabled(); - expect(tiltGesturesEnabled, false); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - tiltGesturesEnabled: true, - onMapCreated: (GoogleMapController controller) { - fail("OnMapCreated should get called only once."); - }, - ), - )); - - tiltGesturesEnabled = await inspector.isTiltGesturesEnabled(); - expect(tiltGesturesEnabled, true); - }); - - testWidgets('testScrollGesturesEnabled', (WidgetTester tester) async { - final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - scrollGesturesEnabled: false, - 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; - bool scrollGesturesEnabled = await inspector.isScrollGesturesEnabled(); - expect(scrollGesturesEnabled, false); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - scrollGesturesEnabled: true, - onMapCreated: (GoogleMapController controller) { - fail("OnMapCreated should get called only once."); - }, - ), - )); - - scrollGesturesEnabled = await inspector.isScrollGesturesEnabled(); - expect(scrollGesturesEnabled, true); - }); - - testWidgets('testInitialCenterLocationAtCenter', (WidgetTester tester) async { - final Completer mapControllerCompleter = - Completer(); - final Key key = GlobalKey(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - onMapCreated: (GoogleMapController controller) { - mapControllerCompleter.complete(controller); - }, - ), - ), - ); - final GoogleMapController mapController = - await mapControllerCompleter.future; - - await tester.pumpAndSettle(); - // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and `mapControllerCompleter.complete(controller)` above should happen - // in `mapRendered`. - // https://github.com/flutter/flutter/issues/54758 - await Future.delayed(Duration(seconds: 1)); - - ScreenCoordinate coordinate = - await mapController.getScreenCoordinate(_kInitialCameraPosition.target); - Rect rect = tester.getRect(find.byKey(key)); - if (Platform.isIOS) { - // On iOS, the coordinate value from the GoogleMapSdk doesn't include the devicePixelRatio`. - // So we don't need to do the conversion like we did below for other platforms. - expect(coordinate.x, (rect.center.dx - rect.topLeft.dx).round()); - expect(coordinate.y, (rect.center.dy - rect.topLeft.dy).round()); - } else { - expect( - coordinate.x, - ((rect.center.dx - rect.topLeft.dx) * - tester.binding.window.devicePixelRatio) - .round()); - expect( - coordinate.y, - ((rect.center.dy - rect.topLeft.dy) * - tester.binding.window.devicePixelRatio) - .round()); - } - }); - - testWidgets('testGetVisibleRegion', (WidgetTester tester) async { - final Key key = GlobalKey(); - final LatLngBounds zeroLatLngBounds = LatLngBounds( - southwest: const LatLng(0, 0), northeast: const LatLng(0, 0)); - - final Completer mapControllerCompleter = - Completer(); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - onMapCreated: (GoogleMapController controller) { - mapControllerCompleter.complete(controller); - }, - ), - )); - await tester.pumpAndSettle(); - - final GoogleMapController mapController = - await mapControllerCompleter.future; - - final LatLngBounds firstVisibleRegion = - await mapController.getVisibleRegion(); - - expect(firstVisibleRegion, isNotNull); - expect(firstVisibleRegion.southwest, isNotNull); - expect(firstVisibleRegion.northeast, isNotNull); - expect(firstVisibleRegion, isNot(zeroLatLngBounds)); - expect(firstVisibleRegion.contains(_kInitialMapCenter), isTrue); - - // Making a new `LatLngBounds` about (10, 10) distance south west to the `firstVisibleRegion`. - // The size of the `LatLngBounds` is 10 by 10. - final LatLng southWest = LatLng(firstVisibleRegion.southwest.latitude - 20, - firstVisibleRegion.southwest.longitude - 20); - final LatLng northEast = LatLng(firstVisibleRegion.southwest.latitude - 10, - firstVisibleRegion.southwest.longitude - 10); - final LatLng newCenter = LatLng( - (northEast.latitude + southWest.latitude) / 2, - (northEast.longitude + southWest.longitude) / 2, - ); - - expect(firstVisibleRegion.contains(northEast), isFalse); - expect(firstVisibleRegion.contains(southWest), isFalse); - - final LatLngBounds latLngBounds = - LatLngBounds(southwest: southWest, northeast: northEast); - - // TODO(iskakaushik): non-zero padding is needed for some device configurations - // https://github.com/flutter/flutter/issues/30575 - final double padding = 0; - await mapController - .moveCamera(CameraUpdate.newLatLngBounds(latLngBounds, padding)); - await tester.pumpAndSettle(const Duration(seconds: 3)); - - final LatLngBounds secondVisibleRegion = - await mapController.getVisibleRegion(); - - expect(secondVisibleRegion, isNotNull); - expect(secondVisibleRegion.southwest, isNotNull); - expect(secondVisibleRegion.northeast, isNotNull); - expect(secondVisibleRegion, isNot(zeroLatLngBounds)); - - expect(firstVisibleRegion, isNot(secondVisibleRegion)); - expect(secondVisibleRegion.contains(newCenter), isTrue); - }); - - testWidgets('testTraffic', (WidgetTester tester) async { - final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - trafficEnabled: true, - 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; - bool isTrafficEnabled = await inspector.isTrafficEnabled(); - expect(isTrafficEnabled, true); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - trafficEnabled: false, - onMapCreated: (GoogleMapController controller) { - fail("OnMapCreated should get called only once."); - }, - ), - )); - - isTrafficEnabled = await inspector.isTrafficEnabled(); - expect(isTrafficEnabled, false); - }); - - testWidgets('testBuildings', (WidgetTester tester) async { - final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - buildingsEnabled: true, - onMapCreated: (GoogleMapController controller) { - final GoogleMapInspector inspector = - // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel); - inspectorCompleter.complete(inspector); - }, - ), - )); - - final GoogleMapInspector inspector = await inspectorCompleter.future; - final bool isBuildingsEnabled = await inspector.isBuildingsEnabled(); - expect(isBuildingsEnabled, true); - }); - - // Location button tests are skipped in Android because we don't have location permission to test. - testWidgets('testMyLocationButtonToggle', (WidgetTester tester) async { - final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - myLocationButtonEnabled: true, - myLocationEnabled: false, - onMapCreated: (GoogleMapController controller) { - final GoogleMapInspector inspector = - // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel); - inspectorCompleter.complete(inspector); - }, - ), - )); - - final GoogleMapInspector inspector = await inspectorCompleter.future; - bool myLocationButtonEnabled = await inspector.isMyLocationButtonEnabled(); - expect(myLocationButtonEnabled, true); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - myLocationButtonEnabled: false, - myLocationEnabled: false, - onMapCreated: (GoogleMapController controller) { - fail("OnMapCreated should get called only once."); - }, - ), - )); - - myLocationButtonEnabled = await inspector.isMyLocationButtonEnabled(); - expect(myLocationButtonEnabled, false); - }, skip: Platform.isAndroid); - - testWidgets('testMyLocationButton initial value false', - (WidgetTester tester) async { - final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - myLocationButtonEnabled: false, - myLocationEnabled: false, - onMapCreated: (GoogleMapController controller) { - final GoogleMapInspector inspector = - // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel); - inspectorCompleter.complete(inspector); - }, - ), - )); - - final GoogleMapInspector inspector = await inspectorCompleter.future; - final bool myLocationButtonEnabled = - await inspector.isMyLocationButtonEnabled(); - expect(myLocationButtonEnabled, false); - }, skip: Platform.isAndroid); - - testWidgets('testMyLocationButton initial value true', - (WidgetTester tester) async { - final Key key = GlobalKey(); - final Completer inspectorCompleter = - Completer(); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - myLocationButtonEnabled: true, - myLocationEnabled: false, - onMapCreated: (GoogleMapController controller) { - final GoogleMapInspector inspector = - // ignore: invalid_use_of_visible_for_testing_member - GoogleMapInspector(controller.channel); - inspectorCompleter.complete(inspector); - }, - ), - )); - - final GoogleMapInspector inspector = await inspectorCompleter.future; - final bool myLocationButtonEnabled = - await inspector.isMyLocationButtonEnabled(); - expect(myLocationButtonEnabled, true); - }, skip: Platform.isAndroid); - - testWidgets('testSetMapStyle valid Json String', (WidgetTester tester) async { - final Key key = GlobalKey(); - final Completer controllerCompleter = - Completer(); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - onMapCreated: (GoogleMapController controller) { - controllerCompleter.complete(controller); - }, - ), - )); - - final GoogleMapController controller = await controllerCompleter.future; - final String mapStyle = - '[{"elementType":"geometry","stylers":[{"color":"#242f3e"}]}]'; - await controller.setMapStyle(mapStyle); - }); - - testWidgets('testSetMapStyle invalid Json String', - (WidgetTester tester) async { - final Key key = GlobalKey(); - final Completer controllerCompleter = - Completer(); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - onMapCreated: (GoogleMapController controller) { - controllerCompleter.complete(controller); - }, - ), - )); - - final GoogleMapController controller = await controllerCompleter.future; - - try { - await controller.setMapStyle('invalid_value'); - fail('expected MapStyleException'); - } on MapStyleException catch (e) { - expect(e.cause, isNotNull); - } - }); - - testWidgets('testSetMapStyle null string', (WidgetTester tester) async { - final Key key = GlobalKey(); - final Completer controllerCompleter = - Completer(); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - onMapCreated: (GoogleMapController controller) { - controllerCompleter.complete(controller); - }, - ), - )); - - final GoogleMapController controller = await controllerCompleter.future; - await controller.setMapStyle(null); - }); - - testWidgets('testGetLatLng', (WidgetTester tester) async { - final Key key = GlobalKey(); - final Completer controllerCompleter = - Completer(); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - onMapCreated: (GoogleMapController controller) { - controllerCompleter.complete(controller); - }, - ), - )); - - final GoogleMapController controller = await controllerCompleter.future; - - await tester.pumpAndSettle(); - // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and `mapControllerCompleter.complete(controller)` above should happen - // in `mapRendered`. - // https://github.com/flutter/flutter/issues/54758 - await Future.delayed(Duration(seconds: 1)); - - final LatLngBounds visibleRegion = await controller.getVisibleRegion(); - final LatLng topLeft = - await controller.getLatLng(const ScreenCoordinate(x: 0, y: 0)); - final LatLng northWest = LatLng( - visibleRegion.northeast.latitude, - visibleRegion.southwest.longitude, - ); - - expect(topLeft, northWest); - }); - - testWidgets('testGetZoomLevel', (WidgetTester tester) async { - final Key key = GlobalKey(); - final Completer controllerCompleter = - Completer(); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - onMapCreated: (GoogleMapController controller) { - controllerCompleter.complete(controller); - }, - ), - )); - - final GoogleMapController controller = await controllerCompleter.future; - - await tester.pumpAndSettle(); - // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and `mapControllerCompleter.complete(controller)` above should happen - // in `mapRendered`. - // https://github.com/flutter/flutter/issues/54758 - await Future.delayed(Duration(seconds: 1)); - - double zoom = await controller.getZoomLevel(); - expect(zoom, _kInitialZoomLevel); - - await controller.moveCamera(CameraUpdate.zoomTo(7)); - await tester.pumpAndSettle(); - zoom = await controller.getZoomLevel(); - expect(zoom, equals(7)); - }); - - testWidgets('testScreenCoordinate', (WidgetTester tester) async { - final Key key = GlobalKey(); - final Completer controllerCompleter = - Completer(); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - key: key, - initialCameraPosition: _kInitialCameraPosition, - onMapCreated: (GoogleMapController controller) { - controllerCompleter.complete(controller); - }, - ), - )); - final GoogleMapController controller = await controllerCompleter.future; - - await tester.pumpAndSettle(); - // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and `mapControllerCompleter.complete(controller)` above should happen - // in `mapRendered`. - // https://github.com/flutter/flutter/issues/54758 - await Future.delayed(Duration(seconds: 1)); - - final LatLngBounds visibleRegion = await controller.getVisibleRegion(); - final LatLng northWest = LatLng( - visibleRegion.northeast.latitude, - visibleRegion.southwest.longitude, - ); - final ScreenCoordinate topLeft = - await controller.getScreenCoordinate(northWest); - expect(topLeft, const ScreenCoordinate(x: 0, y: 0)); - }); - - testWidgets('testResizeWidget', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final GoogleMap map = GoogleMap( - initialCameraPosition: _kInitialCameraPosition, - onMapCreated: (GoogleMapController controller) async { - controllerCompleter.complete(controller); - }, - ); - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: MaterialApp( - home: Scaffold( - body: SizedBox(height: 100, width: 100, child: map))))); - final GoogleMapController controller = await controllerCompleter.future; - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: MaterialApp( - home: Scaffold( - body: SizedBox(height: 400, width: 400, child: map))))); - - await tester.pumpAndSettle(); - // TODO(cyanglaz): Remove this after we added `mapRendered` callback, and `mapControllerCompleter.complete(controller)` above should happen - // in `mapRendered`. - // https://github.com/flutter/flutter/issues/54758 - await Future.delayed(Duration(seconds: 1)); - - // Simple call to make sure that the app hasn't crashed. - final LatLngBounds bounds1 = await controller.getVisibleRegion(); - final LatLngBounds bounds2 = await controller.getVisibleRegion(); - expect(bounds1, bounds2); - }); - - testWidgets('testToggleInfoWindow', (WidgetTester tester) async { - final Marker marker = Marker( - markerId: MarkerId("marker"), - infoWindow: InfoWindow(title: "InfoWindow")); - final Set markers = {marker}; - - Completer controllerCompleter = - Completer(); - - await tester.pumpWidget(Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - initialCameraPosition: const CameraPosition(target: LatLng(10.0, 15.0)), - markers: markers, - onMapCreated: (GoogleMapController googleMapController) { - controllerCompleter.complete(googleMapController); - }, - ), - )); - - GoogleMapController controller = await controllerCompleter.future; - - bool iwVisibleStatus = - await controller.isMarkerInfoWindowShown(marker.markerId); - expect(iwVisibleStatus, false); - - await controller.showMarkerInfoWindow(marker.markerId); - iwVisibleStatus = await controller.isMarkerInfoWindowShown(marker.markerId); - expect(iwVisibleStatus, true); - - await controller.hideMarkerInfoWindow(marker.markerId); - iwVisibleStatus = await controller.isMarkerInfoWindowShown(marker.markerId); - expect(iwVisibleStatus, false); - }); - - testWidgets("fromAssetImage", (WidgetTester tester) async { - double pixelRatio = 2; - final ImageConfiguration imageConfiguration = - ImageConfiguration(devicePixelRatio: pixelRatio); - final BitmapDescriptor mip = await BitmapDescriptor.fromAssetImage( - imageConfiguration, 'red_square.png'); - final BitmapDescriptor scaled = await BitmapDescriptor.fromAssetImage( - imageConfiguration, 'red_square.png', - mipmaps: false); - // 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); - }); - - testWidgets('testTakeSnapshot', (WidgetTester tester) async { - Completer inspectorCompleter = - Completer(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: GoogleMap( - initialCameraPosition: _kInitialCameraPosition, - 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; - 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); -} diff --git a/packages/google_maps_flutter/google_maps_flutter/example/test_driver/google_maps_e2e_test.dart b/packages/google_maps_flutter/google_maps_flutter/example/test_driver/google_maps_e2e_test.dart deleted file mode 100644 index f3aa9e218d82..000000000000 --- a/packages/google_maps_flutter/google_maps_flutter/example/test_driver/google_maps_e2e_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 'dart:async'; -import 'dart:io'; -import 'package:flutter_driver/flutter_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); -} 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 new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_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/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..1428de69885b 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); } @@ -479,6 +509,16 @@ - (void)mapView:(GMSMapView*)mapView didEndDraggingMarker:(GMSMarker*)marker { [_markersController onMarkerDragEnd:markerId coordinate:marker.position]; } +- (void)mapView:(GMSMapView*)mapView didStartDraggingMarker:(GMSMarker*)marker { + NSString* markerId = marker.userData[0]; + [_markersController onMarkerDragStart:markerId coordinate:marker.position]; +} + +- (void)mapView:(GMSMapView*)mapView didDragMarker:(GMSMarker*)marker { + NSString* markerId = marker.userData[0]; + [_markersController onMarkerDrag:markerId coordinate:marker.position]; +} + - (void)mapView:(GMSMapView*)mapView didTapInfoWindowOfMarker:(GMSMarker*)marker { NSString* markerId = marker.userData[0]; [_markersController onInfoWindowTap:markerId]; 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..56251872c4fb 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. @@ -45,11 +45,13 @@ NS_ASSUME_NONNULL_BEGIN - (void)changeMarkers:(NSArray*)markersToChange; - (void)removeMarkerIds:(NSArray*)markerIdsToRemove; - (BOOL)onMarkerTap:(NSString*)markerId; +- (void)onMarkerDragStart:(NSString*)markerId coordinate:(CLLocationCoordinate2D)coordinate; - (void)onMarkerDragEnd:(NSString*)markerId coordinate:(CLLocationCoordinate2D)coordinate; +- (void)onMarkerDrag:(NSString*)markerId coordinate:(CLLocationCoordinate2D)coordinate; - (void)onInfoWindowTap:(NSString*)markerId; - (void)showMarkerInfoWindow:(NSString*)markerId result:(FlutterResult)result; - (void)hideMarkerInfoWindow:(NSString*)markerId result:(FlutterResult)result; - (void)isMarkerInfoWindowShown:(NSString*)markerId result:(FlutterResult)result; @end -NS_ASSUME_NONNULL_END \ 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..51ed825fa7d7 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. @@ -295,6 +295,28 @@ - (BOOL)onMarkerTap:(NSString*)markerId { [_methodChannel invokeMethod:@"marker#onTap" arguments:@{@"markerId" : markerId}]; return controller.consumeTapEvents; } +- (void)onMarkerDragStart:(NSString*)markerId coordinate:(CLLocationCoordinate2D)coordinate { + if (!markerId) { + return; + } + FLTGoogleMapMarkerController* controller = _markerIdToController[markerId]; + if (!controller) { + return; + } + [_methodChannel invokeMethod:@"marker#onDragStart" + arguments:@{@"markerId" : markerId, @"position" : PositionToJson(coordinate)}]; +} +- (void)onMarkerDrag:(NSString*)markerId coordinate:(CLLocationCoordinate2D)coordinate { + if (!markerId) { + return; + } + FLTGoogleMapMarkerController* controller = _markerIdToController[markerId]; + if (!controller) { + return; + } + [_methodChannel invokeMethod:@"marker#onDrag" + arguments:@{@"markerId" : markerId, @"position" : PositionToJson(coordinate)}]; +} - (void)onMarkerDragEnd:(NSString*)markerId coordinate:(CLLocationCoordinate2D)coordinate { if (!markerId) { return; 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*)points; - (void)setZIndex:(int)zIndex; +- (void)setGeodesic:(BOOL)isGeodesic; @end // Defines polyline controllable by Flutter. diff --git a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolylineController.m b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolylineController.m index b701a5f3a6b5..8c70d2c161ba 100644 --- a/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolylineController.m +++ b/packages/google_maps_flutter/google_maps_flutter/ios/Classes/GoogleMapPolylineController.m @@ -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,6 +52,10 @@ - (void)setColor:(UIColor*)color { - (void)setStrokeWidth:(CGFloat)width { _polyline.strokeWidth = width; } + +- (void)setGeodesic:(BOOL)isGeodesic { + _polyline.geodesic = isGeodesic; +} @end static int ToInt(NSNumber* data) { return [FLTGoogleMapJsonConversions toInt:data]; } @@ -95,6 +99,11 @@ static void InterpretPolylineOptions(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 9a1f04d59759..35e4f3faf871 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 @@ -19,6 +19,7 @@ Downloaded by pub (not CocoaPods). s.dependency 'Flutter' 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' } + s.platform = :ios, '9.0' + # GoogleMaps does not support arm64 simulators. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' } end diff --git a/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter/lib/google_maps_flutter.dart index a17b30ca6226..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,10 +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. library google_maps_flutter; import 'dart:async'; +import 'dart:io'; import 'dart:typed_data'; import 'dart:ui'; @@ -12,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'; @@ -42,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 f5ee180ab1fd..d57ac97b1663 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,46 @@ 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.onMarkerDragStart(mapId: mapId).listen( + (MarkerDragStartEvent e) => + _googleMapState.onMarkerDragStart(e.value, e.position)); + GoogleMapsFlutterPlatform.instance.onMarkerDrag(mapId: mapId).listen( + (MarkerDragEvent e) => + _googleMapState.onMarkerDrag(e.value, e.position)); + GoogleMapsFlutterPlatform.instance.onMarkerDragEnd(mapId: mapId).listen( (MarkerDragEndEvent e) => _googleMapState.onMarkerDragEnd(e.value, e.position)); - _googleMapsFlutterPlatform.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 +104,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 +116,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 +128,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 +140,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 +152,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 +185,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 +194,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 +211,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 +227,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 +236,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 +250,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 +264,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,17 +278,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.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 ecd1a708e724..b4ffd22360e8 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. @@ -8,7 +8,35 @@ part of google_maps_flutter; /// /// Pass to [GoogleMap.onMapCreated] to receive a [GoogleMapController] when the /// map is created. -typedef void MapCreatedCallback(GoogleMapController controller); +typedef MapCreatedCallback = void Function(GoogleMapController controller); + +// This counter is used to provide a stable "constant" initialization id +// to the buildView function, so the web implementation can use it as a +// 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 _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 { @@ -16,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, @@ -29,6 +57,7 @@ class GoogleMap extends StatefulWidget { this.scrollGesturesEnabled = true, this.zoomControlsEnabled = true, this.zoomGesturesEnabled = true, + this.liteModeEnabled = false, this.tiltGesturesEnabled = true, this.myLocationEnabled = false, this.myLocationButtonEnabled = true, @@ -38,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, @@ -53,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; @@ -90,6 +120,11 @@ class GoogleMap extends StatefulWidget { /// True if the map view should respond to zoom gestures. final bool zoomGesturesEnabled; + /// True if the map view should be in lite mode. Android only. + /// + /// See https://developers.google.com/maps/documentation/android-sdk/lite#overview_of_lite_mode for more details. + final bool liteModeEnabled; + /// True if the map view should respond to tilt gestures. final bool tiltGesturesEnabled; @@ -108,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: @@ -116,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. /// @@ -189,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; @@ -199,6 +237,8 @@ class GoogleMap extends StatefulWidget { } class _GoogleMapState extends State { + final _mapId = _nextMapCreationId++; + final Completer _controller = Completer(); @@ -206,22 +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), - }; - 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(), ); } @@ -235,6 +273,13 @@ class _GoogleMapState extends State { _circles = keyByCircleId(widget.circles); } + @override + void dispose() async { + super.dispose(); + GoogleMapController controller = await _controller.future; + controller.dispose(); + } + @override void didUpdateWidget(GoogleMap oldWidget) { super.didUpdateWidget(oldWidget); @@ -243,6 +288,7 @@ class _GoogleMapState extends State { _updatePolygons(); _updatePolylines(); _updateCircles(); + _updateTileOverlays(); } void _updateOptions() async { @@ -290,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, @@ -297,110 +349,148 @@ 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 onMarkerDragStart(MarkerId markerId, LatLng position) { + assert(markerId != null); + final Marker? marker = _markers[markerId]; + if (marker == null) { + throw UnknownMapObjectIdError('marker', markerId, 'onDragStart'); + } + final ValueChanged? onDragStart = marker.onDragStart; + if (onDragStart != null) { + onDragStart(position); + } + } + + void onMarkerDrag(MarkerId markerId, LatLng position) { + assert(markerId != null); + final Marker? marker = _markers[markerId]; + if (marker == null) { + throw UnknownMapObjectIdError('marker', markerId, 'onDrag'); + } + final ValueChanged? onDrag = marker.onDrag; + if (onDrag != null) { + onDrag(position); } } void onMarkerDragEnd(MarkerId markerId, LatLng position) { assert(markerId != null); - 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.myLocationEnabled, - this.myLocationButtonEnabled, - this.padding, - this.indoorViewEnabled, - this.trafficEnabled, - this.buildingsEnabled, - }); - - 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, - 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; @@ -424,6 +514,8 @@ class _GoogleMapOptions { final bool zoomGesturesEnabled; + final bool liteModeEnabled; + final bool myLocationEnabled; final bool myLocationButtonEnabled; @@ -437,37 +529,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('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 cf4b869c1a96..61ac88a8f10d 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: 0.5.27+3 +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.11 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.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.1 + flutter_plugin_android_lifecycle: ^2.0.1 + google_maps_flutter_platform_interface: ^2.1.2 dev_dependencies: flutter_test: @@ -17,18 +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 - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.googlemaps - pluginClass: GoogleMapsPlugin - ios: - pluginClass: FLTGoogleMapsPlugin - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.16.3 <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/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 71741c57057b..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,77 +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 trackCameraPosition; + bool? liteModeEnabled; - bool myLocationEnabled; + bool? trackCameraPosition; - bool trafficEnabled; + bool? myLocationEnabled; - bool buildingsEnabled; + bool? trafficEnabled; - bool myLocationButtonEnabled; + bool? buildingsEnabled; - List padding; + bool? myLocationButtonEnabled; - Set markerIdsToRemove; + List? padding; - Set markersToAdd; + Set markerIdsToRemove = {}; - Set markersToChange; + Set markersToAdd = {}; - Set polygonIdsToRemove; + Set markersToChange = {}; - Set polygonsToAdd; + Set polygonIdsToRemove = {}; - Set polygonsToChange; + Set polygonsToAdd = {}; - Set polylineIdsToRemove; + Set polygonsToChange = {}; - Set polylinesToAdd; + Set polylineIdsToRemove = {}; - Set polylinesToChange; + Set polylinesToAdd = {}; - Set circleIdsToRemove; + Set polylinesToChange = {}; - Set circlesToAdd; + Set circleIdsToRemove = {}; - Set circlesToChange; + Set circlesToAdd = {}; + + Set circlesToChange = {}; + + Set tileOverlayIdsToRemove = {}; + + Set tileOverlaysToAdd = {}; + + Set tileOverlaysToChange = {}; Future onMethodCall(MethodCall call) { switch (call.method) { @@ -95,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(() {}); @@ -103,7 +117,7 @@ class FakePlatformGoogleMap { } } - void updateMarkers(Map markerUpdates) { + void updateMarkers(Map? markerUpdates) { if (markerUpdates == null) { return; } @@ -113,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']; @@ -163,7 +169,7 @@ class FakePlatformGoogleMap { return result; } - void updatePolygons(Map polygonUpdates) { + void updatePolygons(Map? polygonUpdates) { if (polygonUpdates == null) { return; } @@ -173,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, )); } @@ -218,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; } @@ -229,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)) @@ -243,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']; @@ -270,7 +270,7 @@ class FakePlatformGoogleMap { return result; } - void updateCircles(Map circleUpdates) { + void updateCircles(Map? circleUpdates) { if (circleUpdates == null) { return; } @@ -280,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']; @@ -317,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']; @@ -356,6 +407,9 @@ class FakePlatformGoogleMap { if (options.containsKey('zoomControlsEnabled')) { zoomControlsEnabled = options['zoomControlsEnabled']; } + if (options.containsKey('liteModeEnabled')) { + liteModeEnabled = options['liteModeEnabled']; + } if (options.containsKey('myLocationEnabled')) { myLocationEnabled = options['myLocationEnabled']; } @@ -375,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, @@ -397,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..d1ec87a4730d 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); @@ -587,4 +587,34 @@ void main() { expect(platformGoogleMap.buildingsEnabled, true); }); + + testWidgets( + 'Default Android widget is AndroidView', + (WidgetTester tester) async { + await tester.pumpWidget( + const Directionality( + textDirection: TextDirection.ltr, + child: GoogleMap( + initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + ), + ), + ); + + expect(find.byType(AndroidView), findsOneWidget); + }, + ); + + // TODO(bparrishMines): Uncomment once https://github.com/flutter/plugins/pull/4017 has landed. + // testWidgets('Use AndroidViewSurface on Android', (WidgetTester tester) async { + // await tester.pumpWidget( + // const Directionality( + // textDirection: TextDirection.ltr, + // child: GoogleMap( + // initialCameraPosition: CameraPosition(target: LatLng(10.0, 15.0)), + // ), + // ), + // ); + // + // expect(find.byType(AndroidViewSurface), findsOneWidget); + // }); } 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 new file mode 100644 index 000000000000..6b3ac906802f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter/test/map_creation_test.dart @@ -0,0 +1,295 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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:stream_transform/stream_transform.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + late TestGoogleMapsFlutterPlatform platform; + + setUp(() { + // Use a mock platform so we never need to hit the MethodChannel code. + platform = TestGoogleMapsFlutterPlatform(); + GoogleMapsFlutterPlatform.instance = platform; + }); + + testWidgets('_webOnlyMapCreationId increments with each GoogleMap widget', ( + WidgetTester tester, + ) async { + // Inject two map widgets... + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: const [ + GoogleMap( + initialCameraPosition: CameraPosition( + target: LatLng(43.362, -5.849), + ), + ), + GoogleMap( + initialCameraPosition: CameraPosition( + target: LatLng(47.649, -122.350), + ), + ), + ], + ), + ), + ); + + // Verify that each one was created with a different _webOnlyMapCreationId. + expect(platform.createdIds.length, 2); + expect(platform.createdIds[0], 0); + expect(platform.createdIds[1], 1); + }); + + testWidgets('Calls platform.dispose when GoogleMap is disposed of', ( + WidgetTester tester, + ) async { + await tester.pumpWidget(GoogleMap( + initialCameraPosition: CameraPosition( + target: LatLng(43.3608, -5.8702), + ), + )); + + // Now dispose of the map... + await tester.pumpWidget(Container()); + + expect(platform.disposed, true); + }); +} + +// 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)); + } + + @override + Future getScreenCoordinate( + LatLng latLng, { + required int mapId, + }) async { + return ScreenCoordinate(x: 0, y: 0); + } + + @override + Future getLatLng( + ScreenCoordinate screenCoordinate, { + required int mapId, + }) async { + return LatLng(0, 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 onMarkerDragStart({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onMarkerDrag({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @override + Stream onMarkerDragEnd({required int mapId}) { + return mapEventStreamController.stream.whereType(); + } + + @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(); + } +} 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 eca5c914a603..3a22dde8b659 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,72 @@ +## 2.1.3 + +* `LatLng` constructor maintains longitude precision when given within + acceptable range + +## 2.1.2 + +* Add additional marker drag events + +## 2.1.1 + +* Method `buildViewWithTextDirection` has been added to the platform interface. + +## 2.1.0 + +* Add support for Hybrid Composition when building the Google Maps widget on Android. Set + `MethodChannelGoogleMapsFlutter.useAndroidViewSurface` to `true` to build with Hybrid Composition. + +## 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`. + +## 1.0.3 + +* Pass icon width/height if present on `fromAssetImage` BitmapDescriptors (web only) + ## 1.0.2 * Update lower bound of dart dependency to 2.1.0. 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 8940a4be1b58..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,27 +1,25 @@ -// 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. +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/google_maps_flutter/google_maps_flutter_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..300700071102 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,7 +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. +export 'src/method_channel/method_channel_google_maps_flutter.dart' + show MethodChannelGoogleMapsFlutter; export 'src/platform_interface/google_maps_flutter_platform.dart'; export 'src/types/types.dart'; export 'src/events/map_event.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/events/map_event.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/events/map_event.dart index c462b4b182e2..614cbe8e29fb 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. @@ -102,6 +102,26 @@ class InfoWindowTapEvent extends MapEvent { InfoWindowTapEvent(int mapId, MarkerId markerId) : super(mapId, markerId); } +/// An event fired when a [Marker] is starting to be dragged to a new [LatLng]. +class MarkerDragStartEvent extends _PositionedMapEvent { + /// Build a MarkerDragStart Event triggered from the map represented by `mapId`. + /// + /// The `position` on this event is the [LatLng] on which the Marker was picked up from. + /// The `value` of this event is a [MarkerId] object that represents the Marker. + MarkerDragStartEvent(int mapId, LatLng position, MarkerId markerId) + : super(mapId, position, markerId); +} + +/// An event fired when a [Marker] is being dragged to a new [LatLng]. +class MarkerDragEvent extends _PositionedMapEvent { + /// Build a MarkerDrag Event triggered from the map represented by `mapId`. + /// + /// The `position` on this event is the [LatLng] on which the Marker was dragged to. + /// The `value` of this event is a [MarkerId] object that represents the Marker. + MarkerDragEvent(int mapId, LatLng position, MarkerId markerId) + : super(mapId, position, markerId); +} + /// An event fired when a [Marker] is dragged to a new [LatLng]. class MarkerDragEndEvent extends _PositionedMapEvent { /// Build a MarkerDragEnd Event triggered from the map represented by `mapId`. diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/method_channel/method_channel_google_maps_flutter.dart index edbc51ab5afd..99f4fddaccd3 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,41 @@ -// 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/rendering.dart'; +import 'package:flutter/services.dart'; import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; import 'package:stream_transform/stream_transform.dart'; +import '../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,24 +53,40 @@ 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'); } + @override + void dispose({required int mapId}) { + // Noop! + } + // The controller we need to broadcast the different events coming // from handleMethodCall. // @@ -61,57 +100,67 @@ 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 onMarkerDragStart({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onMarkerDrag({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onMarkerDragEnd({required int mapId}) { return _events(mapId).whereType(); } @override - Stream onPolylineTap({@required int mapId}) { + 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(); } @@ -123,7 +172,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': @@ -135,10 +184,24 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { MarkerId(call.arguments['markerId']), )); break; + case 'marker#onDragStart': + _mapEventStreamController.add(MarkerDragStartEvent( + mapId, + LatLng.fromJson(call.arguments['position'])!, + MarkerId(call.arguments['markerId']), + )); + break; + case 'marker#onDrag': + _mapEventStreamController.add(MarkerDragEvent( + mapId, + LatLng.fromJson(call.arguments['position'])!, + MarkerId(call.arguments['markerId']), + )); + break; case 'marker#onDragEnd': _mapEventStreamController.add(MarkerDragEndEvent( mapId, - LatLng.fromJson(call.arguments['position']), + LatLng.fromJson(call.arguments['position'])!, MarkerId(call.arguments['markerId']), )); break; @@ -169,30 +232,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( @@ -203,16 +276,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( @@ -221,16 +288,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( @@ -239,16 +300,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( @@ -257,16 +312,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( @@ -275,193 +324,231 @@ 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. + /// Set [GoogleMapsFlutterPlatform] to use [AndroidViewSurface] to build the Google Maps widget. + /// + /// This implementation uses hybrid composition to render the Google Maps + /// Widget on Android. This comes at the cost of some performance on Android + /// versions below 10. See + /// https://flutter.dev/docs/development/platform-integration/platform-views#performance for more + /// information. + /// + /// If set to true, the google map widget should be built with + /// [buildViewWithTextDirection] instead of [buildView]. + /// + /// Defaults to false. + bool useAndroidViewSurface = false; + @override - Widget buildView( - Map creationParams, - Set> gestureRecognizers, - PlatformViewCreatedCallback onPlatformViewCreated) { + Widget buildViewWithTextDirection( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required CameraPosition initialCameraPosition, + required TextDirection textDirection, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set circles = const {}, + Set tileOverlays = const {}, + Set>? gestureRecognizers, + Map mapOptions = const {}, + }) { + 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', - onPlatformViewCreated: onPlatformViewCreated, - gestureRecognizers: gestureRecognizers, - creationParams: creationParams, - creationParamsCodec: const StandardMessageCodec(), - ); + if (useAndroidViewSurface) { + return PlatformViewLink( + viewType: 'plugins.flutter.io/google_maps', + surfaceFactory: ( + BuildContext context, + PlatformViewController controller, + ) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: gestureRecognizers ?? + const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + final SurfaceAndroidViewController controller = + PlatformViewsService.initSurfaceAndroidView( + id: params.id, + viewType: 'plugins.flutter.io/google_maps', + layoutDirection: textDirection, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + onFocus: () => params.onFocusChanged(true), + ); + controller.addOnPlatformViewCreatedListener( + params.onPlatformViewCreated, + ); + controller.addOnPlatformViewCreatedListener( + onPlatformViewCreated, + ); + + controller.create(); + return controller; + }, + ); + } else { + return AndroidView( + viewType: 'plugins.flutter.io/google_maps', + onPlatformViewCreated: onPlatformViewCreated, + gestureRecognizers: gestureRecognizers, + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + ); + } } else if (defaultTargetPlatform == TargetPlatform.iOS) { return UiKitView( viewType: 'plugins.flutter.io/google_maps', @@ -471,7 +558,36 @@ class MethodChannelGoogleMapsFlutter extends GoogleMapsFlutterPlatform { creationParamsCodec: const StandardMessageCodec(), ); } + return Text( '$defaultTargetPlatform is not yet supported by the maps plugin'); } + + @override + Widget buildView( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required CameraPosition initialCameraPosition, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set circles = const {}, + Set tileOverlays = const {}, + Set>? gestureRecognizers, + Map mapOptions = const {}, + }) { + return buildViewWithTextDirection( + creationId, + onPlatformViewCreated, + initialCameraPosition: initialCameraPosition, + textDirection: TextDirection.ltr, + markers: markers, + polygons: polygons, + polylines: polylines, + circles: circles, + tileOverlays: tileOverlays, + gestureRecognizers: gestureRecognizers, + mapOptions: mapOptions, + ); + } } 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 b89d3420c68e..08b4872ad5dd 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,65 +279,127 @@ 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 onMarkerDragStart({required int mapId}) { + throw UnimplementedError('onMarkerDragEnd() has not been implemented.'); + } + + /// A [Marker] has been dragged to a different [LatLng] position. + Stream onMarkerDrag({required int mapId}) { + throw UnimplementedError('onMarkerDragEnd() has not been implemented.'); + } + + /// A [Marker] has been dragged to a different [LatLng] position. + Stream onMarkerDragEnd({required int mapId}) { throw UnimplementedError('onMarkerDragEnd() has not been implemented.'); } /// 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.'); } - /// Returns a widget displaying the map view + /// Dispose of whatever resources the `mapId` is holding on to. + 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.'); } + + /// Returns a widget displaying the map view. + /// + /// This method is similar to [buildView], but contains a parameter for + /// platforms that require a text direction. + /// + /// Default behavior passes all parameters except `textDirection` to + /// [buildView]. This is for backward compatibility with existing + /// implementations. Platforms that use the text direction should override + /// this as the primary implementation, and delegate to it from buildView. + Widget buildViewWithTextDirection( + int creationId, + PlatformViewCreatedCallback onPlatformViewCreated, { + required CameraPosition initialCameraPosition, + required TextDirection textDirection, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set circles = const {}, + Set tileOverlays = const {}, + Set>? gestureRecognizers, + Map mapOptions = const {}, + }) { + return buildView( + creationId, + onPlatformViewCreated, + initialCameraPosition: initialCameraPosition, + markers: markers, + polygons: polygons, + polylines: polylines, + circles: circles, + tileOverlays: tileOverlays, + gestureRecognizers: gestureRecognizers, + mapOptions: mapOptions, + ); + } } 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 40581b43e065..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,20 +1,35 @@ -// 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; import 'package:flutter/services.dart' show AssetBundle; +import 'package:flutter/foundation.dart' show kIsWeb; + /// Defines a bitmap image. For a marker, this class can be used to set the /// image of the marker icon. For a ground overlay, it can be used to set the /// image to place on the surface of the earth. class BitmapDescriptor { const BitmapDescriptor._(this._json); + 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; @@ -47,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. @@ -81,36 +82,88 @@ 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 && size != null) + [ + size.width, + size.height, + ], ]); } /// 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..7a1aaf051388 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. @@ -14,13 +14,16 @@ class LatLng { /// The latitude is clamped to the inclusive interval from -90.0 to +90.0. /// /// The longitude is normalized to the half-open interval from -180.0 - /// (inclusive) to +180.0 (exclusive) + /// (inclusive) to +180.0 (exclusive). const LatLng(double latitude, double longitude) : assert(latitude != null), assert(longitude != null), latitude = (latitude < -90.0 ? -90.0 : (90.0 < latitude ? 90.0 : latitude)), - longitude = (longitude + 180.0) % 360.0 - 180.0; + // Avoids normalization if possible to prevent unnecessary loss of precision + longitude = longitude >= -180 && longitude < 180 + ? longitude + : (longitude + 180.0) % 360.0 - 180.0; /// The latitude in degrees between -90.0 and 90.0, both inclusive. final double latitude; @@ -29,16 +32,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 +71,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 +83,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 +107,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..52255f84f4cc 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, @@ -169,12 +147,17 @@ class Marker { this.visible = true, this.zIndex = 0.0, this.onTap, + this.onDrag, + this.onDragStart, this.onDragEnd, }) : assert(alpha == null || (0.0 <= alpha && alpha <= 1.0)); /// 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 +207,35 @@ 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 start of a drag event. + final ValueChanged? onDragStart; /// Signature reporting the new [LatLng] at the end of a drag event. - final ValueChanged onDragEnd; + final ValueChanged? onDragEnd; + + /// Signature reporting the new [LatLng] during the drag event. + final ValueChanged? onDrag; /// Creates a new [Marker] object whose values are the same as this instance, /// unless overwritten by the specified parameters. Marker copyWith({ - 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? onDragStartParam, + ValueChanged? onDragParam, + ValueChanged? onDragEndParam, }) { return Marker( markerId: markerId, @@ -260,6 +251,8 @@ class Marker { visible: visibleParam ?? visible, zIndex: zIndexParam ?? zIndex, onTap: onTapParam ?? onTap, + onDragStart: onDragStartParam ?? onDragStart, + onDrag: onDragParam ?? onDrag, onDragEnd: onDragEndParam ?? onDragEnd, ); } @@ -268,10 +261,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 +276,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 +289,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 && @@ -319,6 +312,7 @@ class Marker { return 'Marker{markerId: $markerId, alpha: $alpha, anchor: $anchor, ' 'consumeTapEvents: $consumeTapEvents, draggable: $draggable, flat: $flat, ' 'icon: $icon, infoWindow: $infoWindow, position: $position, rotation: $rotation, ' - 'visible: $visible, zIndex: $zIndex, onTap: $onTap}'; + 'visible: $visible, zIndex: $zIndex, onTap: $onTap, onDragStart: $onDragStart, ' + 'onDrag: $onDrag, onDragEnd: $onDragEnd}'; } } diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/lib/src/types/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 b28b7f47652d..d3d7653b746e 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,26 @@ 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.2 +version: 2.1.3 + +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: + async: ^2.5.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" + 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..176f702ff0ff --- /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,126 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:google_maps_flutter_platform_interface/src/events/map_event.dart'; +import 'package:google_maps_flutter_platform_interface/src/method_channel/method_channel_google_maps_flutter.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'dart:async'; + +import 'package:async/async.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + 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); + }); + } + + Future sendPlatformMessage( + int mapId, String method, Map data) async { + final ByteData byteData = const StandardMethodCodec() + .encodeMethodCall(MethodCall(method, data)); + await TestDefaultBinaryMessengerBinding.instance!.defaultBinaryMessenger + .handlePlatformMessage( + "plugins.flutter.io/google_maps_$mapId", byteData, (data) {}); + } + + // 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', + ]); + }); + test('markers send drag event to correct streams', () async { + const int mapId = 1; + final jsonMarkerDragStartEvent = { + "mapId": mapId, + "markerId": "drag-start-marker", + "position": [1.0, 1.0] + }; + final jsonMarkerDragEvent = { + "mapId": mapId, + "markerId": "drag-marker", + "position": [1.0, 1.0] + }; + final jsonMarkerDragEndEvent = { + "mapId": mapId, + "markerId": "drag-end-marker", + "position": [1.0, 1.0] + }; + + final MethodChannelGoogleMapsFlutter maps = + MethodChannelGoogleMapsFlutter(); + maps.ensureChannelInitialized(mapId); + + final StreamQueue markerDragStartStream = + StreamQueue(maps.onMarkerDragStart(mapId: mapId)); + final StreamQueue markerDragStream = + StreamQueue(maps.onMarkerDrag(mapId: mapId)); + final StreamQueue markerDragEndStream = + StreamQueue(maps.onMarkerDragEnd(mapId: mapId)); + + await sendPlatformMessage( + mapId, "marker#onDragStart", jsonMarkerDragStartEvent); + await sendPlatformMessage(mapId, "marker#onDrag", jsonMarkerDragEvent); + await sendPlatformMessage( + mapId, "marker#onDragEnd", jsonMarkerDragEndEvent); + + expect((await markerDragStartStream.next).value.value, + equals("drag-start-marker")); + expect((await markerDragStream.next).value.value, equals("drag-marker")); + expect((await markerDragEndStream.next).value.value, + equals("drag-end-marker")); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/platform_interface/google_maps_flutter_platform_test.dart index a003b94d544c..c381f9e30750 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,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 'package:mockito/mockito.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; +import 'package:mockito/mockito.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; @@ -13,10 +16,13 @@ import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platf void main() { TestWidgetsFlutterBinding.ensureInitialized(); + // Store the initial instance before any tests change it. + final GoogleMapsFlutterPlatform initialInstance = + GoogleMapsFlutterPlatform.instance; + group('$GoogleMapsFlutterPlatform', () { test('$MethodChannelGoogleMapsFlutter() is the default instance', () { - expect(GoogleMapsFlutterPlatform.instance, - isInstanceOf()); + expect(initialInstance, isInstanceOf()); }); test('Cannot be implemented with `implements`', () { @@ -35,29 +41,23 @@ void main() { test('Can be extended', () { 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, - [], - ); - }); + test( + 'default implementation of `buildViewWithTextDirection` delegates to `buildView`', + () { + final GoogleMapsFlutterPlatform platform = + BuildViewGoogleMapsFlutterPlatform(); + expect( + platform.buildViewWithTextDirection( + 0, + (_) {}, + initialCameraPosition: CameraPosition(target: LatLng(0.0, 0.0)), + textDirection: TextDirection.ltr, + ), + isA(), + ); + }, + ); }); } @@ -69,3 +69,22 @@ class ImplementsGoogleMapsFlutterPlatform extends Mock implements GoogleMapsFlutterPlatform {} class ExtendsGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform {} + +class BuildViewGoogleMapsFlutterPlatform extends GoogleMapsFlutterPlatform { + @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 {}, + }) { + return const Text(''); + } +} 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/location_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/location_test.dart new file mode 100644 index 000000000000..80f696177dfd --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/location_test.dart @@ -0,0 +1,62 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('LanLng constructor', () { + test('Maintains longitude precision if within acceptable range', () async { + const lat = -34.509981; + const lng = 150.792384; + + final latLng = LatLng(lat, lng); + + expect(latLng.latitude, equals(lat)); + expect(latLng.longitude, equals(lng)); + }); + + test('Normalizes longitude that is below lower limit', () async { + const lat = -34.509981; + const lng = -270.0; + + final latLng = LatLng(lat, lng); + + expect(latLng.latitude, equals(lat)); + expect(latLng.longitude, equals(90.0)); + }); + + test('Normalizes longitude that is above upper limit', () async { + const lat = -34.509981; + const lng = 270.0; + + final latLng = LatLng(lat, lng); + + expect(latLng.latitude, equals(lat)); + expect(latLng.longitude, equals(-90.0)); + }); + + test('Includes longitude set to lower limit', () async { + const lat = -34.509981; + const lng = -180.0; + + final latLng = LatLng(lat, lng); + + expect(latLng.latitude, equals(lat)); + expect(latLng.longitude, equals(-180.0)); + }); + + test('Normalizes longitude set to upper limit', () async { + const lat = -34.509981; + const lng = 180.0; + + final latLng = LatLng(lat, lng); + + expect(latLng.latitude, equals(lat)); + expect(latLng.longitude, equals(-180.0)); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/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/marker_test.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/marker_test.dart new file mode 100644 index 000000000000..c8f6fa527a95 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/marker_test.dart @@ -0,0 +1,167 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/cupertino.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('$Marker', () { + test('constructor defaults', () { + final Marker marker = Marker(markerId: MarkerId("ABC123")); + + expect(marker.alpha, equals(1.0)); + expect(marker.anchor, equals(const Offset(0.5, 1.0))); + expect(marker.consumeTapEvents, equals(false)); + expect(marker.draggable, equals(false)); + expect(marker.flat, equals(false)); + expect(marker.icon, equals(BitmapDescriptor.defaultMarker)); + expect(marker.infoWindow, equals(InfoWindow.noText)); + expect(marker.position, equals(const LatLng(0.0, 0.0))); + expect(marker.rotation, equals(0.0)); + expect(marker.visible, equals(true)); + expect(marker.zIndex, equals(0.0)); + expect(marker.onTap, equals(null)); + expect(marker.onDrag, equals(null)); + expect(marker.onDragStart, equals(null)); + expect(marker.onDragEnd, equals(null)); + }); + test('constructor alpha is >= 0.0 and <= 1.0', () { + final ValueSetter initWithAlpha = (double alpha) { + Marker(markerId: MarkerId("ABC123"), alpha: alpha); + }; + expect(() => initWithAlpha(-0.5), throwsAssertionError); + expect(() => initWithAlpha(0.0), isNot(throwsAssertionError)); + expect(() => initWithAlpha(0.5), isNot(throwsAssertionError)); + expect(() => initWithAlpha(1.0), isNot(throwsAssertionError)); + expect(() => initWithAlpha(100), throwsAssertionError); + }); + + test('toJson', () { + final BitmapDescriptor testDescriptor = + BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueCyan); + final Marker marker = Marker( + markerId: MarkerId("ABC123"), + alpha: 0.12345, + anchor: Offset(100, 100), + consumeTapEvents: true, + draggable: true, + flat: true, + icon: testDescriptor, + infoWindow: InfoWindow( + title: "Test title", + snippet: "Test snippet", + anchor: Offset(100, 200), + ), + position: LatLng(50, 50), + rotation: 100, + visible: false, + zIndex: 100, + onTap: () {}, + onDragStart: (LatLng latLng) {}, + onDrag: (LatLng latLng) {}, + onDragEnd: (LatLng latLng) {}, + ); + + final Map json = marker.toJson() as Map; + + expect(json, { + 'markerId': "ABC123", + 'alpha': 0.12345, + 'anchor': [100, 100], + 'consumeTapEvents': true, + 'draggable': true, + 'flat': true, + 'icon': testDescriptor.toJson(), + 'infoWindow': { + 'title': "Test title", + 'snippet': "Test snippet", + 'anchor': [100.0, 200.0], + }, + 'position': [50, 50], + 'rotation': 100.0, + 'visible': false, + 'zIndex': 100.0, + }); + }); + test('clone', () { + final Marker marker = Marker(markerId: MarkerId("ABC123")); + final Marker clone = marker.clone(); + + expect(identical(clone, marker), isFalse); + expect(clone, equals(marker)); + }); + test('copyWith', () { + final Marker marker = Marker(markerId: MarkerId("ABC123")); + + final BitmapDescriptor testDescriptor = + BitmapDescriptor.defaultMarkerWithHue(BitmapDescriptor.hueCyan); + final double testAlphaParam = 0.12345; + final Offset testAnchorParam = Offset(100, 100); + final bool testConsumeTapEventsParam = !marker.consumeTapEvents; + final bool testDraggableParam = !marker.draggable; + final bool testFlatParam = !marker.flat; + final BitmapDescriptor testIconParam = testDescriptor; + final InfoWindow testInfoWindowParam = InfoWindow(title: "Test"); + final LatLng testPositionParam = LatLng(100, 100); + final double testRotationParam = 100; + final bool testVisibleParam = !marker.visible; + final double testZIndexParam = 100; + final List log = []; + + final copy = marker.copyWith( + alphaParam: testAlphaParam, + anchorParam: testAnchorParam, + consumeTapEventsParam: testConsumeTapEventsParam, + draggableParam: testDraggableParam, + flatParam: testFlatParam, + iconParam: testIconParam, + infoWindowParam: testInfoWindowParam, + positionParam: testPositionParam, + rotationParam: testRotationParam, + visibleParam: testVisibleParam, + zIndexParam: testZIndexParam, + onTapParam: () { + log.add("onTapParam"); + }, + onDragStartParam: (LatLng latLng) { + log.add("onDragStartParam"); + }, + onDragParam: (LatLng latLng) { + log.add("onDragParam"); + }, + onDragEndParam: (LatLng latLng) { + log.add("onDragEndParam"); + }, + ); + + expect(copy.alpha, equals(testAlphaParam)); + expect(copy.anchor, equals(testAnchorParam)); + expect(copy.consumeTapEvents, equals(testConsumeTapEventsParam)); + expect(copy.draggable, equals(testDraggableParam)); + expect(copy.flat, equals(testFlatParam)); + expect(copy.icon, equals(testIconParam)); + expect(copy.infoWindow, equals(testInfoWindowParam)); + expect(copy.position, equals(testPositionParam)); + expect(copy.rotation, equals(testRotationParam)); + expect(copy.visible, equals(testVisibleParam)); + expect(copy.zIndex, equals(testZIndexParam)); + + copy.onTap!(); + expect(log, contains("onTapParam")); + + copy.onDragStart!(LatLng(0, 1)); + expect(log, contains("onDragStartParam")); + + copy.onDrag!(LatLng(0, 1)); + expect(log, contains("onDragParam")); + + copy.onDragEnd!(LatLng(0, 1)); + expect(log, contains("onDragEndParam")); + }); + }); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/test_maps_object.dart b/packages/google_maps_flutter/google_maps_flutter_platform_interface/test/types/test_maps_object.dart 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 new file mode 100644 index 000000000000..b2fe086f5c9d --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/CHANGELOG.md @@ -0,0 +1,105 @@ +## 0.3.2 + +Add `onDragStart` and `onDrag` to `Marker` + +## 0.3.1 + +* Fix the `getScreenCoordinate(LatLng)` method. [#80710](https://github.com/flutter/flutter/issues/80710) +* Wait until the map tiles have loaded before calling `onPlatformViewCreated`, so +the returned controller is 100% functional (has bounds, a projection, etc...) +* Use zIndex property when initializing Circle objects. [#89374](https://github.com/flutter/flutter/issues/89374) + +## 0.3.0+4 + +* Add `implements` to pubspec. + +## 0.3.0+3 + +* Update the `README.md` usage instructions to not be tied to explicit package versions. + +## 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). + +## 0.1.0+1 + +* Port e2e tests to use the new integration_test package. + +## 0.1.0 + +* First open-source version diff --git a/packages/google_maps_flutter/google_maps_flutter_web/LICENSE b/packages/google_maps_flutter/google_maps_flutter_web/LICENSE new file mode 100644 index 000000000000..8f8c01d50118 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/LICENSE @@ -0,0 +1,51 @@ +google_maps_flutter_web + +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. +-------------------------------------------------------------------------------- +to_screen_location + +The MIT License (MIT) + +Copyright (c) 2008 Krasimir Tsonev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/README.md b/packages/google_maps_flutter/google_maps_flutter_web/README.md new file mode 100644 index 000000000000..9e7ce94e3e59 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/README.md @@ -0,0 +1,48 @@ +# google_maps_flutter_web + +This is an implementation of the [google_maps_flutter](https://pub.dev/packages/google_maps_flutter) plugin for web. Behind the scenes, it uses a14n's [google_maps](https://pub.dev/packages/google_maps) dart JS interop layer. + +## Usage + +### Depend on the package + +This package is not an endorsed implementation of the google_maps_flutter plugin yet, so you'll need to +[add it explicitly](https://pub.dev/packages/google_maps_flutter_web/install). + +### Modify web/index.html + +Get an API Key for Google Maps JavaScript API. Get started [here](https://developers.google.com/maps/documentation/javascript/get-api-key). + +Modify the `` tag of your `web/index.html` to load the Google Maps JavaScript API, like so: + +```html + + + + + + +``` + +Now you should be able to use the Google Maps plugin normally. + +## Limitations of the web version + +The following map options are not available in web, because the map doesn't rotate there: + +* `compassEnabled` +* `rotateGesturesEnabled` +* `tiltGesturesEnabled` + +There's no "Map Toolbar" in web, so the `mapToolbarEnabled` option is unused. + +There's no "My Location" widget in web ([tracking issue](https://github.com/flutter/flutter/issues/64073)), so the following options are ignored, for now: + +* `myLocationButtonEnabled` +* `myLocationEnabled` + +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/example/README.md b/packages/google_maps_flutter/google_maps_flutter_web/example/README.md new file mode 100644 index 000000000000..3cdecfab2ab9 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/README.md @@ -0,0 +1,12 @@ +# Testing + +This package uses `package:integration_test` to run its tests in a web browser. + +See [Plugin Tests > Web Tests](https://github.com/flutter/flutter/wiki/Plugin-Tests#web-tests) +in the Flutter wiki for instructions to setup and run the tests in this package. + +Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) +for more info. + +See [Plugin Tests > Web Tests > Mocks](https://github.com/flutter/flutter/wiki/Plugin-Tests#mocks) +in the Flutter wiki for more information about the `.mocks.dart` files in this package. \ No newline at end of file diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/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..39aa641b10e4 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.dart @@ -0,0 +1,672 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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'), + zIndex: 1234, + ), + }, 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(capturedCircles.first.zIndex, 1234); + 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..530707c6c328 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_controller_test.mocks.dart @@ -0,0 +1,206 @@ +// Mocks generated by Mockito 5.0.16 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/google_maps.dart' as _i2; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart' + as _i4; +import 'package:google_maps_flutter_web/google_maps_flutter_web.dart' as _i3; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types + +class _FakeGMap_0 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_0()) as _i2.GMap); + @override + set googleMap(_i2.GMap? _googleMap) => + super.noSuchMethod(Invocation.setter(#googleMap, _googleMap), + returnValueForMissingStub: null); + @override + int get mapId => + (super.noSuchMethod(Invocation.getter(#mapId), returnValue: 0) as int); + @override + set mapId(int? _mapId) => + super.noSuchMethod(Invocation.setter(#mapId, _mapId), + returnValueForMissingStub: null); + @override + void addCircles(Set<_i4.Circle>? circlesToAdd) => + super.noSuchMethod(Invocation.method(#addCircles, [circlesToAdd]), + returnValueForMissingStub: null); + @override + void changeCircles(Set<_i4.Circle>? circlesToChange) => + super.noSuchMethod(Invocation.method(#changeCircles, [circlesToChange]), + returnValueForMissingStub: null); + @override + void removeCircles(Set<_i4.CircleId>? circleIdsToRemove) => + super.noSuchMethod(Invocation.method(#removeCircles, [circleIdsToRemove]), + returnValueForMissingStub: null); + @override + void bindToMap(int? mapId, _i2.GMap? googleMap) => + super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// 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<_i4.PolygonId, _i3.PolygonController> get polygons => + (super.noSuchMethod(Invocation.getter(#polygons), + returnValue: <_i4.PolygonId, _i3.PolygonController>{}) + as Map<_i4.PolygonId, _i3.PolygonController>); + @override + _i2.GMap get googleMap => (super.noSuchMethod(Invocation.getter(#googleMap), + returnValue: _FakeGMap_0()) as _i2.GMap); + @override + set googleMap(_i2.GMap? _googleMap) => + super.noSuchMethod(Invocation.setter(#googleMap, _googleMap), + returnValueForMissingStub: null); + @override + int get mapId => + (super.noSuchMethod(Invocation.getter(#mapId), returnValue: 0) as int); + @override + set mapId(int? _mapId) => + super.noSuchMethod(Invocation.setter(#mapId, _mapId), + returnValueForMissingStub: null); + @override + void addPolygons(Set<_i4.Polygon>? polygonsToAdd) => + super.noSuchMethod(Invocation.method(#addPolygons, [polygonsToAdd]), + returnValueForMissingStub: null); + @override + void changePolygons(Set<_i4.Polygon>? polygonsToChange) => + super.noSuchMethod(Invocation.method(#changePolygons, [polygonsToChange]), + returnValueForMissingStub: null); + @override + void removePolygons(Set<_i4.PolygonId>? polygonIdsToRemove) => super + .noSuchMethod(Invocation.method(#removePolygons, [polygonIdsToRemove]), + returnValueForMissingStub: null); + @override + void bindToMap(int? mapId, _i2.GMap? googleMap) => + super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// 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<_i4.PolylineId, _i3.PolylineController> get lines => + (super.noSuchMethod(Invocation.getter(#lines), + returnValue: <_i4.PolylineId, _i3.PolylineController>{}) + as Map<_i4.PolylineId, _i3.PolylineController>); + @override + _i2.GMap get googleMap => (super.noSuchMethod(Invocation.getter(#googleMap), + returnValue: _FakeGMap_0()) as _i2.GMap); + @override + set googleMap(_i2.GMap? _googleMap) => + super.noSuchMethod(Invocation.setter(#googleMap, _googleMap), + returnValueForMissingStub: null); + @override + int get mapId => + (super.noSuchMethod(Invocation.getter(#mapId), returnValue: 0) as int); + @override + set mapId(int? _mapId) => + super.noSuchMethod(Invocation.setter(#mapId, _mapId), + returnValueForMissingStub: null); + @override + void addPolylines(Set<_i4.Polyline>? polylinesToAdd) => + super.noSuchMethod(Invocation.method(#addPolylines, [polylinesToAdd]), + returnValueForMissingStub: null); + @override + void changePolylines(Set<_i4.Polyline>? polylinesToChange) => super + .noSuchMethod(Invocation.method(#changePolylines, [polylinesToChange]), + returnValueForMissingStub: null); + @override + void removePolylines(Set<_i4.PolylineId>? polylineIdsToRemove) => super + .noSuchMethod(Invocation.method(#removePolylines, [polylineIdsToRemove]), + returnValueForMissingStub: null); + @override + void bindToMap(int? mapId, _i2.GMap? googleMap) => + super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} + +/// 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<_i4.MarkerId, _i3.MarkerController> get markers => + (super.noSuchMethod(Invocation.getter(#markers), + returnValue: <_i4.MarkerId, _i3.MarkerController>{}) + as Map<_i4.MarkerId, _i3.MarkerController>); + @override + _i2.GMap get googleMap => (super.noSuchMethod(Invocation.getter(#googleMap), + returnValue: _FakeGMap_0()) as _i2.GMap); + @override + set googleMap(_i2.GMap? _googleMap) => + super.noSuchMethod(Invocation.setter(#googleMap, _googleMap), + returnValueForMissingStub: null); + @override + int get mapId => + (super.noSuchMethod(Invocation.getter(#mapId), returnValue: 0) as int); + @override + set mapId(int? _mapId) => + super.noSuchMethod(Invocation.setter(#mapId, _mapId), + returnValueForMissingStub: null); + @override + void addMarkers(Set<_i4.Marker>? markersToAdd) => + super.noSuchMethod(Invocation.method(#addMarkers, [markersToAdd]), + returnValueForMissingStub: null); + @override + void changeMarkers(Set<_i4.Marker>? markersToChange) => + super.noSuchMethod(Invocation.method(#changeMarkers, [markersToChange]), + returnValueForMissingStub: null); + @override + void removeMarkers(Set<_i4.MarkerId>? markerIdsToRemove) => + super.noSuchMethod(Invocation.method(#removeMarkers, [markerIdsToRemove]), + returnValueForMissingStub: null); + @override + void showMarkerInfoWindow(_i4.MarkerId? markerId) => + super.noSuchMethod(Invocation.method(#showMarkerInfoWindow, [markerId]), + returnValueForMissingStub: null); + @override + void hideMarkerInfoWindow(_i4.MarkerId? markerId) => + super.noSuchMethod(Invocation.method(#hideMarkerInfoWindow, [markerId]), + returnValueForMissingStub: null); + @override + bool isInfoWindowShown(_i4.MarkerId? markerId) => + (super.noSuchMethod(Invocation.method(#isInfoWindowShown, [markerId]), + returnValue: false) as bool); + @override + void bindToMap(int? mapId, _i2.GMap? googleMap) => + super.noSuchMethod(Invocation.method(#bindToMap, [mapId, googleMap]), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} 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..a3cf86e593fe --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.dart @@ -0,0 +1,483 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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; + late Completer reportedMapIdCompleter; + int numberOnPlatformViewCreatedCalls = 0; + + void onPlatformViewCreated(int id) { + reportedMapIdCompleter.complete(id); + numberOnPlatformViewCreatedCalls++; + } + + setUp(() { + controller = MockGoogleMapController(); + plugin = GoogleMapsPlugin(); + reportedMapIdCompleter = Completer(); + }); + + 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('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(cache, contains(testMapId)); + expect( + cache[testMapId], + isNotNull, + reason: 'cached controller cannot be null.', + ); + expect( + cache[testMapId]!.isInitialized, + isTrue, + reason: 'buildView calls init on the controller', + ); + }); + + 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)); + }); + + testWidgets( + 'asynchronously reports onPlatformViewCreated the first time it happens', + (WidgetTester tester) async { + final Map cache = {}; + plugin.debugSetMapById(cache); + + plugin.buildView( + testMapId, + onPlatformViewCreated, + initialCameraPosition: initialCameraPosition, + ); + + // Simulate Google Maps JS SDK being "ready" + cache[testMapId]!.stream.add(WebMapReadyEvent(testMapId)); + + expect( + cache[testMapId]!.isInitialized, + isTrue, + reason: 'buildView calls init on the controller', + ); + expect( + await reportedMapIdCompleter.future, + testMapId, + reason: 'Should call onPlatformViewCreated with the mapId', + ); + + // Fire repeated event again... + cache[testMapId]!.stream.add(WebMapReadyEvent(testMapId)); + expect( + numberOnPlatformViewCreatedCalls, + equals(1), + reason: + 'Should not call onPlatformViewCreated for the same controller multiple times', + ); + }); + }); + + 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('onMarkerDragStart', (WidgetTester tester) async { + final event = MarkerDragStartEvent( + mapId, + LatLng(43.3677, -5.8372), + MarkerId('test-123'), + ); + + final stream = plugin.onMarkerDragStart(mapId: mapId); + + await _testStreamFiltering(stream, event); + }); + testWidgets('onMarkerDrag', (WidgetTester tester) async { + final event = MarkerDragEvent( + mapId, + LatLng(43.3677, -5.8372), + MarkerId('test-123'), + ); + + final stream = plugin.onMarkerDrag(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..d2df11c6ffa9 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/google_maps_plugin_test.mocks.dart @@ -0,0 +1,131 @@ +// Mocks generated by Mockito 5.0.16 from annotations +// in google_maps_flutter_web_integration_tests/integration_test/google_maps_plugin_test.dart. +// Do not manually edit this file. + +import 'dart:async' as _i2; + +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart' + as _i3; +import 'package:google_maps_flutter_web/google_maps_flutter_web.dart' as _i4; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types + +class _FakeStreamController_0 extends _i1.Fake + implements _i2.StreamController {} + +class _FakeLatLngBounds_1 extends _i1.Fake implements _i3.LatLngBounds {} + +class _FakeScreenCoordinate_2 extends _i1.Fake implements _i3.ScreenCoordinate { +} + +class _FakeLatLng_3 extends _i1.Fake implements _i3.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 + _i2.StreamController<_i3.MapEvent> get stream => + (super.noSuchMethod(Invocation.getter(#stream), + returnValue: _FakeStreamController_0<_i3.MapEvent>()) + as _i2.StreamController<_i3.MapEvent>); + @override + _i2.Stream<_i3.MapEvent> get events => + (super.noSuchMethod(Invocation.getter(#events), + returnValue: Stream<_i3.MapEvent>.empty()) + as _i2.Stream<_i3.MapEvent>); + @override + bool get isInitialized => + (super.noSuchMethod(Invocation.getter(#isInitialized), returnValue: false) + as bool); + @override + void debugSetOverrides( + {_i4.DebugCreateMapFunction? createMap, + _i4.MarkersController? markers, + _i4.CirclesController? circles, + _i4.PolygonsController? polygons, + _i4.PolylinesController? polylines}) => + super.noSuchMethod( + Invocation.method(#debugSetOverrides, [], { + #createMap: createMap, + #markers: markers, + #circles: circles, + #polygons: polygons, + #polylines: polylines + }), + returnValueForMissingStub: null); + @override + void init() => super.noSuchMethod(Invocation.method(#init, []), + returnValueForMissingStub: null); + @override + void updateRawOptions(Map? optionsUpdate) => + super.noSuchMethod(Invocation.method(#updateRawOptions, [optionsUpdate]), + returnValueForMissingStub: null); + @override + _i2.Future<_i3.LatLngBounds> getVisibleRegion() => (super.noSuchMethod( + Invocation.method(#getVisibleRegion, []), + returnValue: Future<_i3.LatLngBounds>.value(_FakeLatLngBounds_1())) + as _i2.Future<_i3.LatLngBounds>); + @override + _i2.Future<_i3.ScreenCoordinate> getScreenCoordinate(_i3.LatLng? latLng) => + (super.noSuchMethod(Invocation.method(#getScreenCoordinate, [latLng]), + returnValue: + Future<_i3.ScreenCoordinate>.value(_FakeScreenCoordinate_2())) + as _i2.Future<_i3.ScreenCoordinate>); + @override + _i2.Future<_i3.LatLng> getLatLng(_i3.ScreenCoordinate? screenCoordinate) => + (super.noSuchMethod(Invocation.method(#getLatLng, [screenCoordinate]), + returnValue: Future<_i3.LatLng>.value(_FakeLatLng_3())) + as _i2.Future<_i3.LatLng>); + @override + _i2.Future moveCamera(_i3.CameraUpdate? cameraUpdate) => + (super.noSuchMethod(Invocation.method(#moveCamera, [cameraUpdate]), + returnValue: Future.value(), + returnValueForMissingStub: Future.value()) as _i2.Future); + @override + _i2.Future getZoomLevel() => + (super.noSuchMethod(Invocation.method(#getZoomLevel, []), + returnValue: Future.value(0.0)) as _i2.Future); + @override + void updateCircles(_i3.CircleUpdates? updates) => + super.noSuchMethod(Invocation.method(#updateCircles, [updates]), + returnValueForMissingStub: null); + @override + void updatePolygons(_i3.PolygonUpdates? updates) => + super.noSuchMethod(Invocation.method(#updatePolygons, [updates]), + returnValueForMissingStub: null); + @override + void updatePolylines(_i3.PolylineUpdates? updates) => + super.noSuchMethod(Invocation.method(#updatePolylines, [updates]), + returnValueForMissingStub: null); + @override + void updateMarkers(_i3.MarkerUpdates? updates) => + super.noSuchMethod(Invocation.method(#updateMarkers, [updates]), + returnValueForMissingStub: null); + @override + void showInfoWindow(_i3.MarkerId? markerId) => + super.noSuchMethod(Invocation.method(#showInfoWindow, [markerId]), + returnValueForMissingStub: null); + @override + void hideInfoWindow(_i3.MarkerId? markerId) => + super.noSuchMethod(Invocation.method(#hideInfoWindow, [markerId]), + returnValueForMissingStub: null); + @override + bool isInfoWindowShown(_i3.MarkerId? markerId) => + (super.noSuchMethod(Invocation.method(#isInfoWindowShown, [markerId]), + returnValue: false) as bool); + @override + void dispose() => super.noSuchMethod(Invocation.method(#dispose, []), + returnValueForMissingStub: null); + @override + String toString() => super.toString(); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart new file mode 100644 index 000000000000..cfa36febbbfe --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/marker_test.dart @@ -0,0 +1,186 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import '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 onDragStart(gmaps.LatLng _) { + _methodCalledCompleter.complete(true); + } + + void onDrag(gmaps.LatLng _) { + _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('onDragStart gets called', (WidgetTester tester) async { + MarkerController(marker: marker, onDragStart: onDragStart); + + // Trigger a drag end event... + gmaps.Event.trigger(marker, 'dragstart', + [gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0)]); + + expect(await methodCalled, isTrue); + }); + + testWidgets('onDrag gets called', (WidgetTester tester) async { + MarkerController(marker: marker, onDrag: onDrag); + + // Trigger a drag end event... + gmaps.Event.trigger( + marker, 'drag', [gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0)]); + + expect(await methodCalled, isTrue); + }); + + testWidgets('onDragEnd gets called', (WidgetTester tester) async { + MarkerController(marker: marker, onDragEnd: onDragEnd); + + // Trigger a drag end event... + gmaps.Event.trigger(marker, 'dragend', + [gmaps.MapMouseEvent()..latLng = gmaps.LatLng(0, 0)]); + + 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/projection_test.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/projection_test.dart new file mode 100644 index 000000000000..8a5a62013538 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/projection_test.dart @@ -0,0 +1,265 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// These tests render an app with a small map widget, and use its map controller +// to compute values of the default projection. + +// (Tests methods that can't be mocked in `google_maps_controller_test.dart`) + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart' + show GoogleMap, GoogleMapController; +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:integration_test/integration_test.dart'; + +// This value is used when comparing long~num, like LatLng values. +const _acceptableLatLngDelta = 0.0000000001; + +// This value is used when comparing pixel measurements, mostly to gloss over +// browser rounding errors. +const _acceptablePixelDelta = 1; + +/// Test Google Map Controller +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('Methods that require a proper Projection', () { + final LatLng center = LatLng(43.3078, -5.6958); + final Size size = Size(320, 240); + final CameraPosition initialCamera = CameraPosition( + target: center, + zoom: 14, + ); + + late Completer controllerCompleter; + late void Function(GoogleMapController) onMapCreated; + + setUp(() { + controllerCompleter = Completer(); + onMapCreated = (GoogleMapController mapController) { + controllerCompleter.complete(mapController); + }; + }); + + group('getScreenCoordinate', () { + testWidgets('target of map is in center of widget', + (WidgetTester tester) async { + pumpCenteredMap( + tester, + initialCamera: initialCamera, + size: size, + onMapCreated: onMapCreated, + ); + + final GoogleMapController controller = await controllerCompleter.future; + + final ScreenCoordinate screenPosition = + await controller.getScreenCoordinate(center); + + expect( + screenPosition.x, + closeTo(size.width / 2, _acceptablePixelDelta), + ); + expect( + screenPosition.y, + closeTo(size.height / 2, _acceptablePixelDelta), + ); + }); + + testWidgets('NorthWest of visible region corresponds to x:0, y:0', + (WidgetTester tester) async { + pumpCenteredMap( + tester, + initialCamera: initialCamera, + size: size, + onMapCreated: onMapCreated, + ); + final GoogleMapController controller = await controllerCompleter.future; + + final LatLngBounds bounds = await controller.getVisibleRegion(); + final LatLng northWest = LatLng( + bounds.northeast.latitude, + bounds.southwest.longitude, + ); + + final ScreenCoordinate screenPosition = + await controller.getScreenCoordinate(northWest); + + expect(screenPosition.x, closeTo(0, _acceptablePixelDelta)); + expect(screenPosition.y, closeTo(0, _acceptablePixelDelta)); + }); + + testWidgets( + 'SouthEast of visible region corresponds to x:size.width, y:size.height', + (WidgetTester tester) async { + pumpCenteredMap( + tester, + initialCamera: initialCamera, + size: size, + onMapCreated: onMapCreated, + ); + final GoogleMapController controller = await controllerCompleter.future; + + final LatLngBounds bounds = await controller.getVisibleRegion(); + final LatLng southEast = LatLng( + bounds.southwest.latitude, + bounds.northeast.longitude, + ); + + final ScreenCoordinate screenPosition = + await controller.getScreenCoordinate(southEast); + + expect(screenPosition.x, closeTo(size.width, _acceptablePixelDelta)); + expect(screenPosition.y, closeTo(size.height, _acceptablePixelDelta)); + }); + }); + + group('getLatLng', () { + testWidgets('Center of widget is the target of map', + (WidgetTester tester) async { + pumpCenteredMap( + tester, + initialCamera: initialCamera, + size: size, + onMapCreated: onMapCreated, + ); + + final GoogleMapController controller = await controllerCompleter.future; + + final LatLng coords = await controller.getLatLng( + ScreenCoordinate(x: size.width ~/ 2, y: size.height ~/ 2), + ); + + expect( + coords.latitude, + closeTo(center.latitude, _acceptableLatLngDelta), + ); + expect( + coords.longitude, + closeTo(center.longitude, _acceptableLatLngDelta), + ); + }); + + testWidgets('Top-left of widget is NorthWest bound of map', + (WidgetTester tester) async { + pumpCenteredMap( + tester, + initialCamera: initialCamera, + size: size, + onMapCreated: onMapCreated, + ); + final GoogleMapController controller = await controllerCompleter.future; + + final LatLngBounds bounds = await controller.getVisibleRegion(); + final LatLng northWest = LatLng( + bounds.northeast.latitude, + bounds.southwest.longitude, + ); + + final LatLng coords = await controller.getLatLng( + ScreenCoordinate(x: 0, y: 0), + ); + + expect( + coords.latitude, + closeTo(northWest.latitude, _acceptableLatLngDelta), + ); + expect( + coords.longitude, + closeTo(northWest.longitude, _acceptableLatLngDelta), + ); + }); + + testWidgets('Bottom-right of widget is SouthWest bound of map', + (WidgetTester tester) async { + pumpCenteredMap( + tester, + initialCamera: initialCamera, + size: size, + onMapCreated: onMapCreated, + ); + final GoogleMapController controller = await controllerCompleter.future; + + final LatLngBounds bounds = await controller.getVisibleRegion(); + final LatLng southEast = LatLng( + bounds.southwest.latitude, + bounds.northeast.longitude, + ); + + final LatLng coords = await controller.getLatLng( + ScreenCoordinate(x: size.width.toInt(), y: size.height.toInt()), + ); + + expect( + coords.latitude, + closeTo(southEast.latitude, _acceptableLatLngDelta), + ); + expect( + coords.longitude, + closeTo(southEast.longitude, _acceptableLatLngDelta), + ); + }); + }); + }); +} + +// Pumps a CenteredMap Widget into a given tester, with some parameters +void pumpCenteredMap( + WidgetTester tester, { + required CameraPosition initialCamera, + Size size = const Size(320, 240), + void Function(GoogleMapController)? onMapCreated, +}) async { + await tester.pumpWidget( + CenteredMap( + initialCamera: initialCamera, + size: size, + onMapCreated: onMapCreated, + ), + ); + + // This is needed to kick-off the rendering of the JS Map flutter widget + await tester.pump(); +} + +/// Renders a Map widget centered on the screen. +/// This depends in `package:google_maps_flutter` to work. +class CenteredMap extends StatelessWidget { + const CenteredMap({ + required this.initialCamera, + required this.size, + required this.onMapCreated, + Key? key, + }) : super(key: key); + + /// A function that receives the [GoogleMapController] of the Map widget once initialized. + final void Function(GoogleMapController)? onMapCreated; + + /// The size of the rendered map widget. + final Size size; + + /// The initial camera position (center + zoom level) of the Map widget. + final CameraPosition initialCamera; + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + body: Center( + child: SizedBox.fromSize( + size: size, + child: GoogleMap( + initialCameraPosition: initialCamera, + onMapCreated: onMapCreated, + ), + ), + ), + ), + ); + } +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/resources/icon_image_base64.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/integration_test/resources/icon_image_base64.dart 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/example/lib/main.dart b/packages/google_maps_flutter/google_maps_flutter_web/example/lib/main.dart new file mode 100644 index 000000000000..10415204570c --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/lib/main.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/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/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..95a3d4253440 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/example/pubspec.yaml @@ -0,0 +1,27 @@ +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: ^2.1.1 + google_maps: ^5.2.0 + google_maps_flutter: # Used for projection_test.dart + path: ../../google_maps_flutter + http: ^0.13.0 + mockito: ^5.0.0 + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + 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/lib/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.dart new file mode 100644 index 000000000000..0355f2923528 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/google_maps_flutter_web.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. + +library google_maps_flutter_web; + +import 'dart:async'; +import 'dart:html'; +import 'dart:js_util'; +import 'src/shims/dart_ui.dart' as ui; // Conditionally imports dart:ui in web +import 'dart:convert'; + +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/gestures.dart'; + +import 'package:sanitize_html/sanitize_html.dart'; + +import 'package:stream_transform/stream_transform.dart'; + +import 'package:google_maps_flutter_platform_interface/google_maps_flutter_platform_interface.dart'; +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; +import 'package:google_maps/google_maps.dart' as gmaps; + +import 'src/third_party/to_screen_location/to_screen_location.dart'; +import 'src/types.dart'; + +part 'src/google_maps_flutter_web.dart'; +part 'src/google_maps_controller.dart'; +part 'src/circle.dart'; +part 'src/circles.dart'; +part 'src/polygon.dart'; +part 'src/polygons.dart'; +part 'src/polyline.dart'; +part 'src/polylines.dart'; +part 'src/marker.dart'; +part 'src/markers.dart'; +part 'src/convert.dart'; diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circle.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circle.dart new file mode 100644 index 000000000000..65057d8c869e --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circle.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. + +part of google_maps_flutter_web; + +/// The `CircleController` class wraps a [gmaps.Circle] and its `onTap` behavior. +class CircleController { + gmaps.Circle? _circle; + + final bool _consumeTapEvents; + + /// Creates a `CircleController`, which wraps a [gmaps.Circle] object and its `onTap` behavior. + CircleController({ + required gmaps.Circle circle, + bool consumeTapEvents = false, + ui.VoidCallback? onTap, + }) : _circle = circle, + _consumeTapEvents = consumeTapEvents { + if (onTap != null) { + circle.onClick.listen((_) { + onTap.call(); + }); + } + } + + /// Returns the wrapped [gmaps.Circle]. Only used for testing. + @visibleForTesting + 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) { + assert(_circle != null, 'Cannot `update` Circle after calling `remove`.'); + _circle!.options = options; + } + + /// Disposes of the currently wrapped [gmaps.Circle]. + void remove() { + 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 new file mode 100644 index 000000000000..ae8faa038ea6 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/circles.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. + +part of google_maps_flutter_web; + +/// This class manages all the [CircleController]s associated to a [GoogleMapController]. +class CirclesController extends GeometryController { + // A cache of [CircleController]s indexed by their [CircleId]. + final Map _circleIdToController; + + // The stream over which circles broadcast their events + StreamController _streamController; + + /// Initialize the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. + CirclesController({ + required StreamController stream, + }) : _streamController = stream, + _circleIdToController = Map(); + + /// Returns the cache of [CircleController]s. Test only. + @visibleForTesting + Map get circles => _circleIdToController; + + /// Adds a set of [Circle] objects to the cache. + /// + /// Wraps each [Circle] into its corresponding [CircleController]. + void addCircles(Set circlesToAdd) { + circlesToAdd.forEach((circle) { + _addCircle(circle); + }); + } + + void _addCircle(Circle circle) { + if (circle == null) { + return; + } + + final populationOptions = _circleOptionsFromCircle(circle); + gmaps.Circle gmCircle = gmaps.Circle(populationOptions); + gmCircle.map = googleMap; + CircleController controller = CircleController( + circle: gmCircle, + consumeTapEvents: circle.consumeTapEvents, + onTap: () { + _onCircleTap(circle.circleId); + }); + _circleIdToController[circle.circleId] = controller; + } + + /// Updates a set of [Circle] objects with new options. + void changeCircles(Set circlesToChange) { + circlesToChange.forEach((circleToChange) { + _changeCircle(circleToChange); + }); + } + + void _changeCircle(Circle circle) { + 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]; + circleController?.remove(); + _circleIdToController.remove(circleId); + }); + } + + // Handles the global onCircleTap function to funnel events from circles into the stream. + bool _onCircleTap(CircleId circleId) { + // Have you ended here on your debugging? Is this wrong? + // Comment here: https://github.com/flutter/flutter/issues/64084 + _streamController.add(CircleTapEvent(mapId, circleId)); + return _circleIdToController[circleId]?.consumeTapEvents ?? false; + } +} 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 new file mode 100644 index 000000000000..c026a03be804 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/convert.dart @@ -0,0 +1,444 @@ +// Copyright 2013 The Flutter 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; + +// 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 _defaultCssColor = '#000000'; +final _defaultCssOpacity = 0.0; + +// Indices in the plugin side don't match with the ones +// in the gmaps lib. This translates from plugin -> gmaps. +final _mapTypeToMapTypeId = { + 0: gmaps.MapTypeId.ROADMAP, // "none" in the plugin + 1: gmaps.MapTypeId.ROADMAP, + 2: gmaps.MapTypeId.SATELLITE, + 3: gmaps.MapTypeId.TERRAIN, + 4: gmaps.MapTypeId.HYBRID, +}; + +// 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: +// compassEnabled +// rotateGesturesEnabled +// tiltGesturesEnabled +// mapToolbarEnabled is unused in web, there's no "map toolbar" +// myLocationButtonEnabled Widget not available in web yet, it needs to be built on top of the maps widget +// See: https://developers.google.com/maps/documentation/javascript/examples/control-custom +// myLocationEnabled needs to be built through dart:html navigator.geolocation +// See: https://api.dart.dev/stable/2.8.4/dart-html/Geolocation-class.html +// trafficEnabled is handled when creating the GMap object, since it needs to be added as a layer. +// trackCameraPosition is just a boolan value that indicates if the map has an onCameraMove handler. +// indoorViewEnabled seems to not have an equivalent in web +// buildingsEnabled seems to not have an equivalent in web +// padding seems to behave differently in web than mobile. You can't move UI elements in web. +gmaps.MapOptions _rawOptionsToGmapsOptions(Map rawOptions) { + gmaps.MapOptions options = gmaps.MapOptions(); + + if (_mapTypeToMapTypeId.containsKey(rawOptions['mapType'])) { + options.mapTypeId = _mapTypeToMapTypeId[rawOptions['mapType']]; + } + + if (rawOptions['minMaxZoomPreference'] != null) { + options + ..minZoom = rawOptions['minMaxZoomPreference'][0] + ..maxZoom = rawOptions['minMaxZoomPreference'][1]; + } + + if (rawOptions['cameraTargetBounds'] != null) { + // Needs gmaps.MapOptions.restriction and gmaps.MapRestriction + // see: https://developers.google.com/maps/documentation/javascript/reference/map#MapOptions.restriction + } + + if (rawOptions['zoomControlsEnabled'] != null) { + options.zoomControl = rawOptions['zoomControlsEnabled']; + } + + if (rawOptions['styles'] != null) { + options.styles = rawOptions['styles']; + } + + if (rawOptions['scrollGesturesEnabled'] == false || + rawOptions['zoomGesturesEnabled'] == false) { + options.gestureHandling = 'none'; + } else { + options.gestureHandling = 'auto'; + } + + // These don't have any rawOptions entry, but they seem to be off in the native maps. + options.mapTypeControl = false; + options.fullscreenControl = false; + options.streetViewControl = false; + + return options; +} + +gmaps.MapOptions _applyInitialPosition( + CameraPosition initialPosition, + gmaps.MapOptions options, +) { + // Adjust the initial position, if passed... + if (initialPosition != null) { + 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) { + return rawOptions['trafficEnabled'] ?? false; +} + +// The keys we'd expect to see in a serialized MapTypeStyle JSON object. +final _mapStyleKeys = { + 'elementType', + 'featureType', + 'stylers', +}; + +// Checks if the passed in Map contains some of the _mapStyleKeys. +bool _isJsonMapStyle(Map value) { + return _mapStyleKeys.intersection(value.keys.toSet()).isNotEmpty; +} + +// Converts an incoming JSON-encoded Style info, into the correct gmaps array. +List _mapStyles(String? mapStyleJson) { + List styles = []; + if (mapStyleJson != null) { + styles = json + .decode(mapStyleJson, reviver: (key, value) { + if (value is Map && _isJsonMapStyle(value)) { + return gmaps.MapTypeStyle() + ..elementType = value['elementType'] + ..featureType = value['featureType'] + ..stylers = + (value['stylers'] as List).map((e) => jsify(e)).toList(); + } + return value; + }) + .cast() + .toList(); + // .toList calls are required so the JS API understands the underlying data structure. + } + return styles; +} + +gmaps.LatLng _latLngToGmLatLng(LatLng latLng) { + return gmaps.LatLng(latLng.latitude, latLng.longitude); +} + +LatLng _gmLatLngToLatLng(gmaps.LatLng latLng) { + return LatLng(latLng.lat.toDouble(), latLng.lng.toDouble()); +} + +LatLngBounds _gmLatLngBoundsTolatLngBounds(gmaps.LatLngBounds latLngBounds) { + return LatLngBounds( + southwest: _gmLatLngToLatLng(latLngBounds.southWest), + northeast: _gmLatLngToLatLng(latLngBounds.northEast), + ); +} + +CameraPosition _gmViewportToCameraPosition(gmaps.GMap map) { + return CameraPosition( + target: _gmLatLngToLatLng(map.center ?? _nullGmapsLatLng), + bearing: map.heading?.toDouble() ?? 0, + tilt: map.tilt?.toDouble() ?? 0, + zoom: map.zoom?.toDouble() ?? 0, + ); +} + +// 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) { + 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; + } + + // 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 = container + ..zIndex = marker.zIndex; + // TODO: Compute the pixelOffset of the infoWindow, from the size of the Marker, + // and the marker.infoWindow.anchor property. +} + +// Computes the options for a new [gmaps.Marker] from an incoming set of options +// [marker], and the existing marker registered with the map: [currentMarker]. +// Preserves the position from the [currentMarker], if set. +gmaps.MarkerOptions _markerOptionsFromMarker( + Marker marker, + gmaps.Marker? currentMarker, +) { + final iconConfig = marker.icon.toJson() as List; + gmaps.Icon? icon; + + if (iconConfig != null) { + if (iconConfig[0] == 'fromAssetImage') { + assert(iconConfig.length >= 2); + // iconConfig[2] contains the DPIs of the screen, but that information is + // already encoded in the iconConfig[1] + + icon = gmaps.Icon() + ..url = ui.webOnlyAssetManager.getAssetUrl(iconConfig[1]); + + // iconConfig[3] may contain the [width, height] of the image, if passed! + if (iconConfig.length >= 4 && iconConfig[3] != null) { + final size = gmaps.Size(iconConfig[3][0], iconConfig[3][1]); + icon + ..size = size + ..scaledSize = size; + } + } else if (iconConfig[0] == 'fromBytes') { + // Grab the bytes, and put them into a blob + List bytes = iconConfig[1]; + final blob = Blob([bytes]); // Let the browser figure out the encoding + icon = gmaps.Icon()..url = Url.createObjectUrlFromBlob(blob); + } + } + return gmaps.MarkerOptions() + ..position = currentMarker?.position ?? + gmaps.LatLng( + marker.position.latitude, + marker.position.longitude, + ) + ..title = sanitizeHtml(marker.infoWindow.title ?? "") + ..zIndex = marker.zIndex + ..visible = marker.visible + ..opacity = marker.alpha + ..draggable = marker.draggable + ..icon = icon; + // TODO: Compute anchor properly, otherwise infowindows attach to the wrong spot. + // Flat and Rotation are not supported directly on the web. +} + +gmaps.CircleOptions _circleOptionsFromCircle(Circle circle) { + final circleOptions = gmaps.CircleOptions() + ..strokeColor = _getCssColor(circle.strokeColor) + ..strokeOpacity = _getCssOpacity(circle.strokeColor) + ..strokeWeight = circle.strokeWidth + ..fillColor = _getCssColor(circle.fillColor) + ..fillOpacity = _getCssOpacity(circle.fillColor) + ..center = gmaps.LatLng(circle.center.latitude, circle.center.longitude) + ..radius = circle.radius + ..visible = circle.visible + ..zIndex = circle.zIndex; + return circleOptions; +} + +gmaps.PolygonOptions _polygonOptionsFromPolygon( + gmaps.GMap googleMap, Polygon polygon) { + List path = []; + polygon.points.forEach((point) { + path.add(_latLngToGmLatLng(point)); + }); + final polygonDirection = _isPolygonClockwise(path); + List> paths = [path]; + int holeIndex = 0; + polygon.holes.forEach((hole) { + List holePath = + hole.map((point) => _latLngToGmLatLng(point)).toList(); + if (_isPolygonClockwise(holePath) == polygonDirection) { + holePath = holePath.reversed.toList(); + if (kDebugMode) { + print( + 'Hole [$holeIndex] in Polygon [${polygon.polygonId.value}] has been reversed.' + ' Ensure holes in polygons are "wound in the opposite direction to the outer path."' + ' More info: https://github.com/flutter/flutter/issues/74096'); + } + } + paths.add(holePath); + holeIndex++; + }); + return gmaps.PolygonOptions() + ..paths = paths + ..strokeColor = _getCssColor(polygon.strokeColor) + ..strokeOpacity = _getCssOpacity(polygon.strokeColor) + ..strokeWeight = polygon.strokeWidth + ..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 = []; + polyline.points.forEach((point) { + paths.add(_latLngToGmLatLng(point)); + }); + + return gmaps.PolylineOptions() + ..path = paths + ..strokeWeight = polyline.width + ..strokeColor = _getCssColor(polyline.color) + ..strokeOpacity = _getCssOpacity(polyline.color) + ..visible = polyline.visible + ..zIndex = polyline.zIndex + ..geodesic = polyline.geodesic; +// this.endCap = Cap.buttCap, +// this.jointType = JointType.mitered, +// this.patterns = const [], +// this.startCap = Cap.buttCap, +// this.width = 10, +} + +// Translates a [CameraUpdate] into operations on a [gmaps.GMap]. +void _applyCameraUpdate(gmaps.GMap map, CameraUpdate update) { + final json = update.toJson() as List; + switch (json[0]) { + case 'newCameraPosition': + map.heading = json[1]['bearing']; + map.zoom = json[1]['zoom']; + map.panTo(gmaps.LatLng(json[1]['target'][0], json[1]['target'][1])); + map.tilt = json[1]['tilt']; + break; + case 'newLatLng': + map.panTo(gmaps.LatLng(json[1][0], json[1][1])); + break; + case 'newLatLngZoom': + map.zoom = json[2]; + map.panTo(gmaps.LatLng(json[1][0], json[1][1])); + break; + case 'newLatLngBounds': + map.fitBounds(gmaps.LatLngBounds( + gmaps.LatLng(json[1][0][0], json[1][0][1]), + gmaps.LatLng(json[1][1][0], json[1][1][1]))); + // padding = json[2]; + // Needs package:google_maps ^4.0.0 to adjust the padding in fitBounds + break; + case 'scrollBy': + map.panBy(json[1], json[2]); + break; + case 'zoomBy': + gmaps.LatLng? focusLatLng; + double zoomDelta = json[1] ?? 0; + // Web only supports integer changes... + int newZoomDelta = zoomDelta < 0 ? zoomDelta.floor() : zoomDelta.ceil(); + if (json.length == 3) { + // With focus + try { + focusLatLng = _pixelToLatLng(map, json[2][0], json[2][1]); + } catch (e) { + // https://github.com/a14n/dart-google-maps/issues/87 + // print('Error computing new focus LatLng. JS Error: ' + e.toString()); + } + } + map.zoom = (map.zoom ?? 0) + newZoomDelta; + if (focusLatLng != null) { + map.panTo(focusLatLng); + } + break; + case 'zoomIn': + map.zoom = (map.zoom ?? 0) + 1; + break; + case 'zoomOut': + map.zoom = (map.zoom ?? 0) - 1; + break; + case 'zoomTo': + map.zoom = json[1]; + break; + default: + throw UnimplementedError('Unimplemented CameraMove: ${json[0]}.'); + } +} + +// original JS by: Byron Singh (https://stackoverflow.com/a/30541162) +gmaps.LatLng _pixelToLatLng(gmaps.GMap map, int x, int y) { + final bounds = map.bounds; + final projection = map.projection; + final zoom = map.zoom; + + 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 scale = 1 << (zoom!.toInt()); // 2 ^ zoom + + final point = + gmaps.Point((x / scale) + bottomLeft.x!, (y / scale) + topRight.y!); + + return projection.fromPointToLatLng!(point)!; +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart new file mode 100644 index 000000000000..edf47764f346 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_controller.dart @@ -0,0 +1,431 @@ +// Copyright 2013 The Flutter 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; + +/// Type used when passing an override to the _createMap function. +@visibleForTesting +typedef DebugCreateMapFunction = gmaps.GMap Function( + HtmlElement div, gmaps.MapOptions options); + +/// Encapsulates a [gmaps.GMap], its events, and where in the DOM it's rendered. +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 _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; + late HtmlElement _div; + + /// The Flutter widget that will contain the rendered Map. Used for caching. + Widget? get widget { + if (_widget == null && !_streamController.isClosed) { + _widget = HtmlElementView( + viewType: _getViewType(_mapId), + ); + } + return _widget; + } + + // The currently-enabled traffic layer. + gmaps.TrafficLayer? _trafficLayer; + + /// A getter for the current traffic layer. Only for tests. + @visibleForTesting + gmaps.TrafficLayer? get trafficLayer => _trafficLayer; + + // The underlying GMap instance. This is the interface with the JS SDK. + gmaps.GMap? _googleMap; + + // The StreamController used by this controller and the geometry ones. + final StreamController _streamController; + + /// The StreamController for the events of this Map. Only for integration testing. + @visibleForTesting + StreamController get stream => _streamController; + + /// The Stream over which this controller broadcasts events. + Stream get events => _streamController.stream; + + // Geometry controllers, for different features of the map. + CirclesController? _circlesController; + PolygonsController? _polygonsController; + PolylinesController? _polylinesController; + MarkersController? _markersController; + // Keeps track if _attachGeometryControllers has been called or not. + bool _controllersBoundToMap = false; + + // Keeps track if the map is moving or not. + bool _mapIsMoving = false; + + /// Initializes the GMap, and the sub-controllers related to it. Wires events. + GoogleMapController({ + required int mapId, + required StreamController streamController, + required CameraPosition initialCameraPosition, + Set markers = const {}, + Set polygons = const {}, + Set polylines = const {}, + Set circles = const {}, + Set tileOverlays = const {}, + Set> gestureRecognizers = + const >{}, + Map mapOptions = const {}, + }) : _mapId = mapId, + _streamController = streamController, + _initialCameraPosition = initialCameraPosition, + _markers = markers, + _polygons = polygons, + _polylines = polylines, + _circles = circles, + _rawMapOptions = mapOptions { + _circlesController = CirclesController(stream: this._streamController); + _polygonsController = PolygonsController(stream: this._streamController); + _polylinesController = PolylinesController(stream: this._streamController); + _markersController = MarkersController(stream: this._streamController); + + // Register the view factory that will hold the `_div` that holds the map in the DOM. + // The `_div` needs to be created outside of the ViewFactory (and cached!) so we can + // use it to create the [gmaps.GMap] in the `init()` method of this class. + _div = DivElement() + ..id = _getViewType(mapId) + ..style.width = '100%' + ..style.height = '100%'; + + ui.platformViewRegistry.registerViewFactory( + _getViewType(mapId), + (int viewId) => _div, + ); + } + + /// Overrides certain properties to install mocks defined during testing. + @visibleForTesting + void debugSetOverrides({ + DebugCreateMapFunction? createMap, + MarkersController? markers, + CirclesController? circles, + PolygonsController? polygons, + PolylinesController? polylines, + }) { + _overrideCreateMap = createMap; + _markersController = markers ?? _markersController; + _circlesController = circles ?? _circlesController; + _polygonsController = polygons ?? _polygonsController; + _polylinesController = polylines ?? _polylinesController; + } + + DebugCreateMapFunction? _overrideCreateMap; + + gmaps.GMap _createMap(HtmlElement div, gmaps.MapOptions options) { + if (_overrideCreateMap != null) { + return _overrideCreateMap!(div, options); + } + return gmaps.GMap(div, options); + } + + /// A flag that returns true if the controller has been initialized or not. + @visibleForTesting + bool get isInitialized => _googleMap != null; + + /// Starts the JS Maps SDK into the target [_div] with `rawOptions`. + /// + /// (Also initializes the geometry/traffic layers.) + /// + /// The first part of this method starts the rendering of a [gmaps.GMap] inside + /// of the target [_div], with configuration from `rawOptions`. It then stores + /// the created GMap in the [_googleMap] attribute. + /// + /// Not *everything* is rendered with the initial `rawOptions` configuration, + /// geometry and traffic layers (and possibly others in the future) have their + /// own configuration and are rendered on top of a GMap instance later. This + /// happens in the second half of this method. + /// + /// This method is eagerly called from the [GoogleMapsPlugin.buildView] method + /// so the internal [GoogleMapsController] of a Web Map initializes as soon as + /// possible. Check [_attachMapEvents] to see how this controller notifies the + /// plugin of it being fully ready (through the `onTilesloaded.first` event). + /// + /// Failure to call this method would result in the GMap not rendering at all, + /// and most of the public methods on this class no-op'ing. + void init() { + var options = _rawOptionsToGmapsOptions(_rawMapOptions); + // Initial position can only to be set here! + options = _applyInitialPosition(_initialCameraPosition, options); + + // Create the map... + final map = _createMap(_div, options); + _googleMap = map; + + _attachMapEvents(map); + _attachGeometryControllers(map); + + // Now attach the geometry, traffic and any other layers... + _renderInitialGeometry( + markers: _markers, + circles: _circles, + polygons: _polygons, + polylines: _polylines, + ); + + _setTrafficLayer(map, _isTrafficLayerEnabled(_rawMapOptions)); + } + + // Funnels map gmap events into the plugin's stream controller. + void _attachMapEvents(gmaps.GMap map) { + map.onTilesloaded.first.then((event) { + // Report the map as ready to go the first time the tiles load + _streamController.add(WebMapReadyEvent(_mapId)); + }); + map.onClick.listen((event) { + assert(event.latLng != null); + _streamController.add( + MapTapEvent(_mapId, _gmLatLngToLatLng(event.latLng!)), + ); + }); + map.onRightclick.listen((event) { + assert(event.latLng != null); + _streamController.add( + MapLongPressEvent(_mapId, _gmLatLngToLatLng(event.latLng!)), + ); + }); + map.onBoundsChanged.listen((event) { + if (!_mapIsMoving) { + _mapIsMoving = true; + _streamController.add(CameraMoveStartedEvent(_mapId)); + } + _streamController.add( + CameraMoveEvent(_mapId, _gmViewportToCameraPosition(map)), + ); + }); + map.onIdle.listen((event) { + _mapIsMoving = false; + _streamController.add(CameraIdleEvent(_mapId)); + }); + } + + // Binds the Geometry controllers to a map instance + void _attachGeometryControllers(gmaps.GMap map) { + // Now we can add the initial geometry. + // And bind the (ready) map instance to the other geometry controllers. + // + // 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 = 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.'); + + // 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 _rawMapOptions map. + // + // Returns the updated _rawMapOptions object. + Map _mergeRawOptions(Map newOptions) { + _rawMapOptions = { + ..._rawMapOptions, + ...newOptions, + }; + 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)); + } + + // Sets new [gmaps.MapOptions] on the wrapped map. + void _setOptions(gmaps.MapOptions options) { + _googleMap?.options = options; + } + + // 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()..set('map', map); + } + if (!attach && _trafficLayer != null) { + _trafficLayer!.set('map', null); + _trafficLayer = null; + } + } + + // _googleMap manipulation + // Viewport + + /// Returns the [LatLngBounds] of the current viewport. + Future getVisibleRegion() async { + 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.'); + + final point = toScreenLocation(_googleMap!, _latLngToGmLatLng(latLng)); + + 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 { + 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 { + 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 { + 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); + } + + /// 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); + } + + /// 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); + } + + /// 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); + } + + /// 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) ?? 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; + _circlesController = null; + _polygonsController = null; + _polylinesController = null; + _markersController = null; + _streamController.close(); + } +} + +/// An event fired when a [mapId] on web is interactive. +class WebMapReadyEvent extends MapEvent { + /// Build a WebMapReady Event for the map represented by `mapId`. + WebMapReadyEvent(int mapId) : super(mapId, null); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.dart new file mode 100644 index 000000000000..47bfdc7bba15 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/google_maps_flutter_web.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. + +part of google_maps_flutter_web; + +/// The web implementation of [GoogleMapsFlutterPlatform]. +/// +/// This class implements the `package:google_maps_flutter` functionality for the web. +class GoogleMapsPlugin extends GoogleMapsFlutterPlatform { + /// Registers this class as the default instance of [GoogleMapsFlutterPlatform]. + static void registerWith(Registrar registrar) { + GoogleMapsFlutterPlatform.instance = GoogleMapsPlugin(); + } + + // A cache of map controllers by map Id. + Map _mapById = Map(); + + /// Allows tests to inject controllers without going through the buildView flow. + @visibleForTesting + void debugSetMapById(Map mapById) { + _mapById = mapById; + } + + // Convenience getter for a stream of events filtered by their mapId. + Stream _events(int mapId) => _map(mapId).events; + + // Convenience getter for a map controller by its mapId. + GoogleMapController _map(int mapId) { + final controller = _mapById[mapId]; + assert(controller != null, + 'Maps cannot be retrieved before calling buildView!'); + return controller; + } + + @override + Future init(int mapId) async { + // The internal instance of our controller is initialized eagerly in `buildView`, + // so we don't have to do anything in this method, which is left intentionally + // blank. + assert(_map(mapId) != null, 'Must call buildWidget before init!'); + } + + /// Updates the options of a given `mapId`. + /// + /// This attempts to merge the new `optionsUpdate` passed in, with the previous + /// options passed to the map (in other updates, or when creating it). + @override + Future updateMapOptions( + Map optionsUpdate, { + required int mapId, + }) async { + _map(mapId).updateRawOptions(optionsUpdate); + } + + /// Applies the passed in `markerUpdates` to the `mapId`. + @override + Future updateMarkers( + MarkerUpdates markerUpdates, { + required int mapId, + }) async { + _map(mapId).updateMarkers(markerUpdates); + } + + /// Applies the passed in `polygonUpdates` to the `mapId`. + @override + Future updatePolygons( + PolygonUpdates polygonUpdates, { + required int mapId, + }) async { + _map(mapId).updatePolygons(polygonUpdates); + } + + /// Applies the passed in `polylineUpdates` to the `mapId`. + @override + Future updatePolylines( + PolylineUpdates polylineUpdates, { + required int mapId, + }) async { + _map(mapId).updatePolylines(polylineUpdates); + } + + /// Applies the passed in `circleUpdates` to the `mapId`. + @override + Future updateCircles( + CircleUpdates circleUpdates, { + 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, + }) async { + return moveCamera(cameraUpdate, mapId: mapId); + } + + /// Applies the given `cameraUpdate` to the current viewport. + @override + Future moveCamera( + CameraUpdate cameraUpdate, { + required int mapId, + }) async { + return _map(mapId).moveCamera(cameraUpdate); + } + + /// Sets the passed-in `mapStyle` to the map. + /// + /// This function just adds a 'styles' option to the current map options. + /// + /// Subsequent calls to this method override previous calls, you need to + /// pass full styles. + @override + Future setMapStyle( + String? mapStyle, { + required int mapId, + }) async { + _map(mapId).updateRawOptions({ + 'styles': _mapStyles(mapStyle), + }); + } + + /// Returns the bounds of the current viewport. + @override + Future getVisibleRegion({ + required int mapId, + }) { + return _map(mapId).getVisibleRegion(); + } + + /// Returns the screen coordinate (in pixels) of a given `latLng`. + @override + Future getScreenCoordinate( + LatLng latLng, { + required int mapId, + }) { + return _map(mapId).getScreenCoordinate(latLng); + } + + /// Returns the [LatLng] of a [ScreenCoordinate] of the viewport. + @override + Future getLatLng( + ScreenCoordinate screenCoordinate, { + required int mapId, + }) { + return _map(mapId).getLatLng(screenCoordinate); + } + + /// Shows the [InfoWindow] (if any) of the [Marker] identified by `markerId`. + /// + /// See also: + /// * [hideMarkerInfoWindow] to hide the info window. + /// * [isMarkerInfoWindowShown] to check if the info window is visible/hidden. + @override + Future showMarkerInfoWindow( + MarkerId markerId, { + required int mapId, + }) async { + _map(mapId).showInfoWindow(markerId); + } + + /// Hides the [InfoWindow] (if any) of the [Marker] identified by `markerId`. + /// + /// See also: + /// * [showMarkerInfoWindow] to show the info window. + /// * [isMarkerInfoWindowShown] to check if the info window is shown. + @override + Future hideMarkerInfoWindow( + MarkerId markerId, { + required int mapId, + }) async { + _map(mapId).hideInfoWindow(markerId); + } + + /// Returns true if the [InfoWindow] of the [Marker] identified by `markerId` is shown. + /// + /// See also: + /// * [showMarkerInfoWindow] to show the info window. + /// * [hideMarkerInfoWindow] to hide the info window. + @override + Future isMarkerInfoWindowShown( + MarkerId markerId, { + required int mapId, + }) async { + return _map(mapId).isInfoWindowShown(markerId); + } + + /// Returns the zoom level of the `mapId`. + @override + Future getZoomLevel({ + required int mapId, + }) { + return _map(mapId).getZoomLevel(); + } + + // The following are the 11 possible streams of data from the native side + // into the plugin + + @override + Stream onCameraMoveStarted({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onCameraMove({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onCameraIdle({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onMarkerTap({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onInfoWindowTap({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onMarkerDragStart({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onMarkerDrag({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onMarkerDragEnd({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onPolylineTap({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onPolygonTap({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onCircleTap({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onTap({required int mapId}) { + return _events(mapId).whereType(); + } + + @override + Stream onLongPress({required int mapId}) { + return _events(mapId).whereType(); + } + + /// Disposes of the current map. It can't be used afterwards! + @override + void dispose({required int mapId}) { + _map(mapId).dispose(); + _mapById.remove(mapId); + } + + @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 {}, + }) { + // 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( + initialCameraPosition: initialCameraPosition, + mapId: creationId, + streamController: controller, + markers: markers, + polygons: polygons, + polylines: polylines, + circles: circles, + mapOptions: mapOptions, + )..init(); // Initialize the controller + + _mapById[creationId] = mapController; + + mapController.events.whereType().first.then((event) { + assert(creationId == event.mapId, + 'Received WebMapReadyEvent for the wrong map'); + // Notify the plugin now that there's a fully initialized controller. + onPlatformViewCreated.call(event.mapId); + }); + + assert(mapController.widget != null, + 'The widget of a GoogleMapController cannot be null before calling dispose on it.'); + + 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 new file mode 100644 index 000000000000..c4cd40f43323 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/marker.dart @@ -0,0 +1,118 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of google_maps_flutter_web; + +/// The `MarkerController` class wraps a [gmaps.Marker], how it handles events, and its associated (optional) [gmaps.InfoWindow] widget. +class MarkerController { + gmaps.Marker? _marker; + + final bool _consumeTapEvents; + + final gmaps.InfoWindow? _infoWindow; + + bool _infoWindowShown = false; + + /// Creates a `MarkerController`, which wraps a [gmaps.Marker] object, its `onTap`/`onDrag` behavior, and its associated [gmaps.InfoWindow]. + MarkerController({ + required gmaps.Marker marker, + gmaps.InfoWindow? infoWindow, + bool consumeTapEvents = false, + LatLngCallback? onDragStart, + LatLngCallback? onDrag, + LatLngCallback? onDragEnd, + ui.VoidCallback? onTap, + }) : _marker = marker, + _infoWindow = infoWindow, + _consumeTapEvents = consumeTapEvents { + if (onTap != null) { + marker.onClick.listen((event) { + onTap.call(); + }); + } + if (onDragStart != null) { + marker.onDragstart.listen((event) { + if (marker != null) { + marker.position = event.latLng; + } + onDragStart.call(event.latLng ?? _nullGmapsLatLng); + }); + } + if (onDrag != null) { + marker.onDrag.listen((event) { + if (marker != null) { + marker.position = event.latLng; + } + onDrag.call(event.latLng ?? _nullGmapsLatLng); + }); + } + if (onDragEnd != null) { + marker.onDragend.listen((event) { + if (marker != null) { + marker.position = event.latLng; + } + onDragEnd.call(event.latLng ?? _nullGmapsLatLng); + }); + } + } + + /// Returns `true` if this Controller will use its own `onTap` handler to consume events. + bool get consumeTapEvents => _consumeTapEvents; + + /// Returns `true` if the [gmaps.InfoWindow] associated to this marker is being shown. + bool get infoWindowShown => _infoWindowShown; + + /// Returns the [gmaps.Marker] associated to this controller. + 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, { + HtmlElement? newInfoWindowContent, + }) { + assert(_marker != null, 'Cannot `update` Marker after calling `remove`.'); + _marker!.options = options; + if (_infoWindow != null && newInfoWindowContent != null) { + _infoWindow!.content = newInfoWindowContent; + } + } + + /// Disposes of the currently wrapped [gmaps.Marker]. + void remove() { + 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(); + _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); + _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 new file mode 100644 index 000000000000..542a48bcb707 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/markers.dart @@ -0,0 +1,179 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of google_maps_flutter_web; + +/// This class manages a set of [MarkerController]s associated to a [GoogleMapController]. +class MarkersController extends GeometryController { + // A cache of [MarkerController]s indexed by their [MarkerId]. + final Map _markerIdToController; + + // The stream over which markers broadcast their events + StreamController _streamController; + + /// Initialize the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. + MarkersController({ + required StreamController stream, + }) : _streamController = stream, + _markerIdToController = Map(); + + /// Returns the cache of [MarkerController]s. Test only. + @visibleForTesting + Map get markers => _markerIdToController; + + /// Adds a set of [Marker] objects to the cache. + /// + /// Wraps each [Marker] into its corresponding [MarkerController]. + void addMarkers(Set markersToAdd) { + markersToAdd.forEach(_addMarker); + } + + void _addMarker(Marker marker) { + if (marker == null) { + return; + } + + final infoWindowOptions = _infoWindowOptionsFromMarker(marker); + gmaps.InfoWindow? gmInfoWindow; + + if (infoWindowOptions != null) { + gmInfoWindow = gmaps.InfoWindow(infoWindowOptions); + // Google Maps' JS SDK does not have a click event on the InfoWindow, so + // we make one... + if (infoWindowOptions.content is HtmlElement) { + final content = infoWindowOptions.content as HtmlElement; + content.onClick.listen((_) { + _onInfoWindowTap(marker.markerId); + }); + } + } + + final currentMarker = _markerIdToController[marker.markerId]?.marker; + + final populationOptions = _markerOptionsFromMarker(marker, currentMarker); + gmaps.Marker gmMarker = gmaps.Marker(populationOptions); + gmMarker.map = googleMap; + MarkerController controller = MarkerController( + marker: gmMarker, + infoWindow: gmInfoWindow, + consumeTapEvents: marker.consumeTapEvents, + onTap: () { + this.showMarkerInfoWindow(marker.markerId); + _onMarkerTap(marker.markerId); + }, + onDragStart: (gmaps.LatLng latLng) { + _onMarkerDragStart(marker.markerId, latLng); + }, + onDrag: (gmaps.LatLng latLng) { + _onMarkerDrag(marker.markerId, latLng); + }, + onDragEnd: (gmaps.LatLng latLng) { + _onMarkerDragEnd(marker.markerId, latLng); + }, + ); + _markerIdToController[marker.markerId] = controller; + } + + /// Updates a set of [Marker] objects with new options. + void changeMarkers(Set markersToChange) { + markersToChange.forEach(_changeMarker); + } + + void _changeMarker(Marker marker) { + MarkerController? markerController = _markerIdToController[marker.markerId]; + if (markerController != null) { + final markerOptions = _markerOptionsFromMarker( + marker, + markerController.marker, + ); + final infoWindow = _infoWindowOptionsFromMarker(marker); + markerController.update( + markerOptions, + newInfoWindowContent: infoWindow?.content as HtmlElement?, + ); + } + } + + /// Removes a set of [MarkerId]s from the cache. + void removeMarkers(Set markerIdsToRemove) { + markerIdsToRemove.forEach(_removeMarker); + } + + void _removeMarker(MarkerId markerId) { + final MarkerController? markerController = _markerIdToController[markerId]; + markerController?.remove(); + _markerIdToController.remove(markerId); + } + + // InfoWindow... + + /// Shows the [InfoWindow] of a [MarkerId]. + /// + /// See also [hideMarkerInfoWindow] and [isInfoWindowShown]. + void showMarkerInfoWindow(MarkerId markerId) { + _hideAllMarkerInfoWindow(); + MarkerController? markerController = _markerIdToController[markerId]; + markerController?.showInfoWindow(); + } + + /// Hides the [InfoWindow] of a [MarkerId]. + /// + /// See also [showMarkerInfoWindow] and [isInfoWindowShown]. + void hideMarkerInfoWindow(MarkerId markerId) { + MarkerController? markerController = _markerIdToController[markerId]; + markerController?.hideInfoWindow(); + } + + /// Returns whether or not the [InfoWindow] of a [MarkerId] is shown. + /// + /// See also [showMarkerInfoWindow] and [hideMarkerInfoWindow]. + bool isInfoWindowShown(MarkerId markerId) { + MarkerController? markerController = _markerIdToController[markerId]; + return markerController?.infoWindowShown ?? false; + } + + // Handle internal events + + bool _onMarkerTap(MarkerId markerId) { + // Have you ended here on your debugging? Is this wrong? + // Comment here: https://github.com/flutter/flutter/issues/64084 + _streamController.add(MarkerTapEvent(mapId, markerId)); + return _markerIdToController[markerId]?.consumeTapEvents ?? false; + } + + void _onInfoWindowTap(MarkerId markerId) { + _streamController.add(InfoWindowTapEvent(mapId, markerId)); + } + + void _onMarkerDragStart(MarkerId markerId, gmaps.LatLng latLng) { + _streamController.add(MarkerDragStartEvent( + mapId, + _gmLatLngToLatLng(latLng), + markerId, + )); + } + + void _onMarkerDrag(MarkerId markerId, gmaps.LatLng latLng) { + _streamController.add(MarkerDragEvent( + mapId, + _gmLatLngToLatLng(latLng), + markerId, + )); + } + + void _onMarkerDragEnd(MarkerId markerId, gmaps.LatLng latLng) { + _streamController.add(MarkerDragEndEvent( + mapId, + _gmLatLngToLatLng(latLng), + 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 new file mode 100644 index 000000000000..9921d2ff3876 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygon.dart @@ -0,0 +1,50 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of google_maps_flutter_web; + +/// The `PolygonController` class wraps a [gmaps.Polygon] and its `onTap` behavior. +class PolygonController { + gmaps.Polygon? _polygon; + + final bool _consumeTapEvents; + + /// Creates a `PolygonController` that wraps a [gmaps.Polygon] object and its `onTap` behavior. + PolygonController({ + required gmaps.Polygon polygon, + bool consumeTapEvents = false, + ui.VoidCallback? onTap, + }) : _polygon = polygon, + _consumeTapEvents = consumeTapEvents { + if (onTap != null) { + polygon.onClick.listen((event) { + onTap.call(); + }); + } + } + + /// Returns the wrapped [gmaps.Polygon]. Only used for testing. + @visibleForTesting + 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) { + assert(_polygon != null, 'Cannot `update` Polygon after calling `remove`.'); + _polygon!.options = options; + } + + /// Disposes of the currently wrapped [gmaps.Polygon]. + void remove() { + 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 new file mode 100644 index 000000000000..8a9643156351 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polygons.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. + +part of google_maps_flutter_web; + +/// This class manages a set of [PolygonController]s associated to a [GoogleMapController]. +class PolygonsController extends GeometryController { + // A cache of [PolygonController]s indexed by their [PolygonId]. + final Map _polygonIdToController; + + // The stream over which polygons broadcast events + StreamController _streamController; + + /// Initializes the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. + PolygonsController({ + required StreamController stream, + }) : _streamController = stream, + _polygonIdToController = Map(); + + /// Returns the cache of [PolygonController]s. Test only. + @visibleForTesting + Map get polygons => _polygonIdToController; + + /// Adds a set of [Polygon] objects to the cache. + /// + /// Wraps each Polygon into its corresponding [PolygonController]. + void addPolygons(Set polygonsToAdd) { + if (polygonsToAdd != null) { + polygonsToAdd.forEach((polygon) { + _addPolygon(polygon); + }); + } + } + + void _addPolygon(Polygon polygon) { + if (polygon == null) { + return; + } + + final populationOptions = _polygonOptionsFromPolygon(googleMap, polygon); + gmaps.Polygon gmPolygon = gmaps.Polygon(populationOptions); + gmPolygon.map = googleMap; + PolygonController controller = PolygonController( + polygon: gmPolygon, + consumeTapEvents: polygon.consumeTapEvents, + onTap: () { + _onPolygonTap(polygon.polygonId); + }); + _polygonIdToController[polygon.polygonId] = controller; + } + + /// Updates a set of [Polygon] objects with new options. + void changePolygons(Set polygonsToChange) { + if (polygonsToChange != null) { + polygonsToChange.forEach((polygonToChange) { + _changePolygon(polygonToChange); + }); + } + } + + void _changePolygon(Polygon polygon) { + PolygonController? polygonController = + _polygonIdToController[polygon.polygonId]; + polygonController?.update(_polygonOptionsFromPolygon(googleMap, polygon)); + } + + /// Removes a set of [PolygonId]s from the cache. + void removePolygons(Set polygonIdsToRemove) { + polygonIdsToRemove.forEach((polygonId) { + final PolygonController? polygonController = + _polygonIdToController[polygonId]; + polygonController?.remove(); + _polygonIdToController.remove(polygonId); + }); + } + + // Handle internal events + bool _onPolygonTap(PolygonId polygonId) { + // Have you ended here on your debugging? Is this wrong? + // Comment here: https://github.com/flutter/flutter/issues/64084 + _streamController.add(PolygonTapEvent(mapId, polygonId)); + return _polygonIdToController[polygonId]?.consumeTapEvents ?? false; + } +} 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 new file mode 100644 index 000000000000..eb4b6d88b503 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polyline.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. + +part of google_maps_flutter_web; + +/// The `PolygonController` class wraps a [gmaps.Polyline] and its `onTap` behavior. +class PolylineController { + gmaps.Polyline? _polyline; + + final bool _consumeTapEvents; + + /// Creates a `PolylineController` that wraps a [gmaps.Polyline] object and its `onTap` behavior. + PolylineController({ + required gmaps.Polyline polyline, + bool consumeTapEvents = false, + ui.VoidCallback? onTap, + }) : _polyline = polyline, + _consumeTapEvents = consumeTapEvents { + if (onTap != null) { + polyline.onClick.listen((event) { + onTap.call(); + }); + } + } + + /// Returns the wrapped [gmaps.Polyline]. Only used for testing. + @visibleForTesting + 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) { + assert( + _polyline != null, 'Cannot `update` Polyline after calling `remove`.'); + _polyline!.options = options; + } + + /// Disposes of the currently wrapped [gmaps.Polyline]. + void remove() { + 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 new file mode 100644 index 000000000000..695b29554c04 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/polylines.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. + +part of google_maps_flutter_web; + +/// This class manages a set of [PolylinesController]s associated to a [GoogleMapController]. +class PolylinesController extends GeometryController { + // A cache of [PolylineController]s indexed by their [PolylineId]. + final Map _polylineIdToController; + + // The stream over which polylines broadcast their events + StreamController _streamController; + + /// Initializes the cache. The [StreamController] comes from the [GoogleMapController], and is shared with other controllers. + PolylinesController({ + required StreamController stream, + }) : _streamController = stream, + _polylineIdToController = Map(); + + /// Returns the cache of [PolylineContrller]s. Test only. + @visibleForTesting + Map get lines => _polylineIdToController; + + /// Adds a set of [Polyline] objects to the cache. + /// + /// Wraps each line into its corresponding [PolylineController]. + void addPolylines(Set polylinesToAdd) { + polylinesToAdd.forEach((polyline) { + _addPolyline(polyline); + }); + } + + void _addPolyline(Polyline polyline) { + if (polyline == null) { + return; + } + + final polylineOptions = _polylineOptionsFromPolyline(googleMap, polyline); + gmaps.Polyline gmPolyline = gmaps.Polyline(polylineOptions); + gmPolyline.map = googleMap; + PolylineController controller = PolylineController( + polyline: gmPolyline, + consumeTapEvents: polyline.consumeTapEvents, + onTap: () { + _onPolylineTap(polyline.polylineId); + }); + _polylineIdToController[polyline.polylineId] = controller; + } + + /// Updates a set of [Polyline] objects with new options. + void changePolylines(Set polylinesToChange) { + polylinesToChange.forEach((polylineToChange) { + _changePolyline(polylineToChange); + }); + } + + void _changePolyline(Polyline polyline) { + 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 = + _polylineIdToController[polylineId]; + polylineController?.remove(); + _polylineIdToController.remove(polylineId); + }); + } + + // Handle internal events + + bool _onPolylineTap(PolylineId polylineId) { + // Have you ended here on your debugging? Is this wrong? + // Comment here: https://github.com/flutter/flutter/issues/64084 + _streamController.add(PolylineTapEvent(mapId, polylineId)); + return _polylineIdToController[polylineId]?.consumeTapEvents ?? false; + } +} 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/third_party/to_screen_location/LICENSE b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/LICENSE new file mode 100644 index 000000000000..ab4e163abe54 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2008 Krasimir Tsonev + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/README.md b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/README.md new file mode 100644 index 000000000000..8bd4a39c065f --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/README.md @@ -0,0 +1,14 @@ +# to_screen_location + +The code in this directory is a Dart re-implementation of Krasimir Tsonev's blog +post: [GoogleMaps API v3: convert LatLng object to actual pixels][blog-post]. + +The blog post describes a way to implement the [`toScreenLocation` method][method] +of the Google Maps Platform SDK for the web. + +Used under license (MIT), [available here][blog-license], and in the accompanying +LICENSE file. + +[blog-license]: https://krasimirtsonev.com/license +[blog-post]: https://krasimirtsonev.com/blog/article/google-maps-api-v3-convert-latlng-object-to-actual-pixels-point-object +[method]: https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/Projection#toScreenLocation(com.google.android.libraries.maps.model.LatLng) diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/to_screen_location.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/to_screen_location.dart new file mode 100644 index 000000000000..2963111fdcc3 --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/third_party/to_screen_location/to_screen_location.dart @@ -0,0 +1,57 @@ +// The MIT License (MIT) +// +// Copyright (c) 2008 Krasimir Tsonev +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import 'package:google_maps/google_maps.dart' as gmaps; + +/// Returns a screen location that corresponds to a geographical coordinate ([gmaps.LatLng]). +/// +/// The screen location is in pixels relative to the top left of the Map widget +/// (not of the whole screen/app). +/// +/// See: https://developers.google.com/maps/documentation/android-sdk/reference/com/google/android/libraries/maps/Projection#public-point-toscreenlocation-latlng-location +gmaps.Point toScreenLocation(gmaps.GMap map, gmaps.LatLng coords) { + final zoom = map.zoom; + final bounds = map.bounds; + final projection = map.projection; + + assert( + bounds != null, 'Map Bounds required to compute screen x/y of LatLng.'); + assert(projection != null, + 'Map Projection required to compute screen x/y of LatLng.'); + assert(zoom != null, + 'Current map zoom level required to compute screen x/y of LatLng.'); + + final ne = bounds!.northEast; + final sw = bounds.southWest; + + final topRight = projection!.fromLatLngToPoint!(ne)!; + final bottomLeft = projection.fromLatLngToPoint!(sw)!; + + final scale = 1 << (zoom!.toInt()); // 2 ^ zoom + + final worldPoint = projection.fromLatLngToPoint!(coords)!; + + return gmaps.Point( + ((worldPoint.x! - bottomLeft.x!) * scale).toInt(), + ((worldPoint.y! - topRight.y!) * scale).toInt(), + ); +} diff --git a/packages/google_maps_flutter/google_maps_flutter_web/lib/src/types.dart b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/types.dart new file mode 100644 index 000000000000..ff980eb4c34b --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/lib/src/types.dart @@ -0,0 +1,30 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:google_maps_flutter_web/google_maps_flutter_web.dart'; +import 'package:google_maps/google_maps.dart' as gmaps; + +/// A void function that handles a [gmaps.LatLng] as a parameter. +/// +/// Similar to [ui.VoidCallback], but specific for Marker drag events. +typedef LatLngCallback = void Function(gmaps.LatLng latLng); + +/// The base class for all "geometry" group controllers. +/// +/// This lets all Geometry controllers ([MarkersController], [CirclesController], +/// [PolygonsController], [PolylinesController]) to be bound to a [gmaps.GMap] +/// instance and our internal `mapId` value. +abstract class GeometryController { + /// The GMap instance that this controller operates on. + late gmaps.GMap googleMap; + + /// The map ID for events. + late int mapId; + + /// Binds a `mapId` and the [gmaps.GMap] instance to this controller. + void bindToMap(int mapId, gmaps.GMap googleMap) { + this.mapId = mapId; + this.googleMap = 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 new file mode 100644 index 000000000000..1f5fe4d96ccc --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_web/pubspec.yaml @@ -0,0 +1,37 @@ +name: google_maps_flutter_web +description: Web platform implementation of google_maps_flutter +repository: https://github.com/flutter/plugins/tree/master/packages/google_maps_flutter/google_maps_flutter_web +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+maps%22 +version: 0.3.2 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +flutter: + plugin: + implements: google_maps_flutter + platforms: + web: + pluginClass: GoogleMapsPlugin + fileName: google_maps_flutter_web.dart + +dependencies: + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + google_maps_flutter_platform_interface: ^2.1.2 + google_maps: ^5.2.0 + meta: ^1.3.0 + sanitize_html: ^2.0.0 + stream_transform: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + pedantic: ^1.10.0 + +# The example deliberately includes limited-use secrets. +false_secrets: + - /example/web/index.html diff --git a/packages/google_maps_flutter/google_maps_flutter_web/test/README.md b/packages/google_maps_flutter/google_maps_flutter_web/test/README.md new file mode 100644 index 000000000000..7c5b4ad682ba --- /dev/null +++ b/packages/google_maps_flutter/google_maps_flutter_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/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_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/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 a0d2d470020d..6107560ce610 100644 --- a/packages/google_sign_in/google_sign_in/CHANGELOG.md +++ b/packages/google_sign_in/google_sign_in/CHANGELOG.md @@ -1,3 +1,90 @@ +## 5.1.1 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. + +## 5.1.0 + +* Add reAuthenticate option to signInSilently to allow re-authentication to be requested + +* Updated Android lint settings. + +## 5.0.7 + +* Mark iOS arm64 simulators as unsupported. + +## 5.0.6 + +* Remove references to the Android V1 embedding. + +## 5.0.5 + +* Add iOS unit and UI integration test targets. +* Add iOS unit test module map. +* Exclude arm64 simulators in example app. + +## 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. + +## 4.5.3 + +* Update package:e2e -> package:integration_test + +## 4.5.2 + +* Update package:e2e reference to use the local version in the flutter/plugins + repository. + +## 4.5.1 + +* Add note on Apple sign in requirement in README. + +## 4.5.0 + +* Add support for getting `serverAuthCode`. + ## 4.4.6 * Update lower bound of dart dependency to 2.1.0. diff --git a/packages/google_sign_in/google_sign_in/LICENSE b/packages/google_sign_in/google_sign_in/LICENSE old mode 100755 new mode 100644 index 4da9688730d1..c6823b81eb84 --- a/packages/google_sign_in/google_sign_in/LICENSE +++ b/packages/google_sign_in/google_sign_in/LICENSE @@ -1,7 +1,7 @@ -Copyright 2016, the Flutter project 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: +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. @@ -13,14 +13,13 @@ met: 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. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/google_sign_in/google_sign_in/README.md b/packages/google_sign_in/google_sign_in/README.md old mode 100755 new mode 100644 index f9e8415f0745..f3787474eeef --- a/packages/google_sign_in/google_sign_in/README.md +++ b/packages/google_sign_in/google_sign_in/README.md @@ -1,34 +1,46 @@ -# 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 + +This plugin requires iOS 9.0 or higher. + +1. [First register your application](https://firebase.google.com/docs/ios/setup). +2. Make sure the file you download in step 1 is named + `GoogleService-Info.plist`. +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,12 +61,33 @@ Make sure you've filled out all required fields in the console for [OAuth consen ``` +#### 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. + +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. + +### 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 @@ -71,6 +104,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. @@ -87,13 +121,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..ea98b315f147 --- 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 @@ -30,10 +30,26 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } } } dependencies { implementation 'com.google.android.gms:play-services-auth:16.0.1' implementation 'com.google.guava:guava:20.0' + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-inline:3.9.0' } diff --git a/packages/google_sign_in/google_sign_in/android/gradle.properties b/packages/google_sign_in/google_sign_in/android/gradle.properties deleted file mode 100755 index 38c8d4544ff1..000000000000 --- a/packages/google_sign_in/google_sign_in/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true -android.useAndroidX=true -android.enableJetifier=true diff --git a/packages/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 e8fcd29075e3..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; @@ -60,7 +60,8 @@ public class GoogleSignInPlugin implements MethodCallHandler, FlutterPlugin, Act private MethodChannel channel; private ActivityPluginBinding activityPluginBinding; - public static void registerWith(PluginRegistry.Registrar registrar) { + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { GoogleSignInPlugin instance = new GoogleSignInPlugin(); instance.initInstance(registrar.messenger(), registrar.context(), new GoogleSignInWrapper()); instance.setUpRegistrar(registrar); @@ -135,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: @@ -187,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 @@ -308,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; @@ -333,8 +343,12 @@ 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)); } for (String scope : requestedScopes) { optionsBuilder.requestScopes(new Scope(scope)); @@ -484,6 +498,7 @@ private void onSignInAccount(GoogleSignInAccount account) { response.put("email", account.getEmail()); response.put("id", account.getId()); response.put("idToken", account.getIdToken()); + response.put("serverAuthCode", account.getServerAuthCode()); response.put("displayName", account.getDisplayName()); if (account.getPhotoUrl() != null) { response.put("photoUrl", account.getPhotoUrl().toString()); 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/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java b/packages/google_sign_in/google_sign_in/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java new file mode 100644 index 000000000000..3b6ad960f548 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/android/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInTest.java @@ -0,0 +1,198 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlesignin; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import com.google.android.gms.auth.api.signin.GoogleSignInAccount; +import com.google.android.gms.common.api.Scope; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.PluginRegistry; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; + +public class GoogleSignInTest { + @Mock Context mockContext; + @Mock Activity mockActivity; + @Mock PluginRegistry.Registrar mockRegistrar; + @Mock BinaryMessenger mockMessenger; + @Spy MethodChannel.Result result; + @Mock GoogleSignInWrapper mockGoogleSignIn; + @Mock GoogleSignInAccount account; + private GoogleSignInPlugin plugin; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mockRegistrar.messenger()).thenReturn(mockMessenger); + when(mockRegistrar.context()).thenReturn(mockContext); + when(mockRegistrar.activity()).thenReturn(mockActivity); + plugin = new GoogleSignInPlugin(); + plugin.initInstance(mockRegistrar.messenger(), mockRegistrar.context(), mockGoogleSignIn); + plugin.setUpRegistrar(mockRegistrar); + } + + @Test + public void requestScopes_ResultErrorIfAccountIsNull() { + MethodCall methodCall = new MethodCall("requestScopes", null); + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null); + plugin.onMethodCall(methodCall, result); + verify(result).error("sign_in_required", "No account to grant scopes.", null); + } + + @Test + public void requestScopes_ResultTrueIfAlreadyGranted() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); + when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); + when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(true); + + plugin.onMethodCall(methodCall, result); + verify(result).success(true); + } + + @Test + public void requestScopes_RequestsPermissionIfNotGranted() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); + when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); + when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); + + plugin.onMethodCall(methodCall, result); + + verify(mockGoogleSignIn) + .requestPermissions(mockActivity, 53295, account, new Scope[] {requestedScope}); + } + + @Test + public void requestScopes_ReturnsFalseIfPermissionDenied() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockRegistrar).addActivityResultListener(captor.capture()); + PluginRegistry.ActivityResultListener listener = captor.getValue(); + + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); + when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); + when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); + + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, + Activity.RESULT_CANCELED, + new Intent()); + + verify(result).success(false); + } + + @Test + public void requestScopes_ReturnsTrueIfPermissionGranted() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockRegistrar).addActivityResultListener(captor.capture()); + PluginRegistry.ActivityResultListener listener = captor.getValue(); + + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); + when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); + when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); + + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); + + verify(result).success(true); + } + + @Test + public void requestScopes_mayBeCalledRepeatedly_ifAlreadyGranted() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockRegistrar).addActivityResultListener(captor.capture()); + PluginRegistry.ActivityResultListener listener = captor.getValue(); + + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); + when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); + when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); + + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); + + verify(result, times(2)).success(true); + } + + @Test + public void requestScopes_mayBeCalledRepeatedly_ifNotSignedIn() { + HashMap> arguments = new HashMap<>(); + arguments.put("scopes", Collections.singletonList("requestedScope")); + MethodCall methodCall = new MethodCall("requestScopes", arguments); + Scope requestedScope = new Scope("requestedScope"); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(PluginRegistry.ActivityResultListener.class); + verify(mockRegistrar).addActivityResultListener(captor.capture()); + PluginRegistry.ActivityResultListener listener = captor.getValue(); + + when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null); + + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); + plugin.onMethodCall(methodCall, result); + listener.onActivityResult( + GoogleSignInPlugin.Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); + + verify(result, times(2)).error("sign_in_required", "No account to grant scopes.", null); + } + + @Test(expected = IllegalStateException.class) + public void signInThrowsWithoutActivity() { + final GoogleSignInPlugin plugin = new GoogleSignInPlugin(); + plugin.initInstance( + mock(BinaryMessenger.class), mock(Context.class), mock(GoogleSignInWrapper.class)); + + plugin.onMethodCall(new MethodCall("signIn", null), null); + } +} diff --git a/packages/google_sign_in/google_sign_in/example/README.md b/packages/google_sign_in/google_sign_in/example/README.md 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..5d574a2c6a51 --- 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' @@ -35,6 +35,7 @@ android { applicationId "io.flutter.plugins.googlesigninexample" minSdkVersion 16 targetSdkVersion 28 + multiDexEnabled true versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -60,5 +61,7 @@ flutter { 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' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + api 'androidx.test:core:1.2.0' } diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java b/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java new file mode 100644 index 000000000000..edc01de491af --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/FlutterActivityTest.java @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlesigninexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class FlutterActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/GoogleSignInTest.java b/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/GoogleSignInTest.java new file mode 100644 index 000000000000..561d9d4e7a82 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/android/app/src/androidTest/java/io/flutter/plugins/googlesigninexample/GoogleSignInTest.java @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlesigninexample; + +import static org.junit.Assert.assertTrue; + +import androidx.test.core.app.ActivityScenario; +import io.flutter.plugins.googlesignin.GoogleSignInPlugin; +import org.junit.Test; + +public class GoogleSignInTest { + @Test + public void googleSignInPluginIsAdded() { + final ActivityScenario scenario = + ActivityScenario.launch(GoogleSignInTestActivity.class); + scenario.onActivity( + activity -> { + assertTrue(activity.engine.getPlugins().has(GoogleSignInPlugin.class)); + }); + } +} diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/debug/AndroidManifest.xml b/packages/google_sign_in/google_sign_in/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..4d764900a530 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + 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 index df80f829c1e7..22a34d7218f7 --- 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 @@ -14,12 +14,6 @@ - - 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 deleted file mode 100644 index f7ea0c4043a6..000000000000 --- a/packages/google_sign_in/google_sign_in/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.app.FlutterActivity; -import io.flutter.plugins.googlesignin.GoogleSignInPlugin; -import io.flutter.view.FlutterMain; - -public class EmbeddingV1Activity extends 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/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 deleted file mode 100644 index 8bddbff7ce27..000000000000 --- a/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/EmbeddingV1ActivityTest.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.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} 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 deleted file mode 100644 index 77cdcee9bcdb..000000000000 --- a/packages/google_sign_in/google_sign_in/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.e2e.FlutterRunner; -import io.flutter.embedding.android.FlutterActivity; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class FlutterActivityTest { - @Rule - public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); -} diff --git a/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/GoogleSignInTestActivity.java b/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/GoogleSignInTestActivity.java new file mode 100644 index 000000000000..09506a2632df --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/android/app/src/main/java/io/flutter/plugins/googlesigninexample/GoogleSignInTestActivity.java @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.googlesigninexample; + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; + +// Makes the FlutterEngine accessible for testing. +public class GoogleSignInTestActivity extends FlutterActivity { + public FlutterEngine engine; + + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + engine = flutterEngine; + } +} diff --git a/packages/google_sign_in/google_sign_in/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/main/res/values/strings.xml b/packages/google_sign_in/google_sign_in/example/android/app/src/main/res/values/strings.xml new file mode 100644 index 000000000000..c7e28ffcedd1 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + YOUR_WEB_CLIENT_ID + diff --git a/packages/google_sign_in/google_sign_in/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 deleted file mode 100644 index acc7996ee94b..000000000000 --- a/packages/google_sign_in/google_sign_in/example/android/app/src/test/java/io/flutter/plugins/googlesignin/GoogleSignInPluginTests.java +++ /dev/null @@ -1,189 +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.googlesignin; - -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import com.google.android.gms.auth.api.signin.GoogleSignInAccount; -import com.google.android.gms.common.api.Scope; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.PluginRegistry; -import io.flutter.plugin.common.PluginRegistry.ActivityResultListener; -import io.flutter.plugins.googlesignin.GoogleSignInPlugin.Delegate; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.Spy; - -public class GoogleSignInPluginTests { - - @Mock Context mockContext; - @Mock Activity mockActivity; - @Mock PluginRegistry.Registrar mockRegistrar; - @Mock BinaryMessenger mockMessenger; - @Spy MethodChannel.Result result; - @Mock GoogleSignInWrapper mockGoogleSignIn; - @Mock GoogleSignInAccount account; - private GoogleSignInPlugin plugin; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - when(mockRegistrar.messenger()).thenReturn(mockMessenger); - when(mockRegistrar.context()).thenReturn(mockContext); - when(mockRegistrar.activity()).thenReturn(mockActivity); - plugin = new GoogleSignInPlugin(); - plugin.initInstance(mockRegistrar.messenger(), mockRegistrar.context(), mockGoogleSignIn); - plugin.setUpRegistrar(mockRegistrar); - } - - @Test - public void requestScopes_ResultErrorIfAccountIsNull() { - MethodCall methodCall = new MethodCall("requestScopes", null); - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null); - plugin.onMethodCall(methodCall, result); - verify(result).error("sign_in_required", "No account to grant scopes.", null); - } - - @Test - public void requestScopes_ResultTrueIfAlreadyGranted() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - - MethodCall methodCall = new MethodCall("requestScopes", arguments); - Scope requestedScope = new Scope("requestedScope"); - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); - when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); - when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(true); - - plugin.onMethodCall(methodCall, result); - verify(result).success(true); - } - - @Test - public void requestScopes_RequestsPermissionIfNotGranted() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); - Scope requestedScope = new Scope("requestedScope"); - - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); - when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); - when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); - - plugin.onMethodCall(methodCall, result); - - verify(mockGoogleSignIn) - .requestPermissions(mockActivity, 53295, account, new Scope[] {requestedScope}); - } - - @Test - public void requestScopes_ReturnsFalseIfPermissionDenied() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); - Scope requestedScope = new Scope("requestedScope"); - - ArgumentCaptor captor = - ArgumentCaptor.forClass(ActivityResultListener.class); - verify(mockRegistrar).addActivityResultListener(captor.capture()); - ActivityResultListener listener = captor.getValue(); - - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); - when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); - when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); - - plugin.onMethodCall(methodCall, result); - listener.onActivityResult( - Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_CANCELED, new Intent()); - - verify(result).success(false); - } - - @Test - public void requestScopes_ReturnsTrueIfPermissionGranted() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); - Scope requestedScope = new Scope("requestedScope"); - - ArgumentCaptor captor = - ArgumentCaptor.forClass(ActivityResultListener.class); - verify(mockRegistrar).addActivityResultListener(captor.capture()); - ActivityResultListener listener = captor.getValue(); - - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); - when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); - when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); - - plugin.onMethodCall(methodCall, result); - listener.onActivityResult( - Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - - verify(result).success(true); - } - - @Test - public void requestScopes_mayBeCalledRepeatedly_ifAlreadyGranted() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); - Scope requestedScope = new Scope("requestedScope"); - - ArgumentCaptor captor = - ArgumentCaptor.forClass(ActivityResultListener.class); - verify(mockRegistrar).addActivityResultListener(captor.capture()); - ActivityResultListener listener = captor.getValue(); - - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(account); - when(account.getGrantedScopes()).thenReturn(Collections.singleton(requestedScope)); - when(mockGoogleSignIn.hasPermissions(account, requestedScope)).thenReturn(false); - - plugin.onMethodCall(methodCall, result); - listener.onActivityResult( - Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - plugin.onMethodCall(methodCall, result); - listener.onActivityResult( - Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - - verify(result, times(2)).success(true); - } - - @Test - public void requestScopes_mayBeCalledRepeatedly_ifNotSignedIn() { - HashMap> arguments = new HashMap<>(); - arguments.put("scopes", Collections.singletonList("requestedScope")); - MethodCall methodCall = new MethodCall("requestScopes", arguments); - Scope requestedScope = new Scope("requestedScope"); - - ArgumentCaptor captor = - ArgumentCaptor.forClass(ActivityResultListener.class); - verify(mockRegistrar).addActivityResultListener(captor.capture()); - ActivityResultListener listener = captor.getValue(); - - when(mockGoogleSignIn.getLastSignedInAccount(mockContext)).thenReturn(null); - - plugin.onMethodCall(methodCall, result); - listener.onActivityResult( - Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - plugin.onMethodCall(methodCall, result); - listener.onActivityResult( - Delegate.REQUEST_CODE_REQUEST_SCOPE, Activity.RESULT_OK, new Intent()); - - verify(result, times(2)).error("sign_in_required", "No account to grant scopes.", null); - } -} 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 index 6c2de8086bcd..3a9c234f96d4 --- 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 @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 8.0 + 9.0 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..e577a3081fe8 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/ios/Podfile @@ -0,0 +1,47 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + + pod 'OCMock','3.5' + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + target.build_configurations.each do |build_configuration| + # GoogleSignIn does not support arm64 simulators. + build_configuration.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64 i386' + end + end +end diff --git a/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.pbxproj b/packages/google_sign_in/google_sign_in/example/ios/Runner.xcodeproj/project.pbxproj index faaaa58070bd..06857ed2bd59 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; @@ -375,7 +539,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -425,7 +589,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -439,6 +603,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -449,7 +614,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; @@ -460,6 +625,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ENABLE_BITCODE = NO; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)/Flutter", @@ -470,8 +636,62 @@ "$(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; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Debug; + }; + F76AC1AA2666D0540040C8BC /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 45D93D4513839BFEA2AA74FE /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = "i386 arm64"; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Release; + }; + F76AC1B82666D0610040C8BC /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_TARGET_NAME = Runner; + }; + name = Debug; + }; + F76AC1B92666D0610040C8BC /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = RunnerUITests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_TARGET_NAME = Runner; }; name = Release; }; @@ -496,6 +716,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 @@ + + + + + + + + 1:479882132969:ios:2643f950e0a0da08 DATABASE_URL https://my-flutter-proj.firebaseio.com + SERVER_CLIENT_ID + YOUR_SERVER_CLIENT_ID \ No newline at end of file diff --git a/packages/google_sign_in/google_sign_in/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..6f8b821a5299 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/example/ios/RunnerTests/GoogleSignInTests.m @@ -0,0 +1,491 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import Flutter; + +@import XCTest; +@import google_sign_in; +@import google_sign_in.Test; +@import GoogleSignIn; + +// OCMock library doesn't generate a valid modulemap. +#import + +@interface FLTGoogleSignInPluginTest : XCTestCase + +@property(strong, nonatomic) NSObject *mockBinaryMessenger; +@property(strong, nonatomic) NSObject *mockPluginRegistrar; +@property(strong, nonatomic) FLTGoogleSignInPlugin *plugin; +@property(strong, nonatomic) id mockSignIn; + +@end + +@implementation FLTGoogleSignInPluginTest + +- (void)setUp { + [super setUp]; + self.mockBinaryMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); + self.mockPluginRegistrar = OCMProtocolMock(@protocol(FlutterPluginRegistrar)); + + id mockSignIn = OCMClassMock([GIDSignIn class]); + self.mockSignIn = mockSignIn; + + OCMStub(self.mockPluginRegistrar.messenger).andReturn(self.mockBinaryMessenger); + self.plugin = [[FLTGoogleSignInPlugin alloc] initWithSignIn:mockSignIn]; + [FLTGoogleSignInPlugin registerWithRegistrar:self.mockPluginRegistrar]; +} + +- (void)testUnimplementedMethod { + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"bogus" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(id result) { + XCTAssertEqualObjects(result, FlutterMethodNotImplemented); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testSignOut { + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signOut" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(id result) { + XCTAssertNil(result); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + OCMVerify([self.mockSignIn signOut]); +} + +- (void)testDisconnect { + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"disconnect" + arguments:nil]; + + [self.plugin handleMethodCall:methodCall + result:^(id result){ + }]; + OCMVerify([self.mockSignIn disconnect]); +} + +- (void)testClearAuthCache { + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"clearAuthCache" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(id result) { + XCTAssertNil(result); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +#pragma mark - Init + +- (void)testInitGamesSignInUnsupported { + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"init" + arguments:@{@"signInOption" : @"SignInOption.games"}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"unsupported-options"); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testInitGoogleServiceInfoPlist { + FlutterMethodCall *methodCall = [FlutterMethodCall + methodCallWithMethodName:@"init" + arguments:@{@"scopes" : @[ @"mockScope1" ], @"hostedDomain" : @"example.com"}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(id result) { + XCTAssertNil(result); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + + id mockSignIn = self.mockSignIn; + OCMVerify([mockSignIn setScopes:@[ @"mockScope1" ]]); + OCMVerify([mockSignIn setHostedDomain:@"example.com"]); + + // Set in example app GoogleService-Info.plist. + OCMVerify([mockSignIn + setClientID:@"479882132969-9i9aqik3jfjd7qhci1nqf0bm2g71rm1u.apps.googleusercontent.com"]); + OCMVerify([mockSignIn setServerClientID:@"YOUR_SERVER_CLIENT_ID"]); +} + +- (void)testInitNullDomain { + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"init" + arguments:@{@"hostedDomain" : [NSNull null]}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(id r) { + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + OCMVerify([self.mockSignIn setHostedDomain:nil]); +} + +- (void)testInitDynamicClientId { + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"init" + arguments:@{@"clientId" : @"mockClientId"}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(id r) { + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; + OCMVerify([self.mockSignIn setClientID:@"mockClientId"]); +} + +#pragma mark - Is signed in + +- (void)testIsNotSignedIn { + OCMStub([self.mockSignIn hasPreviousSignIn]).andReturn(NO); + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"isSignedIn" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(NSNumber *result) { + XCTAssertFalse(result.boolValue); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testIsSignedIn { + OCMStub([self.mockSignIn hasPreviousSignIn]).andReturn(YES); + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"isSignedIn" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(NSNumber *result) { + XCTAssertTrue(result.boolValue); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +#pragma mark - Sign in silently + +- (void)testSignInSilently { + OCMExpect([self.mockSignIn restorePreviousSignIn]); + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signInSilently" + arguments:nil]; + + [self.plugin handleMethodCall:methodCall + result:^(id result){ + }]; + OCMVerifyAll(self.mockSignIn); +} + +- (void)testSignInSilentlyFailsConcurrently { + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signInSilently" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + + OCMExpect([self.mockSignIn restorePreviousSignIn]).andDo(^(NSInvocation *invocation) { + // Simulate calling the same method while the previous one is in flight. + [self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"concurrent-requests"); + [expectation fulfill]; + }]; + }); + + [self.plugin handleMethodCall:methodCall + result:^(id result){ + }]; + + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +#pragma mark - Sign in + +- (void)testSignIn { + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signIn" + arguments:nil]; + + [self.plugin handleMethodCall:methodCall + result:^(NSNumber *result){ + }]; + + id mockSignIn = self.mockSignIn; + OCMVerify([mockSignIn + setPresentingViewController:[OCMArg isKindOfClass:[FlutterViewController class]]]); + OCMVerify([mockSignIn signIn]); +} + +- (void)testSignInExecption { + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"signIn" + arguments:nil]; + OCMExpect([self.mockSignIn signIn]) + .andThrow([NSException exceptionWithName:@"MockName" reason:@"MockReason" userInfo:nil]); + + __block FlutterError *error; + XCTAssertThrows([self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + error = result; + }]); + + XCTAssertEqualObjects(error.code, @"google_sign_in"); + XCTAssertEqualObjects(error.message, @"MockReason"); + XCTAssertEqualObjects(error.details, @"MockName"); +} + +#pragma mark - Get tokens + +- (void)testGetTokens { + id mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + + id mockAuthentication = OCMClassMock([GIDAuthentication class]); + OCMStub([mockAuthentication idToken]).andReturn(@"mockIdToken"); + OCMStub([mockAuthentication accessToken]).andReturn(@"mockAccessToken"); + [[mockAuthentication stub] + getTokensWithHandler:[OCMArg invokeBlockWithArgs:mockAuthentication, [NSNull null], nil]]; + OCMStub([mockUser authentication]).andReturn(mockAuthentication); + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(NSDictionary *result) { + XCTAssertEqualObjects(result[@"idToken"], @"mockIdToken"); + XCTAssertEqualObjects(result[@"accessToken"], @"mockAccessToken"); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testGetTokensNoAuthKeychainError { + id mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + + id mockAuthentication = OCMClassMock([GIDAuthentication class]); + NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeHasNoAuthInKeychain + userInfo:nil]; + [[mockAuthentication stub] + getTokensWithHandler:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + OCMStub([mockUser authentication]).andReturn(mockAuthentication); + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"sign_in_required"); + XCTAssertEqualObjects(result.message, kGIDSignInErrorDomain); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testGetTokensCancelledError { + id mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + + id mockAuthentication = OCMClassMock([GIDAuthentication class]); + NSError *error = [NSError errorWithDomain:kGIDSignInErrorDomain + code:kGIDSignInErrorCodeCanceled + userInfo:nil]; + [[mockAuthentication stub] + getTokensWithHandler:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + OCMStub([mockUser authentication]).andReturn(mockAuthentication); + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"sign_in_canceled"); + XCTAssertEqualObjects(result.message, kGIDSignInErrorDomain); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testGetTokensURLError { + id mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + + id mockAuthentication = OCMClassMock([GIDAuthentication class]); + NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:nil]; + [[mockAuthentication stub] + getTokensWithHandler:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + OCMStub([mockUser authentication]).andReturn(mockAuthentication); + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"network_error"); + XCTAssertEqualObjects(result.message, NSURLErrorDomain); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testGetTokensUnknownError { + id mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + + id mockAuthentication = OCMClassMock([GIDAuthentication class]); + NSError *error = [NSError errorWithDomain:@"BogusDomain" code:42 userInfo:nil]; + [[mockAuthentication stub] + getTokensWithHandler:[OCMArg invokeBlockWithArgs:[NSNull null], error, nil]]; + OCMStub([mockUser authentication]).andReturn(mockAuthentication); + + FlutterMethodCall *methodCall = [FlutterMethodCall methodCallWithMethodName:@"getTokens" + arguments:nil]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"sign_in_failed"); + XCTAssertEqualObjects(result.message, @"BogusDomain"); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +#pragma mark - Request scopes + +- (void)testRequestScopesResultErrorIfNotSignedIn { + OCMStub([self.mockSignIn currentUser]).andReturn(nil); + + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"requestScopes" + arguments:@{@"scopes" : @[ @"mockScope1" ]}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(FlutterError *result) { + XCTAssertEqualObjects(result.code, @"sign_in_required"); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testRequestScopesIfNoMissingScope { + // Mock Google Signin internal calls + GIDGoogleUser *mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + NSArray *requestedScopes = @[ @"mockScope1" ]; + OCMStub(mockUser.grantedScopes).andReturn(requestedScopes); + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"requestScopes" + arguments:@{@"scopes" : requestedScopes}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(NSNumber *result) { + XCTAssertTrue(result.boolValue); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testRequestScopesRequestsIfNotGranted { + // Mock Google Signin internal calls + GIDGoogleUser *mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + NSArray *requestedScopes = @[ @"mockScope1" ]; + OCMStub(mockUser.grantedScopes).andReturn(@[]); + id mockSignIn = self.mockSignIn; + OCMStub([mockSignIn scopes]).andReturn(@[]); + + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"requestScopes" + arguments:@{@"scopes" : requestedScopes}]; + + [self.plugin handleMethodCall:methodCall + result:^(id r){ + }]; + + OCMVerify([mockSignIn setScopes:@[ @"mockScope1" ]]); + OCMVerify([mockSignIn signIn]); +} + +- (void)testRequestScopesReturnsFalseIfNotGranted { + // Mock Google Signin internal calls + GIDGoogleUser *mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + NSArray *requestedScopes = @[ @"mockScope1" ]; + OCMStub(mockUser.grantedScopes).andReturn(@[]); + + OCMStub([self.mockSignIn signIn]).andDo(^(NSInvocation *invocation) { + [((NSObject *)self.plugin) signIn:self.mockSignIn + didSignInForUser:mockUser + withError:nil]; + }); + + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"requestScopes" + arguments:@{@"scopes" : requestedScopes}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns false"]; + [self.plugin handleMethodCall:methodCall + result:^(NSNumber *result) { + XCTAssertFalse(result.boolValue); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +- (void)testRequestScopesReturnsTrueIfGranted { + // Mock Google Signin internal calls + GIDGoogleUser *mockUser = OCMClassMock([GIDGoogleUser class]); + OCMStub([self.mockSignIn currentUser]).andReturn(mockUser); + NSArray *requestedScopes = @[ @"mockScope1" ]; + NSMutableArray *availableScopes = [NSMutableArray new]; + OCMStub(mockUser.grantedScopes).andReturn(availableScopes); + + OCMStub([self.mockSignIn signIn]).andDo(^(NSInvocation *invocation) { + [availableScopes addObject:@"mockScope1"]; + [((NSObject *)self.plugin) signIn:self.mockSignIn + didSignInForUser:mockUser + withError:nil]; + }); + + FlutterMethodCall *methodCall = + [FlutterMethodCall methodCallWithMethodName:@"requestScopes" + arguments:@{@"scopes" : requestedScopes}]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"expect result returns true"]; + [self.plugin handleMethodCall:methodCall + result:^(NSNumber *result) { + XCTAssertTrue(result.boolValue); + [expectation fulfill]; + }]; + [self waitForExpectationsWithTimeout:5.0 handler:nil]; +} + +@end diff --git a/packages/google_sign_in/google_sign_in/example/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 e3ab95e618f7..dfd942d3d438 --- a/packages/google_sign_in/google_sign_in/example/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in/example/pubspec.yaml @@ -1,22 +1,30 @@ name: google_sign_in_example description: Example of Google Sign-In plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" 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 - e2e: ^0.2.1 + espresso: ^0.1.0+2 + pedantic: ^1.10.0 + 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/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 9049fcd62a33..d13d64d2ba04 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,8 +1,10 @@ -// 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 "FLTGoogleSignInPlugin_Test.h" + #import // The key within `GoogleService-Info.plist` used to hold the application's @@ -10,6 +12,8 @@ // for more info. static NSString *const kClientIdKey = @"CLIENT_ID"; +static NSString *const kServerClientIdKey = @"SERVER_CLIENT_ID"; + // These error codes must match with ones declared on Android and Dart sides. static NSString *const kErrorReasonSignInRequired = @"sign_in_required"; static NSString *const kErrorReasonSignInCanceled = @"sign_in_canceled"; @@ -33,11 +37,15 @@ } @interface FLTGoogleSignInPlugin () +@property(strong, readonly) GIDSignIn *signIn; + +// Redeclared as not a designated initializer. +- (instancetype)init; @end @implementation FLTGoogleSignInPlugin { FlutterResult _accountRequest; - NSArray *_additionalScopesRequest; + NSArray *_additionalScopesRequest; } + (void)registerWithRegistrar:(NSObject *)registrar { @@ -50,9 +58,14 @@ + (void)registerWithRegistrar:(NSObject *)registrar { } - (instancetype)init { + return [self initWithSignIn:GIDSignIn.sharedInstance]; +} + +- (instancetype)initWithSignIn:(GIDSignIn *)signIn { self = [super init]; if (self) { - [GIDSignIn sharedInstance].delegate = self; + _signIn = signIn; + _signIn.delegate = self; // On the iOS simulator, we get "Broken pipe" errors after sign-in for some // unknown reason. We can avoid crashing the app by ignoring them. @@ -74,10 +87,23 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result NSString *path = [[NSBundle mainBundle] pathForResource:@"GoogleService-Info" ofType:@"plist"]; if (path) { - NSMutableDictionary *plist = [[NSMutableDictionary alloc] initWithContentsOfFile:path]; - [GIDSignIn sharedInstance].clientID = plist[kClientIdKey]; - [GIDSignIn sharedInstance].scopes = call.arguments[@"scopes"]; - [GIDSignIn sharedInstance].hostedDomain = call.arguments[@"hostedDomain"]; + NSMutableDictionary *plist = + [[NSMutableDictionary alloc] initWithContentsOfFile:path]; + BOOL hasDynamicClientId = [call.arguments[@"clientId"] isKindOfClass:[NSString class]]; + + if (hasDynamicClientId) { + self.signIn.clientID = call.arguments[@"clientId"]; + } else { + self.signIn.clientID = plist[kClientIdKey]; + } + + self.signIn.serverClientID = plist[kServerClientIdKey]; + self.signIn.scopes = call.arguments[@"scopes"]; + if (call.arguments[@"hostedDomain"] == [NSNull null]) { + self.signIn.hostedDomain = nil; + } else { + self.signIn.hostedDomain = call.arguments[@"hostedDomain"]; + } result(nil); } else { result([FlutterError errorWithCode:@"missing-config" @@ -87,23 +113,23 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } } else if ([call.method isEqualToString:@"signInSilently"]) { if ([self setAccountRequest:result]) { - [[GIDSignIn sharedInstance] restorePreviousSignIn]; + [self.signIn restorePreviousSignIn]; } } else if ([call.method isEqualToString:@"isSignedIn"]) { - result(@([[GIDSignIn sharedInstance] hasPreviousSignIn])); + result(@([self.signIn hasPreviousSignIn])); } else if ([call.method isEqualToString:@"signIn"]) { - [GIDSignIn sharedInstance].presentingViewController = [self topViewController]; + self.signIn.presentingViewController = [self topViewController]; if ([self setAccountRequest:result]) { @try { - [[GIDSignIn sharedInstance] signIn]; + [self.signIn signIn]; } @catch (NSException *e) { result([FlutterError errorWithCode:@"google_sign_in" message:e.reason details:e.name]); [e raise]; } } } else if ([call.method isEqualToString:@"getTokens"]) { - GIDGoogleUser *currentUser = [GIDSignIn sharedInstance].currentUser; + GIDGoogleUser *currentUser = self.signIn.currentUser; GIDAuthentication *auth = currentUser.authentication; [auth getTokensWithHandler:^void(GIDAuthentication *authentication, NSError *error) { result(error != nil ? getFlutterError(error) : @{ @@ -112,18 +138,18 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result }); }]; } else if ([call.method isEqualToString:@"signOut"]) { - [[GIDSignIn sharedInstance] signOut]; + [self.signIn signOut]; result(nil); } else if ([call.method isEqualToString:@"disconnect"]) { if ([self setAccountRequest:result]) { - [[GIDSignIn sharedInstance] disconnect]; + [self.signIn disconnect]; } } else if ([call.method isEqualToString:@"clearAuthCache"]) { // There's nothing to be done here on iOS since the expired/invalid // tokens are refreshed automatically by getTokensWithHandler. result(nil); } else if ([call.method isEqualToString:@"requestScopes"]) { - GIDGoogleUser *user = [GIDSignIn sharedInstance].currentUser; + GIDGoogleUser *user = self.signIn.currentUser; if (user == nil) { result([FlutterError errorWithCode:@"sign_in_required" message:@"No account to grant scopes." @@ -131,9 +157,9 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result return; } - NSArray *currentScopes = [GIDSignIn sharedInstance].scopes; - NSArray *scopes = call.arguments[@"scopes"]; - NSArray *missingScopes = [scopes + NSArray *currentScopes = self.signIn.scopes; + NSArray *scopes = call.arguments[@"scopes"]; + NSArray *missingScopes = [scopes filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id scope, NSDictionary *bindings) { return ![user.grantedScopes containsObject:scope]; @@ -146,12 +172,11 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result if ([self setAccountRequest:result]) { _additionalScopesRequest = missingScopes; - [GIDSignIn sharedInstance].scopes = - [currentScopes arrayByAddingObjectsFromArray:missingScopes]; - [GIDSignIn sharedInstance].presentingViewController = [self topViewController]; - [GIDSignIn sharedInstance].loginHint = user.profile.email; + self.signIn.scopes = [currentScopes arrayByAddingObjectsFromArray:missingScopes]; + self.signIn.presentingViewController = [self topViewController]; + self.signIn.loginHint = user.profile.email; @try { - [[GIDSignIn sharedInstance] signIn]; + [self.signIn signIn]; } @catch (NSException *e) { result([FlutterError errorWithCode:@"request_scopes" message:e.reason details:e.name]); } @@ -172,8 +197,10 @@ - (BOOL)setAccountRequest:(FlutterResult)request { return YES; } -- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary *)options { - return [[GIDSignIn sharedInstance] handleURL:url]; +- (BOOL)application:(UIApplication *)app + openURL:(NSURL *)url + options:(NSDictionary *)options { + return [self.signIn handleURL:url]; } #pragma mark - protocol @@ -221,6 +248,7 @@ - (void)signIn:(GIDSignIn *)signIn @"email" : user.profile.email ?: [NSNull null], @"id" : user.userID ?: [NSNull null], @"photoUrl" : [photoUrl absoluteString] ?: [NSNull null], + @"serverAuthCode" : user.serverAuthCode ?: [NSNull null] } error:nil]; } @@ -235,7 +263,7 @@ - (void)signIn:(GIDSignIn *)signIn #pragma mark - private methods -- (void)respondWithAccount:(id)account error:(NSError *)error { +- (void)respondWithAccount:(NSDictionary *)account error:(NSError *)error { FlutterResult result = _accountRequest; _accountRequest = nil; result(error != nil ? getFlutterError(error) : account); diff --git a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.modulemap b/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.modulemap new file mode 100644 index 000000000000..271f509e7fd7 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin.modulemap @@ -0,0 +1,10 @@ +framework module google_sign_in { + umbrella header "google_sign_in-umbrella.h" + + export * + module * { export * } + + explicit module Test { + header "FLTGoogleSignInPlugin_Test.h" + } +} diff --git a/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin_Test.h b/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin_Test.h new file mode 100644 index 000000000000..8fa6cf348018 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/ios/Classes/FLTGoogleSignInPlugin_Test.h @@ -0,0 +1,17 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This header is available in the Test module. Import via "@import google_sign_in.Test;" + +#import + +@class GIDSignIn; + +/// Methods exposed for unit testing. +@interface FLTGoogleSignInPlugin () + +/// Inject @c GIDSignIn for testing. +- (instancetype)initWithSignIn:(GIDSignIn *)signIn NS_DESIGNATED_INITIALIZER; + +@end diff --git a/packages/google_sign_in/google_sign_in/ios/Classes/google_sign_in-umbrella.h b/packages/google_sign_in/google_sign_in/ios/Classes/google_sign_in-umbrella.h new file mode 100644 index 000000000000..343c390f1782 --- /dev/null +++ b/packages/google_sign_in/google_sign_in/ios/Classes/google_sign_in-umbrella.h @@ -0,0 +1,9 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +FOUNDATION_EXPORT double google_sign_inVersionNumber; +FOUNDATION_EXPORT const unsigned char google_sign_inVersionString[]; diff --git a/packages/google_sign_in/google_sign_in/ios/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..270ca274f3e4 --- 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 @@ -12,17 +12,15 @@ Enables Google Sign-In in Flutter apps. s.license = { :type => 'BSD', :file => '../LICENSE' } s.author = { 'Flutter Team' => 'flutter-dev@googlegroups.com' } s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/google_sign_in' } - s.source_files = 'Classes/**/*' + s.source_files = 'Classes/**/*.{h,m}' s.public_header_files = 'Classes/**/*.h' + s.module_map = 'Classes/FLTGoogleSignInPlugin.modulemap' s.dependency 'Flutter' s.dependency 'GoogleSignIn', '~> 5.0' s.static_framework = true - s.platform = :ios, '8.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } + s.platform = :ios, '9.0' - s.test_spec 'Tests' do |test_spec| - test_spec.source_files = 'Tests/**/*' - test_spec.dependency 'OCMock','3.5' - end + # GoogleSignIn ~> 5.0 does not support arm64 simulators. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64' } end diff --git a/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart b/packages/google_sign_in/google_sign_in/lib/google_sign_in.dart index 7402c7a69816..04d60fbc7d21 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,10 +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; @override String toString() => 'GoogleSignInAuthentication:$_data'; @@ -54,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; @@ -63,9 +66,9 @@ class GoogleSignInAccount implements GoogleIdentity { final String id; @override - final String photoUrl; + final String? photoUrl; - final String _idToken; + final String? _idToken; final GoogleSignIn _googleSignIn; /// Retrieve [GoogleSignInAuthentication] for this account. @@ -102,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", }; } @@ -114,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); } @@ -171,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, @@ -209,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(); @@ -234,7 +239,7 @@ class GoogleSignIn { : null); } - GoogleSignInAccount _setCurrentUser(GoogleSignInAccount currentUser) { + GoogleSignInAccount? _setCurrentUser(GoogleSignInAccount? currentUser) { if (currentUser != _currentUser) { _currentUser = currentUser; _currentUserController.add(_currentUser); @@ -255,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. @@ -276,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. @@ -300,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. /// @@ -309,23 +314,26 @@ class GoogleSignIn { /// successful sign in or `null` if there is no previously authenticated user. /// Use [signIn] method to trigger interactive sign in process. /// - /// Authentication process is triggered only if there is no currently signed in + /// Authentication is triggered if there is no currently signed in /// user (that is when `currentUser == null`), otherwise this method returns /// a Future which resolves to the same user instance. /// - /// Re-authentication can be triggered only after [signOut] or [disconnect]. + /// Re-authentication can be triggered after [signOut] or [disconnect]. It can + /// also be triggered by setting [reAuthenticate] to `true` if a new ID token + /// is required. /// /// When [suppressErrors] is set to `false` and an error occurred during sign in /// returned Future completes with [PlatformException] whose `code` can be /// 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, + bool reAuthenticate = false, }) async { try { return await _addMethodCall(GoogleSignInPlatform.instance.signInSilently, - canSkipCall: true); + canSkipCall: !reAuthenticate); } catch (_) { if (suppressErrors) { return null; @@ -351,8 +359,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; @@ -360,12 +368,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 e02c8ee17f11..aa0a686776fb 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.4.6 +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.1.1 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" flutter: plugin: @@ -16,26 +21,26 @@ flutter: default_package: google_sign_in_web dependencies: - google_sign_in_platform_interface: ^1.1.0 flutter: sdk: flutter - meta: ^1.0.4 - # The design on https://flutter.dev/go/federated-plugins was to leave - # this constraint as "any". We cannot do it right now as it fails pub publish - # validation, so we set a ^ constraint. - # TODO(amirh): Revisit this (either update this part in the design or the pub tool). - # https://github.com/flutter/flutter/issues/46264 - google_sign_in_web: ^0.9.1 + 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 - e2e: ^0.2.1 + http: ^0.13.0 + integration_test: + sdk: flutter + pedantic: ^1.10.0 -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4 <2.0.0" +# The example deliberately includes limited-use secrets. +false_secrets: + - /example/android/app/google-services.json + - /example/ios/Runner/GoogleService-Info.plist + - /example/ios/RunnerTests/GoogleSignInTests.m + - /example/lib/main.dart + - /example/web/index.html diff --git a/packages/google_sign_in/google_sign_in/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_e2e.dart b/packages/google_sign_in/google_sign_in/test/google_sign_in_e2e.dart deleted file mode 100644 index 0c6431f37bf4..000000000000 --- a/packages/google_sign_in/google_sign_in/test/google_sign_in_e2e.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:e2e/e2e.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:google_sign_in/google_sign_in.dart'; - -void main() { - E2EWidgetsFlutterBinding.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/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 898c27fd9f7e..444edc4336ce --- 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. @@ -36,12 +36,13 @@ void main() { 'getTokens': { 'idToken': '123', 'accessToken': '456', + 'serverAuthCode': '789', }, }; final List log = []; - Map responses; - GoogleSignIn googleSignIn; + late Map responses; + late GoogleSignIn googleSignIn; setUp(() { responses = Map.from(kDefaultResponses); @@ -63,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( @@ -83,6 +94,7 @@ void main() { 'signInOption': 'SignInOption.standard', 'scopes': [], 'hostedDomain': null, + 'clientId': fakeClientId, }), isMethodCall('signIn', arguments: null), ], @@ -93,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), ]); }); @@ -108,11 +116,7 @@ void main() { expect( log, [ - isMethodCall('init', arguments: { - 'signInOption': 'SignInOption.standard', - 'scopes': [], - 'hostedDomain': null, - }), + _isSignInMethodCall(), isMethodCall('disconnect', arguments: null), ], ); @@ -125,11 +129,7 @@ void main() { expect( log, [ - isMethodCall('init', arguments: { - 'signInOption': 'SignInOption.standard', - 'scopes': [], - 'hostedDomain': null, - }), + _isSignInMethodCall(), isMethodCall('disconnect', arguments: null), ], ); @@ -139,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), ]); }); @@ -151,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), ], @@ -170,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), ], ); @@ -203,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), ], @@ -215,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), ], ); @@ -245,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(), @@ -256,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), @@ -270,8 +246,8 @@ void main() { }); test('queue of many concurrent calls', () async { - final List> futures = - >[ + final List> futures = + >[ googleSignIn.signInSilently(), googleSignIn.signOut(), googleSignIn.signIn(), @@ -281,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), @@ -309,6 +281,22 @@ void main() { throwsA(isInstanceOf())); }); + test('signInSilently allows re-authentication to be requested', () async { + await googleSignIn.signInSilently(); + expect(googleSignIn.currentUser, isNotNull); + + await googleSignIn.signInSilently(reAuthenticate: true); + + expect( + log, + [ + _isSignInMethodCall(), + isMethodCall('signInSilently', arguments: null), + isMethodCall('signInSilently', arguments: null), + ], + ); + }); + test('can sign in after init failed before', () async { int initCount = 0; channel.setMockMethodCallHandler((MethodCall methodCall) { @@ -332,11 +320,7 @@ void main() { expect( log, [ - isMethodCall('init', arguments: { - 'signInOption': 'SignInOption.standard', - 'scopes': [], - 'hostedDomain': null, - }), + _isSignInMethodCall(), isMethodCall('signInSilently', arguments: null), ], ); @@ -351,11 +335,7 @@ void main() { expect( log, [ - isMethodCall('init', arguments: { - 'signInOption': 'SignInOption.games', - 'scopes': [], - 'hostedDomain': null, - }), + _isSignInMethodCall(signInOption: 'SignInOption.games'), isMethodCall('signInSilently', arguments: null), ], ); @@ -365,11 +345,12 @@ 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'); expect(auth.idToken, '123'); + expect(auth.serverAuthCode, '789'); expect( log, [ @@ -389,11 +370,7 @@ void main() { expect( log, [ - isMethodCall('init', arguments: { - 'signInOption': 'SignInOption.standard', - 'scopes': [], - 'hostedDomain': null, - }), + _isSignInMethodCall(), isMethodCall('signIn', arguments: null), isMethodCall('requestScopes', arguments: { 'scopes': ['testScope'], @@ -411,11 +388,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(); @@ -428,7 +405,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)); @@ -445,3 +422,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/google_sign_in_e2e_test.dart b/packages/google_sign_in/google_sign_in/test_driver/google_sign_in_e2e_test.dart deleted file mode 100644 index 9f1704fc4003..000000000000 --- a/packages/google_sign_in/google_sign_in/test_driver/google_sign_in_e2e_test.dart +++ /dev/null @@ -1,15 +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:io'; -import 'package:flutter_driver/flutter_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); -} 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..917864173f7d 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,19 @@ +## 2.1.0 + +* Add serverAuthCode attribute to user data + +## 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 c89293372cf3..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,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/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..42038879e90b 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,28 @@ abstract class GoogleSignInPlatform { /// /// See: /// https://developers.google.com/identity/sign-in/web/reference#gapiauth2initparams - Future init( - {@required String hostedDomain, - List scopes, - SignInOption signInOption, - String clientId}) async { + Future init({ + List scopes = const [], + SignInOption signInOption = SignInOption.standard, + String? hostedDomain, + String? clientId, + }) async { throw UnimplementedError('init() has not been implemented.'); } /// Attempts to reuse pre-existing credentials to sign in again, without user interaction. - Future signInSilently() async { + Future signInSilently() async { throw UnimplementedError('signInSilently() has not been implemented.'); } /// Signs in the user with the options specified to [init]. - Future signIn() async { + Future signIn() async { throw UnimplementedError('signIn() has not been implemented.'); } /// Returns the Tokens used to authenticate other API calls. Future getTokens( - {@required String email, bool shouldRecoverAuth}) async { + {required String email, bool? shouldRecoverAuth}) async { throw UnimplementedError('getTokens() has not been implemented.'); } @@ -118,7 +119,7 @@ abstract class GoogleSignInPlatform { } /// Clears any cached information that the plugin may be holding on to. - Future clearAuthCache({@required String token}) async { + Future clearAuthCache({required String token}) async { throw UnimplementedError('clearAuthCache() has not been implemented.'); } diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/method_channel_google_sign_in.dart index 4d2a34fe0fe7..23c35ac240b9 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,27 +20,29 @@ 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); @@ -48,12 +50,12 @@ class MethodChannelGoogleSignIn extends GoogleSignInPlatform { @override Future getTokens( - {String email, bool shouldRecoverAuth = true}) { + {required String email, bool? shouldRecoverAuth = true}) { return channel .invokeMapMethod('getTokens', { 'email': email, 'shouldRecoverAuth': shouldRecoverAuth, - }).then(getTokenDataFromMap); + }).then((result) => getTokenDataFromMap(result!)); } @override @@ -67,23 +69,23 @@ class MethodChannelGoogleSignIn extends GoogleSignInPlatform { } @override - Future isSignedIn() { - return channel.invokeMethod('isSignedIn'); + Future isSignedIn() async { + return (await channel.invokeMethod('isSignedIn'))!; } @override - Future clearAuthCache({String token}) { + Future clearAuthCache({String? token}) { return channel.invokeMethod( 'clearAuthCache', - {'token': token}, + {'token': token}, ); } @override - Future requestScopes(List scopes) { - return channel.invokeMethod( + Future requestScopes(List scopes) async { + return (await channel.invokeMethod( 'requestScopes', >{'scopes': scopes}, - ); + ))!; } } diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/types.dart index c60402200bdd..e30966f87598 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,20 @@ enum SignInOption { /// Holds information about the signed in user. class GoogleSignInUserData { - /// Uses the given data to construct an instance. Any of these parameters - /// could be null. - GoogleSignInUserData( - {this.displayName, this.email, this.id, this.photoUrl, this.idToken}); + /// Uses the given data to construct an instance. + GoogleSignInUserData({ + required this.email, + required this.id, + this.displayName, + this.photoUrl, + this.idToken, + this.serverAuthCode, + }); /// The display name of the signed in user. /// /// Not guaranteed to be present for all users, even when configured. - String displayName; + String? displayName; /// The email address of the signed in user. /// @@ -56,15 +61,18 @@ class GoogleSignInUserData { /// The photo url of the signed in user if the user has a profile picture. /// /// Not guaranteed to be present for all users, even when configured. - String photoUrl; + String? photoUrl; /// A token that can be sent to your own server to verify the authentication /// data. - String idToken; + String? idToken; + + /// Server auth code used to access Google Login + String? serverAuthCode; @override - int get hashCode => - hashObjects([displayName, email, id, photoUrl, idToken]); + int get hashCode => hashObjects( + [displayName, email, id, photoUrl, idToken, serverAuthCode]); @override bool operator ==(dynamic other) { @@ -75,13 +83,14 @@ class GoogleSignInUserData { otherUserData.email == email && otherUserData.id == id && otherUserData.photoUrl == photoUrl && - otherUserData.idToken == idToken; + otherUserData.idToken == idToken && + otherUserData.serverAuthCode == serverAuthCode; } } /// Holds authentication data after sign in. class GoogleSignInTokenData { - /// Either or both parameters may be null. + /// Build `GoogleSignInTokenData`. GoogleSignInTokenData({ this.idToken, this.accessToken, @@ -89,13 +98,13 @@ class GoogleSignInTokenData { }); /// An OpenID Connect ID token for the authenticated user. - String idToken; + String? idToken; /// The OAuth2 access token used to access Google services. - String accessToken; + String? accessToken; /// Server auth code used to access Google Login - String serverAuthCode; + String? serverAuthCode; @override int get hashCode => hash3(idToken, accessToken, serverAuthCode); diff --git a/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart b/packages/google_sign_in/google_sign_in_platform_interface/lib/src/utils.dart index 1ae828604af6..0d89835fb498 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,27 +1,25 @@ -// 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']); + idToken: data['idToken'], + serverAuthCode: data['serverAuthCode']); } /// 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..aa41039cdf5a 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.1.0 + +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..a3450b6f3fb9 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. @@ -7,15 +7,18 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; void main() { + // Store the initial instance before any tests change it. + final GoogleSignInPlatform initialInstance = GoogleSignInPlatform.instance; + group('$GoogleSignInPlatform', () { test('$MethodChannelGoogleSignIn is the default instance', () { - expect(GoogleSignInPlatform.instance, isA()); + expect(initialInstance, isA()); }); test('Cannot be implemented with `implements`', () { expect(() { GoogleSignInPlatform.instance = ImplementsGoogleSignInPlatform(); - }, throwsAssertionError); + }, throwsA(isA())); }); test('Can be extended', () { @@ -23,14 +26,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..fcb443f84293 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. @@ -13,6 +13,8 @@ const Map kUserData = { "id": "8162538176523816253123", "photoUrl": "https://lh5.googleusercontent.com/photo.jpg", "displayName": "John Doe", + 'idToken': '123', + 'serverAuthCode': '789', }; const Map kTokenData = { @@ -29,10 +31,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 +46,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 +102,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 74d5c8bbdb37..556f69524026 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,39 @@ +## 0.10.0+3 + +* Updated URL to the `google_sign_in` package in README. + +## 0.10.0+2 + +* Add `implements` to pubspec. + +## 0.10.0+1 + +* Updated installation instructions in README. + +## 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()`. +* Add two new JS-interop types to be able to unwrap JS errors in release mode. +* Align the fields of the thrown PlatformExceptions with the mobile version. +* Migrate tests to run with `flutter drive` + +## 0.9.1+2 + +* Update package:e2e reference to use the local version in the flutter/plugins + repository. + +## 0.9.1+1 + +* Remove Android folder from `google_sign_in_web`. + ## 0.9.1 * Ensure the web code returns `null` when the user is not signed in, instead of a `null-object` User. Fixes [issue 52338](https://github.com/flutter/flutter/issues/52338). diff --git a/packages/google_sign_in/google_sign_in_web/LICENSE b/packages/google_sign_in/google_sign_in_web/LICENSE index 4da9688730d1..c6823b81eb84 100644 --- a/packages/google_sign_in/google_sign_in_web/LICENSE +++ b/packages/google_sign_in/google_sign_in_web/LICENSE @@ -1,7 +1,7 @@ -Copyright 2016, the Flutter project 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: +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. @@ -13,14 +13,13 @@ met: 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. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/google_sign_in/google_sign_in_web/README.md b/packages/google_sign_in/google_sign_in_web/README.md index 930212c17558..4ee1a2956b45 100644 --- a/packages/google_sign_in/google_sign_in_web/README.md +++ b/packages/google_sign_in/google_sign_in_web/README.md @@ -1,23 +1,14 @@ -# google_sign_in_web +# google\_sign\_in\_web -The web implementation of [google_sign_in](https://pub.dev/google_sign_in/google_sign_in) +The web implementation of [google_sign_in](https://pub.dev/packages/google_sign_in) ## Usage ### Import the package -This package is the endorsed implementation of `google_sign_in` for the web platform since version `4.1.0`, so it gets automatically added to your dependencies by depending on `google_sign_in: ^4.1.0`. - -No modifications to your pubspec.yaml should be required in a recent enough version of Flutter (`>=1.12.13+hotfix.4`): - -```yaml -... -dependencies: - ... - google_sign_in: ^4.1.0 - ... -... -``` +This package is [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin), +which means you can simply use `google_sign_in` +normally. This package will be automatically included in your app when you do. ### Web integration @@ -96,13 +87,9 @@ 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. -In order to run tests in this package, do: - -``` -flutter test --platform chrome -j1 -``` +**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. Contributions to this package are welcome. Read the [Contributing to Flutter Plugins](https://github.com/flutter/plugins/blob/master/CONTRIBUTING.md) guide to get started. diff --git a/packages/google_sign_in/google_sign_in_web/android/build.gradle b/packages/google_sign_in/google_sign_in_web/android/build.gradle deleted file mode 100644 index c69092f76006..000000000000 --- a/packages/google_sign_in/google_sign_in_web/android/build.gradle +++ /dev/null @@ -1,33 +0,0 @@ -group 'io.flutter.plugins.google_sign_in_web' -version '1.0' - -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - } -} - -rootProject.allprojects { - repositories { - google() - jcenter() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 28 - - defaultConfig { - minSdkVersion 16 - } - lintOptions { - disable 'InvalidPackage' - } -} diff --git a/packages/google_sign_in/google_sign_in_web/android/gradle.properties b/packages/google_sign_in/google_sign_in_web/android/gradle.properties deleted file mode 100644 index 7be3d8b46841..000000000000 --- a/packages/google_sign_in/google_sign_in_web/android/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true diff --git a/packages/google_sign_in/google_sign_in_web/android/gradle/wrapper/gradle-wrapper.properties b/packages/google_sign_in/google_sign_in_web/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 019065d1d650..000000000000 --- a/packages/google_sign_in/google_sign_in_web/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/google_sign_in_web/android/settings.gradle b/packages/google_sign_in/google_sign_in_web/android/settings.gradle deleted file mode 100644 index 240e74e2cd99..000000000000 --- a/packages/google_sign_in/google_sign_in_web/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'google_sign_in_web' diff --git a/packages/google_sign_in/google_sign_in_web/android/src/main/AndroidManifest.xml b/packages/google_sign_in/google_sign_in_web/android/src/main/AndroidManifest.xml deleted file mode 100644 index 9921be43a405..000000000000 --- a/packages/google_sign_in/google_sign_in_web/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/packages/google_sign_in/google_sign_in_web/android/src/main/java/io/flutter/plugins/google_sign_in_web/GoogleSignInWebPlugin.java b/packages/google_sign_in/google_sign_in_web/android/src/main/java/io/flutter/plugins/google_sign_in_web/GoogleSignInWebPlugin.java deleted file mode 100644 index 3b4fdcd69b21..000000000000 --- a/packages/google_sign_in/google_sign_in_web/android/src/main/java/io/flutter/plugins/google_sign_in_web/GoogleSignInWebPlugin.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.flutter.plugins.google_sign_in_web; - -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.PluginRegistry.Registrar; - -/** GoogleSignInWebPlugin */ -public class GoogleSignInWebPlugin implements FlutterPlugin { - @Override - public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) {} - - // This static function is optional and equivalent to onAttachedToEngine. It supports the old - // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting - // plugin registration via this function while apps migrate to use the new Android APIs - // post-flutter-1.12 via https://flutter.dev/go/android-project-migration. - // - // It is encouraged to share logic between onAttachedToEngine and registerWith to keep - // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called - // depending on the user's project. onAttachedToEngine or registerWith must both be defined - // in the same class. - public static void registerWith(Registrar registrar) {} - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) {} -} 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..8a6e74b107ea --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/README.md @@ -0,0 +1,9 @@ +# Testing + +This package uses `package:integration_test` to run its tests in a web browser. + +See [Plugin Tests > Web Tests](https://github.com/flutter/flutter/wiki/Plugin-Tests#web-tests) +in the Flutter wiki for instructions to setup and run the tests in this package. + +Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) +for more info. \ No newline at end of file diff --git a/packages/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/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/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/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/example/integration_test/gapi_mocks/src/auth2_init.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/auth2_init.dart new file mode 100644 index 000000000000..2a085ccf3588 --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/gapi_mocks/src/auth2_init.dart @@ -0,0 +1,107 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +part of gapi_mocks; + +// JS mock of a gapi.auth2, with a successfully identified user +String auth2InitSuccess(GoogleSignInUserData userData) => testIife(''' +${gapi()} + +var mockUser = ${googleUser(userData)}; + +function GapiAuth2() {} +GapiAuth2.prototype.init = function (initOptions) { + return { + then: (onSuccess, onError) => { + window.setTimeout(() => { + onSuccess(window.gapi.auth2); + }, 30); + }, + currentUser: { + listen: (cb) => { + window.setTimeout(() => { + cb(mockUser); + }, 30); + } + } + } +}; + +GapiAuth2.prototype.getAuthInstance = function () { + return { + signIn: () => { + return new Promise((resolve, reject) => { + window.setTimeout(() => { + resolve(mockUser); + }, 30); + }); + }, + currentUser: { + get: () => mockUser, + }, + } +}; + +window.gapi.auth2 = new GapiAuth2(); +'''); + +String auth2InitError() => testIife(''' +${gapi()} + +function GapiAuth2() {} +GapiAuth2.prototype.init = function (initOptions) { + return { + then: (onSuccess, onError) => { + window.setTimeout(() => { + onError({ + error: 'idpiframe_initialization_failed', + details: 'This error was raised from a test.', + }); + }, 30); + } + } +}; + +window.gapi.auth2 = new GapiAuth2(); +'''); + +String auth2SignInError([String error = 'popup_closed_by_user']) => testIife(''' +${gapi()} + +var mockUser = null; + +function GapiAuth2() {} +GapiAuth2.prototype.init = function (initOptions) { + return { + then: (onSuccess, onError) => { + window.setTimeout(() => { + onSuccess(window.gapi.auth2); + }, 30); + }, + currentUser: { + listen: (cb) => { + window.setTimeout(() => { + cb(mockUser); + }, 30); + } + } + } +}; + +GapiAuth2.prototype.getAuthInstance = function () { + return { + signIn: () => { + return new Promise((resolve, reject) => { + window.setTimeout(() => { + reject({ + error: '${error}' + }); + }, 30); + }); + }, + } +}; + +window.gapi.auth2 = new GapiAuth2(); +'''); diff --git a/packages/google_sign_in/google_sign_in_web/test/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/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/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/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/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/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/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/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/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/example/integration_test/src/test_utils.dart b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/test_utils.dart new file mode 100644 index 000000000000..89f9b55f3ddf --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/integration_test/src/test_utils.dart @@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; + +String toBase64Url(String contents) { + // Open the file + return 'data:text/javascript;base64,' + base64.encode(utf8.encode(contents)); +} diff --git a/packages/google_sign_in/google_sign_in_web/example/lib/main.dart b/packages/google_sign_in/google_sign_in_web/example/lib/main.dart new file mode 100644 index 000000000000..10415204570c --- /dev/null +++ b/packages/google_sign_in/google_sign_in_web/example/lib/main.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/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/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 bb43ba100c5d..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(); @@ -105,59 +106,71 @@ class GoogleSignInPlugin extends GoogleSignInPlatform { // state of the authentication, i.e: if you logout elsewhere... isAuthInitialized.complete(); - }), allowInterop((dynamic reason) { + }), allowInterop((auth2.GoogleAuthInitFailureError reason) { // onError - throw PlatformException( - code: 'google_sign_in', - message: reason.error, - details: reason.details, - ); + isAuthInitialized.completeError(PlatformException( + code: reason.error, + message: reason.details, + details: + 'https://developers.google.com/identity/sign-in/web/reference#error_codes', + )); })); - return null; + return _isAuthInitialized; } @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; - - return gapiUserToPluginUserData(await auth2.getAuthInstance().signIn()); + try { + return gapiUserToPluginUserData(await auth2.getAuthInstance()?.signIn()); + } on auth2.GoogleAuthSignInError catch (reason) { + throw PlatformException( + code: reason.error, + message: 'Exception raised from GoogleAuth.signIn()', + details: + 'https://developers.google.com/identity/sign-in/web/reference#error_codes_2', + ); + } } @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(); } @@ -165,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 @@ -185,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 e05bedf3af1e..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,21 +10,55 @@ 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 String get details; + external set details(String? value); +} + +@anonymous +@JS() +class GoogleAuthSignInError { + external String get error; + external set error(String value); +} + +@anonymous +@JS() +class OfflineAccessResponse { + external String? get code; + external set code(String? value); +} + // Module gapi.auth2 /// GoogleAuth is a singleton class that provides methods to allow the user to sign in with a Google account, /// get the user's current sign-in status, get specific data from the user's Google profile, /// request additional scopes, and sign out from the current account. -@JS("gapi.auth2.GoogleAuth") +@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. external dynamic then(dynamic onInit(GoogleAuth googleAuth), - [dynamic onFailure(dynamic /*{error: string, details: string}*/ reason)]); + [dynamic onFailure(GoogleAuthInitFailureError reason)]); /// Signs out all accounts from the application. external dynamic signOut(); @@ -51,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)); } } @@ -99,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 @@ -143,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, @@ -161,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 @@ -263,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, @@ -331,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]); @@ -374,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 90dca0e13303..723dbe9ce56f 100644 --- a/packages/google_sign_in/google_sign_in_web/pubspec.yaml +++ b/packages/google_sign_in/google_sign_in_web/pubspec.yaml @@ -1,32 +1,32 @@ 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.1 +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+3 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: + implements: google_sign_in platforms: web: pluginClass: GoogleSignInPlugin 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 - -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 new file mode 100644 index 000000000000..7c5b4ad682ba --- /dev/null +++ b/packages/google_sign_in/google_sign_in_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/google_sign_in/google_sign_in_web/test/auth2_test.dart b/packages/google_sign_in/google_sign_in_web/test/auth2_test.dart deleted file mode 100644 index 40bc8a404d06..000000000000 --- a/packages/google_sign_in/google_sign_in_web/test/auth2_test.dart +++ /dev/null @@ -1,83 +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 '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 'utils.dart'; - -void main() { - 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, - ); - - // The pre-configured use case for the instances of the plugin in this test - gapiUrl = toBase64Url(gapi_mocks.auth2InitSuccess(expectedUserData)); - - GoogleSignInPlugin plugin; - - setUp(() { - plugin = GoogleSignInPlugin(); - }); - - test('Init requires clientId', () async { - expect(plugin.init(hostedDomain: ''), throwsAssertionError); - }); - - test('Init doesn\'t accept spaces in scopes', () 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; - }); - - test('signInSilently', () async { - GoogleSignInUserData actualUser = await plugin.signInSilently(); - - expect(actualUser, expectedUserData); - }); - - test('signIn', () async { - GoogleSignInUserData actualUser = await plugin.signIn(); - - expect(actualUser, expectedUserData); - }); - - test('getTokens', () async { - GoogleSignInTokenData actualToken = - await plugin.getTokens(email: expectedUserData.email); - - expect(actualToken, expectedTokenData); - }); - - test('requestScopes', () async { - bool scopeGranted = await plugin.requestScopes(['newScope']); - - expect(scopeGranted, isTrue); - }); - }); -} diff --git a/packages/google_sign_in/google_sign_in_web/test/gapi_load_test.dart b/packages/google_sign_in/google_sign_in_web/test/gapi_load_test.dart deleted file mode 100644 index 6703beca2cad..000000000000 --- a/packages/google_sign_in/google_sign_in_web/test/gapi_load_test.dart +++ /dev/null @@ -1,44 +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: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 'gapi_mocks/gapi_mocks.dart' as gapi_mocks; -import 'utils.dart'; - -void main() { - gapiUrl = toBase64Url(gapi_mocks.auth2InitSuccess(GoogleSignInUserData())); - - test('Plugin is initialized after GAPI fully loads and init is called', - () 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/gapi_mocks/src/auth2_init.dart b/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/auth2_init.dart deleted file mode 100644 index 1846033151c1..000000000000 --- a/packages/google_sign_in/google_sign_in_web/test/gapi_mocks/src/auth2_init.dart +++ /dev/null @@ -1,47 +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. - -part of gapi_mocks; - -// JS mock of a gapi.auth2, with a successfully identified user -String auth2InitSuccess(GoogleSignInUserData userData) => testIife(''' -${gapi()} - -var mockUser = ${googleUser(userData)}; - -function GapiAuth2() {} -GapiAuth2.prototype.init = function (initOptions) { - return { - then: (onSuccess, onError) => { - window.setTimeout(() => { - onSuccess(window.gapi.auth2); - }, 30); - }, - currentUser: { - listen: (cb) => { - window.setTimeout(() => { - cb(mockUser); - }, 30); - } - } - } -}; - -GapiAuth2.prototype.getAuthInstance = function () { - return { - signIn: () => { - return new Promise((resolve, reject) => { - window.setTimeout(() => { - resolve(mockUser); - }, 30); - }); - }, - currentUser: { - get: () => mockUser, - }, - } -}; - -window.gapi.auth2 = new GapiAuth2(); -'''); diff --git a/packages/google_sign_in/google_sign_in_web/test/gapi_utils_test.dart b/packages/google_sign_in/google_sign_in_web/test/gapi_utils_test.dart deleted file mode 100644 index 2dc49fc5b67a..000000000000 --- a/packages/google_sign_in/google_sign_in_web/test/gapi_utils_test.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. -@TestOn('browser') - -import 'package:flutter_test/flutter_test.dart'; - -import 'package:google_sign_in_web/src/generated/gapiauth2.dart' as gapi; -import 'package:google_sign_in_web/src/utils.dart'; -import 'package:mockito/mockito.dart'; - -class MockGoogleUser extends Mock implements gapi.GoogleUser {} - -class MockBasicProfile extends Mock implements gapi.BasicProfile {} - -void main() { - // The non-null use cases are covered by the auth2_test.dart file. - - group('gapiUserToPluginUserData', () { - var mockUser; - - setUp(() { - mockUser = MockGoogleUser(); - }); - - test('null user -> null response', () { - expect(gapiUserToPluginUserData(null), isNull); - }); - - test('not signed-in user -> null response', () { - when(mockUser.isSignedIn()).thenReturn(false); - expect(gapiUserToPluginUserData(mockUser), isNull); - }); - - test('signed-in, but null profile user -> null response', () { - when(mockUser.isSignedIn()).thenReturn(true); - expect(gapiUserToPluginUserData(mockUser), isNull); - }); - - test('signed-in, null userId in profile user -> null response', () { - 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/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/utils.dart b/packages/google_sign_in/google_sign_in_web/test/utils.dart deleted file mode 100644 index 5a6c8906682c..000000000000 --- a/packages/google_sign_in/google_sign_in_web/test/utils.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. - -import 'dart:convert'; - -String toBase64Url(String contents) { - // Open the file - return 'data:text/javascript;base64,' + base64.encode(utf8.encode(contents)); -} 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 75af68133df8..4c89be1c3e48 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,247 @@ +## 0.8.4+2 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. + +## 0.8.4+1 + +* Fix README Example for `ImagePickerCache` to cache multiple files. + +## 0.8.4 + +* Update `ImagePickerCache` to cache multiple files. + +## 0.8.3+3 + +* Fix pickImage not returning a value on iOS when dismissing PHPicker sheet by swiping. +* Updated Android lint settings. + +## 0.8.3+2 + +* Fix using Camera as image source on Android 11+ + +## 0.8.3+1 + +* Fixed README Example. + +## 0.8.3 + +* Move `ImagePickerFromLimitedGalleryUITests` to `RunnerUITests` target. +* Improved handling of bad image data when applying metadata changes on iOS. + +## 0.8.2 + +* Added new methods that return `package:cross_file` `XFile` instances. [Docs](https://pub.dev/documentation/cross_file/latest/index.html). +* Deprecate methods that return `PickedFile` instances: + * `getImage`: use **`pickImage`** instead. + * `getVideo`: use **`pickVideo`** instead. + * `getMultiImage`: use **`pickMultiImage`** instead. + * `getLostData`: use **`retrieveLostData`** instead. + +## 0.8.1+4 + +* Fixes an issue where `preferredCameraDevice` option is not working for `getVideo` method. +* Refactor unit tests that were device-only before. + +## 0.8.1+3 + +* Fix image picker causing a crash when the cache directory is deleted. + +## 0.8.1+2 + +* Update the example app to support the multi-image feature. + +## 0.8.1+1 + +* Expose errors thrown in `pickImage` and `pickVideo` docs. + +## 0.8.1 + +* Add a new method `getMultiImage` to allow picking multiple images on iOS 14 or higher +and Android 4.3 or higher. Returns only 1 image for lower versions of iOS and Android. +* Known issue: On Android, `getLostData` will only get the last picked image when picking multiple images, +see: [#84634](https://github.com/flutter/flutter/issues/84634). + +## 0.8.0+4 + +* Cleaned up the README example + +## 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. + +## 0.6.7+10 + +* Updated documentation with code that does not throw an error when image is not picked. + +## 0.6.7+9 + +* Updated the ExifInterface to the AndroidX version to support more file formats; +* Update documentation of `getImage()` regarding compression support for specific image types. + +## 0.6.7+8 + +* Update documentation of getImage() about Android's disability to preference front/rear camera. + +## 0.6.7+7 + +* Updating documentation to use isEmpty check. + +## 0.6.7+6 + +* Update package:e2e -> package:integration_test + +## 0.6.7+5 + +* Update package:e2e reference to use the local version in the flutter/plugins + repository. + + +## 0.6.7+4 + +* Support iOS simulator x86_64 architecture. + +## 0.6.7+3 + +* Fixes to the example app: + * Make videos in web start muted. This allows auto-play across browsers. + * Prevent the app from disposing of video controllers too early. + +## 0.6.7+2 + +* iOS: Fixes unpresentable album/image picker if window's root view controller is already presenting other view controller. + +## 0.6.7+1 + +* Add web support to the example app. + +## 0.6.7 + +* Utilize the new platform_interface package. +* **This change marks old methods as `deprecated`. Please check the README for migration instructions to the new API.** + +## 0.6.6+5 + +* Pin the version of the platform interface to 1.0.0 until the plugin refactor +is ready to go. + +## 0.6.6+4 + +* Fix bug, sometimes double click cancel button will crash. + ## 0.6.6+3 * Update README diff --git a/packages/image_picker/image_picker/LICENSE b/packages/image_picker/image_picker/LICENSE old mode 100755 new mode 100644 index 63b955309caf..0be8bbc3e68d --- a/packages/image_picker/image_picker/LICENSE +++ b/packages/image_picker/image_picker/LICENSE @@ -1,9 +1,9 @@ image_picker -Copyright 2017, the Flutter project 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: +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. @@ -15,17 +15,16 @@ met: 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. +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- aFileChooser @@ -229,4 +228,4 @@ aFileChooser distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file + limitations under the License. diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md index aacf9cf5104f..d8f5835fd402 100755 --- a/packages/image_picker/image_picker/README.md +++ b/packages/image_picker/image_picker/README.md @@ -1,16 +1,21 @@ # 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 +This plugin requires iOS 9.0 or higher. + +Starting with version **0.8.1** the iOS implementation uses PHPicker to pick (multiple) images on iOS 14 or higher. +As a result of implementing PHPicker it becomes impossible to pick HEIC images on the iOS simulator in iOS 14+. This is a known issue. Please test this on a real device, or test with non-HEIC images until Apple solves this issue. [63426347 - Apple known issue](https://www.google.com/search?q=63426347+apple&sxsrf=ALeKk01YnTMid5S0PYvhL8GbgXJ40ZS[…]t=gws-wiz&ved=0ahUKEwjKh8XH_5HwAhWL_rsIHUmHDN8Q4dUDCA8&uact=5) + Add the following keys to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: * `NSPhotoLibraryUsageDescription` - describe why your app needs permission for the photo library. This is called _Privacy - Photo Library Usage Description_ in the visual editor. @@ -19,53 +24,33 @@ Add the following keys to your _Info.plist_ file, located in `/ios ### Android -#### API 29+ +Starting with version **0.8.1** the Android implementation support to pick (multiple) images on Android 4.3 or higher. + No configuration required - the plugin should work out of the box. -#### 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 ``` dart import 'package:image_picker/image_picker.dart'; -class MyHomePage extends StatefulWidget { - @override - _MyHomePageState createState() => _MyHomePageState(); -} - -class _MyHomePageState extends State { - File _image; - - Future getImage() async { - var image = await ImagePicker.pickImage(source: ImageSource.camera); - - setState(() { - _image = image; - }); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: Text('Image Picker Example'), - ), - body: Center( - child: _image == null - ? Text('No image selected.') - : Image.file(_image), - ), - floatingActionButton: FloatingActionButton( - onPressed: getImage, - tooltip: 'Pick Image', - child: Icon(Icons.add_a_photo), - ), - ); - } -} + ... + final ImagePicker _picker = ImagePicker(); + // Pick an image + final XFile? image = await _picker.pickImage(source: ImageSource.gallery); + // Capture a photo + final XFile? photo = await _picker.pickImage(source: ImageSource.camera); + // Pick a video + final XFile? image = await _picker.pickVideo(source: ImageSource.gallery); + // Capture a video + final XFile? photo = await _picker.pickVideo(source: ImageSource.camera); + // Pick multiple images + final List? images = await _picker.pickMultiImage(); + ... ``` ### Handling MainActivity destruction on Android @@ -73,20 +58,16 @@ class _MyHomePageState extends State { Android system -- although very rarely -- sometimes kills the MainActivity after the image_picker finishes. When this happens, we lost the data selected from the image_picker. You can use `retrieveLostData` to retrieve the lost data in this situation. For example: ```dart -Future retrieveLostData() async { +Future getLostData() async { final LostDataResponse response = - await ImagePicker.retrieveLostData(); - if (response == null) { + await picker.retrieveLostData(); + if (response.isEmpty) { return; } - if (response.file != null) { - setState(() { - if (response.type == RetrieveType.video) { - _handleVideo(response.file); - } else { - _handleImage(response.file); - } - }); + if (response.files != null) { + for(final XFile file in response.files) { + _handleFile(file); + } } else { _handleError(response.exception); } @@ -94,3 +75,16 @@ Future retrieveLostData() async { ``` There's no way to detect when this happens, so calling this method at the right place is essential. We recommend to wire this into some kind of start up check. Please refer to the example app to see how we used it. + +## Migrating to 0.8.2+ + +Starting with version **0.8.2** of the image_picker plugin, new methods have been added for picking files that return `XFile` instances (from the [cross_file](https://pub.dev/packages/cross_file) package) rather than the plugin's own `PickedFile` instances. While the previous methods still exist, it is already recommended to start migrating over to their new equivalents. Eventually, `PickedFile` and the methods that return instances of it will be deprecated and removed. + +#### Call the new methods + +| Old API | New API | +|---------|---------| +| `PickedFile image = await _picker.getImage(...)` | `XFile image = await _picker.pickImage(...)` | +| `List images = await _picker.getMultiImage(...)` | `List images = await _picker.pickMultiImage(...)` | +| `PickedFile video = await _picker.getVideo(...)` | `XFile video = await _picker.pickVideo(...)` | +| `LostData response = await _picker.getLostData()` | `LostDataResponse response = await _picker.retrieveLostData()` | \ No newline at end of file diff --git a/packages/image_picker/image_picker/android/build.gradle b/packages/image_picker/image_picker/android/build.gradle index 19f14a286a61..1e6439e6a4eb 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 @@ -33,9 +30,33 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } dependencies { implementation 'androidx.core:core:1.0.2' implementation 'androidx.annotation:annotation:1.0.0' + implementation 'androidx.exifinterface:exifinterface:1.3.0' + + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-core:3.10.0' + testImplementation 'androidx.test:core:1.2.0' + testImplementation "org.robolectric:robolectric:4.3.1" + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } } } diff --git a/packages/image_picker/image_picker/android/gradle.properties b/packages/image_picker/image_picker/android/gradle.properties deleted file mode 100755 index 8bd86f680510..000000000000 --- a/packages/image_picker/image_picker/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M 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 fd7db57e96cc..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,11 +1,11 @@ -// 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. package io.flutter.plugins.imagepicker; -import android.media.ExifInterface; import android.util.Log; +import androidx.exifinterface.media.ExifInterface; import java.util.Arrays; import java.util.List; 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..983dbabf66c3 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. @@ -10,12 +10,16 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import io.flutter.plugin.common.MethodCall; +import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; class ImagePickerCache { static final String MAP_KEY_PATH = "path"; + static final String MAP_KEY_PATH_LIST = "pathList"; static final String MAP_KEY_MAX_WIDTH = "maxWidth"; static final String MAP_KEY_MAX_HEIGHT = "maxHeight"; static final String MAP_KEY_IMAGE_QUALITY = "imageQuality"; @@ -50,7 +54,8 @@ class ImagePickerCache { } void saveTypeWithMethodCallName(String methodCallName) { - if (methodCallName.equals(ImagePickerPlugin.METHOD_CALL_IMAGE)) { + if (methodCallName.equals(ImagePickerPlugin.METHOD_CALL_IMAGE) + | methodCallName.equals(ImagePickerPlugin.METHOD_CALL_MULTI_IMAGE)) { setType("image"); } else if (methodCallName.equals(ImagePickerPlugin.METHOD_CALL_VIDEO)) { setType("video"); @@ -99,11 +104,13 @@ String retrievePendingCameraMediaUriPath() { } void saveResult( - @Nullable String path, @Nullable String errorCode, @Nullable String errorMessage) { + @Nullable ArrayList path, @Nullable String errorCode, @Nullable String errorMessage) { + Set imageSet = new HashSet<>(); + imageSet.addAll(path); SharedPreferences.Editor editor = prefs.edit(); if (path != null) { - editor.putString(FLUTTER_IMAGE_PICKER_IMAGE_PATH_KEY, path); + editor.putStringSet(FLUTTER_IMAGE_PICKER_IMAGE_PATH_KEY, imageSet); } if (errorCode != null) { editor.putString(SHARED_PREFERENCE_ERROR_CODE_KEY, errorCode); @@ -121,12 +128,17 @@ void clear() { Map getCacheMap() { Map resultMap = new HashMap<>(); + ArrayList pathList = new ArrayList<>(); boolean hasData = false; if (prefs.contains(FLUTTER_IMAGE_PICKER_IMAGE_PATH_KEY)) { - final String imagePathValue = prefs.getString(FLUTTER_IMAGE_PICKER_IMAGE_PATH_KEY, ""); - resultMap.put(MAP_KEY_PATH, imagePathValue); - hasData = true; + final Set imagePathList = + prefs.getStringSet(FLUTTER_IMAGE_PICKER_IMAGE_PATH_KEY, null); + if (imagePathList != null) { + pathList.addAll(imagePathList); + resultMap.put(MAP_KEY_PATH_LIST, pathList); + hasData = true; + } } if (prefs.contains(SHARED_PREFERENCE_ERROR_CODE_KEY)) { @@ -159,7 +171,6 @@ Map getCacheMap() { resultMap.put(MAP_KEY_IMAGE_QUALITY, 100); } } - return resultMap; } } 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..a60c1f173041 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. @@ -6,6 +6,7 @@ import android.Manifest; import android.app.Activity; +import android.content.ActivityNotFoundException; import android.content.Intent; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; @@ -22,6 +23,7 @@ import io.flutter.plugin.common.PluginRegistry; import java.io.File; import java.io.IOException; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.UUID; @@ -42,10 +44,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,21 +76,19 @@ 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_MULTI_IMAGE_FROM_GALLERY = 2346; @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; - private final IntentResolver intentResolver; private final FileUriResolver fileUriResolver; private final FileUtils fileUtils; private CameraDevice cameraDevice; @@ -103,10 +101,6 @@ interface PermissionManager { boolean needRequestCameraPermission(); } - interface IntentResolver { - boolean resolveActivity(Intent intent); - } - interface FileUriResolver { Uri resolveFileProviderUriForFile(String fileProviderName, File imageFile); @@ -150,12 +144,6 @@ public boolean needRequestCameraPermission() { return ImagePickerUtils.needRequestCameraPermission(activity); } }, - new IntentResolver() { - @Override - public boolean resolveActivity(Intent intent) { - return intent.resolveActivity(activity.getPackageManager()) != null; - } - }, new FileUriResolver() { @Override public Uri resolveFileProviderUriForFile(String fileProviderName, File file) { @@ -192,7 +180,6 @@ public void onScanCompleted(String path, Uri uri) { final MethodCall methodCall, final ImagePickerCache cache, final PermissionManager permissionManager, - final IntentResolver intentResolver, final FileUriResolver fileUriResolver, final FileUtils fileUtils) { this.activity = activity; @@ -202,7 +189,6 @@ public void onScanCompleted(String path, Uri uri) { this.pendingResult = result; this.methodCall = methodCall; this.permissionManager = permissionManager; - this.intentResolver = intentResolver; this.fileUriResolver = fileUriResolver; this.fileUtils = fileUtils; this.cache = cache; @@ -231,17 +217,21 @@ void saveStateBeforeResult() { void retrieveLostImage(MethodChannel.Result result) { Map resultMap = cache.getCacheMap(); - String path = (String) resultMap.get(cache.MAP_KEY_PATH); - if (path != null) { - Double maxWidth = (Double) resultMap.get(cache.MAP_KEY_MAX_WIDTH); - Double maxHeight = (Double) resultMap.get(cache.MAP_KEY_MAX_HEIGHT); - int imageQuality = - resultMap.get(cache.MAP_KEY_IMAGE_QUALITY) == null - ? 100 - : (int) resultMap.get(cache.MAP_KEY_IMAGE_QUALITY); - - String newPath = imageResizer.resizeImageIfNeeded(path, maxWidth, maxHeight, imageQuality); - resultMap.put(cache.MAP_KEY_PATH, newPath); + ArrayList pathList = (ArrayList) resultMap.get(cache.MAP_KEY_PATH_LIST); + ArrayList newPathList = new ArrayList<>(); + if (pathList != null) { + for (String path : pathList) { + Double maxWidth = (Double) resultMap.get(cache.MAP_KEY_MAX_WIDTH); + Double maxHeight = (Double) resultMap.get(cache.MAP_KEY_MAX_HEIGHT); + int imageQuality = + resultMap.get(cache.MAP_KEY_IMAGE_QUALITY) == null + ? 100 + : (int) resultMap.get(cache.MAP_KEY_IMAGE_QUALITY); + + newPathList.add(imageResizer.resizeImageIfNeeded(path, maxWidth, maxHeight, imageQuality)); + } + resultMap.put(cache.MAP_KEY_PATH_LIST, newPathList); + resultMap.put(cache.MAP_KEY_PATH, newPathList.get(newPathList.size() - 1)); } if (resultMap.isEmpty()) { result.success(null); @@ -257,12 +247,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(); } @@ -299,13 +283,6 @@ private void launchTakeVideoWithCameraIntent() { useFrontCamera(intent); } - boolean canTakePhotos = intentResolver.resolveActivity(intent); - - if (!canTakePhotos) { - finishWithError("no_available_camera", "No cameras available for taking pictures."); - return; - } - File videoFile = createTemporaryWritableVideoFile(); pendingCameraMediaUri = Uri.parse("file:" + videoFile.getAbsolutePath()); @@ -313,7 +290,18 @@ private void launchTakeVideoWithCameraIntent() { intent.putExtra(MediaStore.EXTRA_OUTPUT, videoUri); grantUriPermissions(intent, videoUri); - activity.startActivityForResult(intent, REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA); + try { + activity.startActivityForResult(intent, REQUEST_CODE_TAKE_VIDEO_WITH_CAMERA); + } catch (ActivityNotFoundException e) { + try { + // If we can't delete the file again here, there's not really anything we can do about it. + //noinspection ResultOfMethodCallIgnored + videoFile.delete(); + } catch (SecurityException exception) { + exception.printStackTrace(); + } + finishWithError("no_available_camera", "No cameras available for taking pictures."); + } } public void chooseImageFromGallery(MethodCall methodCall, MethodChannel.Result result) { @@ -322,13 +310,16 @@ 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); + launchPickImageFromGalleryIntent(); + } + + public void chooseMultiImageFromGallery(MethodCall methodCall, MethodChannel.Result result) { + if (!setPendingMethodCallAndResult(methodCall, result)) { + finishWithAlreadyActiveError(result); return; } - launchPickImageFromGalleryIntent(); + launchMultiPickImageFromGalleryIntent(); } private void launchPickImageFromGalleryIntent() { @@ -338,6 +329,16 @@ private void launchPickImageFromGalleryIntent() { activity.startActivityForResult(pickImageIntent, REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY); } + private void launchMultiPickImageFromGalleryIntent() { + Intent pickImageIntent = new Intent(Intent.ACTION_GET_CONTENT); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + pickImageIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + } + pickImageIntent.setType("image/*"); + + activity.startActivityForResult(pickImageIntent, REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY); + } + public void takeImageWithCamera(MethodCall methodCall, MethodChannel.Result result) { if (!setPendingMethodCallAndResult(methodCall, result)) { finishWithAlreadyActiveError(result); @@ -366,13 +367,6 @@ private void launchTakeImageWithCameraIntent() { useFrontCamera(intent); } - boolean canTakePhotos = intentResolver.resolveActivity(intent); - - if (!canTakePhotos) { - finishWithError("no_available_camera", "No cameras available for taking pictures."); - return; - } - File imageFile = createTemporaryWritableImageFile(); pendingCameraMediaUri = Uri.parse("file:" + imageFile.getAbsolutePath()); @@ -380,7 +374,18 @@ private void launchTakeImageWithCameraIntent() { intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri); grantUriPermissions(intent, imageUri); - activity.startActivityForResult(intent, REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA); + try { + activity.startActivityForResult(intent, REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA); + } catch (ActivityNotFoundException e) { + try { + // If we can't delete the file again here, there's not really anything we can do about it. + //noinspection ResultOfMethodCallIgnored + imageFile.delete(); + } catch (SecurityException exception) { + exception.printStackTrace(); + } + finishWithError("no_available_camera", "No cameras available for taking pictures."); + } } private File createTemporaryWritableImageFile() { @@ -396,6 +401,7 @@ private File createTemporaryWritableFile(String suffix) { File image; try { + externalFilesDirectory.mkdirs(); image = File.createTempFile(filename, suffix, externalFilesDirectory); } catch (IOException e) { throw new RuntimeException(e); @@ -424,16 +430,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 +446,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."); @@ -470,6 +462,9 @@ public boolean onActivityResult(int requestCode, int resultCode, Intent data) { case REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY: handleChooseImageResult(resultCode, data); break; + case REQUEST_CODE_CHOOSE_MULTI_IMAGE_FROM_GALLERY: + handleChooseMultiImageResult(resultCode, data); + break; case REQUEST_CODE_TAKE_IMAGE_WITH_CAMERA: handleCaptureImageResult(resultCode); break; @@ -497,6 +492,24 @@ private void handleChooseImageResult(int resultCode, Intent data) { finishWithSuccess(null); } + private void handleChooseMultiImageResult(int resultCode, Intent intent) { + if (resultCode == Activity.RESULT_OK && intent != null) { + ArrayList paths = new ArrayList<>(); + if (intent.getClipData() != null) { + for (int i = 0; i < intent.getClipData().getItemCount(); i++) { + paths.add(fileUtils.getPathFromUri(activity, intent.getClipData().getItemAt(i).getUri())); + } + } else { + paths.add(fileUtils.getPathFromUri(activity, intent.getData())); + } + handleMultiImageResult(paths, false); + return; + } + + // User cancelled choosing a picture. + finishWithSuccess(null); + } + private void handleChooseVideoResult(int resultCode, Intent data) { if (resultCode == Activity.RESULT_OK && data != null) { String path = fileUtils.getPathFromUri(activity, data.getData()); @@ -546,26 +559,48 @@ public void onPathReady(String path) { finishWithSuccess(null); } - private void handleImageResult(String path, boolean shouldDeleteOriginalIfScaled) { + private void handleMultiImageResult( + ArrayList paths, boolean shouldDeleteOriginalIfScaled) { if (methodCall != null) { - Double maxWidth = methodCall.argument("maxWidth"); - Double maxHeight = methodCall.argument("maxHeight"); - Integer imageQuality = methodCall.argument("imageQuality"); - - String finalImagePath = - imageResizer.resizeImageIfNeeded(path, maxWidth, maxHeight, imageQuality); - - finishWithSuccess(finalImagePath); + ArrayList finalPath = new ArrayList<>(); + for (int i = 0; i < paths.size(); i++) { + String finalImagePath = getResizedImagePath(paths.get(i)); + + //delete original file if scaled + if (finalImagePath != null + && !finalImagePath.equals(paths.get(i)) + && shouldDeleteOriginalIfScaled) { + new File(paths.get(i)).delete(); + } + finalPath.add(i, finalImagePath); + } + finishWithListSuccess(finalPath); + } else { + finishWithListSuccess(paths); + } + } + private void handleImageResult(String path, boolean shouldDeleteOriginalIfScaled) { + if (methodCall != null) { + String finalImagePath = getResizedImagePath(path); //delete original file if scaled if (finalImagePath != null && !finalImagePath.equals(path) && shouldDeleteOriginalIfScaled) { new File(path).delete(); } + finishWithSuccess(finalImagePath); } else { finishWithSuccess(path); } } + private String getResizedImagePath(String path) { + Double maxWidth = methodCall.argument("maxWidth"); + Double maxHeight = methodCall.argument("maxHeight"); + Integer imageQuality = methodCall.argument("imageQuality"); + + return imageResizer.resizeImageIfNeeded(path, maxWidth, maxHeight, imageQuality); + } + private void handleVideoResult(String path) { finishWithSuccess(path); } @@ -587,13 +622,24 @@ private boolean setPendingMethodCallAndResult( private void finishWithSuccess(String imagePath) { if (pendingResult == null) { - cache.saveResult(imagePath, null, null); + ArrayList pathList = new ArrayList<>(); + pathList.add(imagePath); + cache.saveResult(pathList, null, null); return; } pendingResult.success(imagePath); clearMethodCallAndResult(); } + private void finishWithListSuccess(ArrayList imagePaths) { + if (pendingResult == null) { + cache.saveResult(imagePaths, null, null); + return; + } + pendingResult.success(imagePaths); + clearMethodCallAndResult(); + } + private void finishWithAlreadyActiveError(MethodChannel.Result result) { result.error("already_active", "Image picker is already active", null); } diff --git a/packages/image_picker/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerFileProvider.java b/packages/image_picker/image_picker/android/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 ed509fc515e8..577675bd433a 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; @@ -92,6 +91,7 @@ public void onActivityStopped(Activity activity) { } static final String METHOD_CALL_IMAGE = "pickImage"; + static final String METHOD_CALL_MULTI_IMAGE = "pickMultiImage"; static final String METHOD_CALL_VIDEO = "pickVideo"; private static final String METHOD_CALL_RETRIEVE = "retrieve"; private static final int CAMERA_DEVICE_FRONT = 1; @@ -111,7 +111,8 @@ public void onActivityStopped(Activity activity) { private Lifecycle lifecycle; private LifeCycleObserver observer; - public static void registerWith(PluginRegistry.Registrar registrar) { + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { if (registrar.activity() == null) { // If a background flutter view tries to register the plugin, there will be no activity from the registrar, // we stop the registering process immediately because the ImagePicker requires an activity. @@ -215,11 +216,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); @@ -302,6 +303,9 @@ public void onMethodCall(MethodCall call, MethodChannel.Result rawResult) { throw new IllegalArgumentException("Invalid image source: " + imageSource); } break; + case METHOD_CALL_MULTI_IMAGE: + delegate.chooseMultiImageFromGallery(call, result); + break; case METHOD_CALL_VIDEO: imageSource = call.argument("source"); switch (imageSource) { 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/android/app/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java b/packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java similarity index 81% rename from packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/FileUtilTest.java rename to packages/image_picker/image_picker/android/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/android/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/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java similarity index 98% rename from packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerCacheTest.java rename to packages/image_picker/image_picker/android/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/android/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/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java similarity index 83% rename from packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java rename to packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java index aa9b00521f53..d2ee7b0b7d61 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/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerDelegateTest.java @@ -1,16 +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.imagepicker; import static org.hamcrest.core.IsEqual.equalTo; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.Manifest; import android.app.Activity; +import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; @@ -18,9 +28,16 @@ import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import org.junit.After; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; public class ImagePickerDelegateTest { @@ -34,12 +51,12 @@ public class ImagePickerDelegateTest { @Mock MethodCall mockMethodCall; @Mock MethodChannel.Result mockResult; @Mock ImagePickerDelegate.PermissionManager mockPermissionManager; - @Mock ImagePickerDelegate.IntentResolver mockIntentResolver; @Mock FileUtils mockFileUtils; @Mock Intent mockIntent; @Mock ImagePickerCache cache; ImagePickerDelegate.FileUriResolver mockFileUriResolver; + MockedStatic mockStaticFile; private static class MockFileUriResolver implements ImagePickerDelegate.FileUriResolver { @Override @@ -57,6 +74,11 @@ public void getFullImagePath(Uri imageUri, ImagePickerDelegate.OnPathReadyListen public void setUp() { MockitoAnnotations.initMocks(this); + mockStaticFile = Mockito.mockStatic(File.class); + mockStaticFile + .when(() -> File.createTempFile(any(), any(), any())) + .thenReturn(new File("/tmpfile")); + when(mockActivity.getPackageName()).thenReturn("com.example.test"); when(mockActivity.getPackageManager()).thenReturn(mock(PackageManager.class)); @@ -80,6 +102,11 @@ public void setUp() { when(mockIntent.getData()).thenReturn(mockUri); } + @After + public void tearDown() { + mockStaticFile.close(); + } + @Test public void whenConstructed_setsCorrectFileProviderName() { ImagePickerDelegate delegate = createDelegate(); @@ -97,20 +124,15 @@ public void chooseImageFromGallery_WhenPendingResultExists_FinishesWithAlreadyAc } @Test - public void chooseImageFromGallery_WhenHasNoExternalStoragePermission_RequestsForPermission() { - when(mockPermissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE)) - .thenReturn(false); + public void chooseMultiImageFromGallery_WhenPendingResultExists_FinishesWithAlreadyActiveError() { + ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); - ImagePickerDelegate delegate = createDelegate(); - delegate.chooseImageFromGallery(mockMethodCall, mockResult); + delegate.chooseMultiImageFromGallery(mockMethodCall, mockResult); - verify(mockPermissionManager) - .askForPermission( - Manifest.permission.READ_EXTERNAL_STORAGE, - ImagePickerDelegate.REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION); + verifyFinishedWithAlreadyActiveError(); + verifyNoMoreInteractions(mockResult); } - @Test public void chooseImageFromGallery_WhenHasExternalStoragePermission_LaunchesChooseFromGalleryIntent() { when(mockPermissionManager.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE)) @@ -150,7 +172,6 @@ public void takeImageWithCamera_WhenHasNoCameraPermission_RequestsForPermission( @Test public void takeImageWithCamera_WhenCameraPermissionNotPresent_RequestsForPermission() { when(mockPermissionManager.needRequestCameraPermission()).thenReturn(false); - when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true); ImagePickerDelegate delegate = createDelegate(); delegate.takeImageWithCamera(mockMethodCall, mockResult); @@ -164,7 +185,6 @@ public void takeImageWithCamera_WhenCameraPermissionNotPresent_RequestsForPermis public void takeImageWithCamera_WhenHasCameraPermission_AndAnActivityCanHandleCameraIntent_LaunchesTakeWithCameraIntent() { when(mockPermissionManager.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(true); - when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true); ImagePickerDelegate delegate = createDelegate(); delegate.takeImageWithCamera(mockMethodCall, mockResult); @@ -178,8 +198,9 @@ public void takeImageWithCamera_WhenCameraPermissionNotPresent_RequestsForPermis public void takeImageWithCamera_WhenHasCameraPermission_AndNoActivityToHandleCameraIntent_FinishesWithNoCamerasAvailableError() { when(mockPermissionManager.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(true); - when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(false); - + doThrow(ActivityNotFoundException.class) + .when(mockActivity) + .startActivityForResult(any(Intent.class), anyInt()); ImagePickerDelegate delegate = createDelegate(); delegate.takeImageWithCamera(mockMethodCall, mockResult); @@ -189,47 +210,15 @@ 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(); - - delegate.onRequestPermissionsResult( - ImagePickerDelegate.REQUEST_EXTERNAL_IMAGE_STORAGE_PERMISSION, - new String[] {Manifest.permission.READ_EXTERNAL_STORAGE}, - new int[] {PackageManager.PERMISSION_GRANTED}); - - verify(mockActivity) - .startActivityForResult( - any(Intent.class), eq(ImagePickerDelegate.REQUEST_CODE_CHOOSE_IMAGE_FROM_GALLERY)); - } - - @Test - public void - onRequestChooseVideoPermissionsResult_WhenReadExternalStorageGranted_LaunchesChooseVideoFromGalleryIntent() { - ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); + public void takeImageWithCamera_WritesImageToCacheDirectory() { + when(mockPermissionManager.isPermissionGranted(Manifest.permission.CAMERA)).thenReturn(true); - 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 @@ -248,7 +237,6 @@ public void onRequestPermissionsResult_WhenCameraPermissionDenied_FinishesWithEr @Test public void onRequestTakeVideoPermissionsResult_WhenCameraPermissionGranted_LaunchesTakeVideoWithCameraIntent() { - when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true); ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); delegate.onRequestPermissionsResult( @@ -264,7 +252,6 @@ public void onRequestPermissionsResult_WhenCameraPermissionDenied_FinishesWithEr @Test public void onRequestTakeImagePermissionsResult_WhenCameraPermissionGranted_LaunchesTakeWithCameraIntent() { - when(mockIntentResolver.resolveActivity(any(Intent.class))).thenReturn(true); ImagePickerDelegate delegate = createDelegateWithPendingResultAndMethodCall(); delegate.onRequestPermissionsResult( @@ -387,16 +374,43 @@ public void onActivityResult_WhenImageTakenWithCamera_AndNoResizeNeeded_Finishes verifyNoMoreInteractions(mockResult); } + @Test + public void + retrieveLostImage_ShouldBeAbleToReturnLastItemFromResultMapWhenSingleFileIsRecovered() { + Map resultMap = new HashMap<>(); + ArrayList pathList = new ArrayList<>(); + pathList.add("/example/first_item"); + pathList.add("/example/last_item"); + resultMap.put("pathList", pathList); + + when(mockImageResizer.resizeImageIfNeeded(pathList.get(0), null, null, 100)) + .thenReturn(pathList.get(0)); + when(mockImageResizer.resizeImageIfNeeded(pathList.get(1), null, null, 100)) + .thenReturn(pathList.get(1)); + when(cache.getCacheMap()).thenReturn(resultMap); + + MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + + ImagePickerDelegate mockDelegate = createDelegate(); + + ArgumentCaptor> valueCapture = ArgumentCaptor.forClass(Map.class); + + doNothing().when(mockResult).success(valueCapture.capture()); + + mockDelegate.retrieveLostImage(mockResult); + + assertEquals("/example/last_item", valueCapture.getValue().get("path")); + } + private ImagePickerDelegate createDelegate() { return new ImagePickerDelegate( mockActivity, - null, + new File("/image_picker_cache"), mockImageResizer, null, null, cache, mockPermissionManager, - mockIntentResolver, mockFileUriResolver, mockFileUtils); } @@ -404,13 +418,12 @@ private ImagePickerDelegate createDelegate() { private ImagePickerDelegate createDelegateWithPendingResultAndMethodCall() { return new ImagePickerDelegate( mockActivity, - null, + new File("/image_picker_cache"), mockImageResizer, mockResult, mockMethodCall, cache, mockPermissionManager, - mockIntentResolver, mockFileUriResolver, mockFileUtils); } 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/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java similarity index 80% rename from packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java rename to packages/image_picker/image_picker/android/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java index 59ae23d562bb..422b8be74f7c 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/android/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,7 +18,7 @@ import android.app.Application; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.PluginRegistry; +import java.io.File; import java.util.HashMap; import java.util.Map; import org.junit.Before; @@ -25,11 +32,15 @@ public class ImagePickerPluginTest { private static final int SOURCE_CAMERA = 0; private static final int SOURCE_GALLERY = 1; private static final String PICK_IMAGE = "pickImage"; + private static final String PICK_MULTI_IMAGE = "pickMultiImage"; private static final String PICK_VIDEO = "pickVideo"; @Rule public ExpectedException exception = ExpectedException.none(); - @Mock PluginRegistry.Registrar mockRegistrar; + @SuppressWarnings("deprecation") + @Mock + io.flutter.plugin.common.PluginRegistry.Registrar mockRegistrar; + @Mock Activity mockActivity; @Mock Application mockApplication; @Mock ImagePickerDelegate mockImagePickerDelegate; @@ -82,6 +93,14 @@ public void onMethodCall_WhenSourceIsGallery_InvokesChooseImageFromGallery() { verifyZeroInteractions(mockResult); } + @Test + public void onMethodCall_InvokesChooseMultiImageFromGallery() { + MethodCall call = buildMethodCall(PICK_MULTI_IMAGE); + plugin.onMethodCall(call, mockResult); + verify(mockImagePickerDelegate).chooseMultiImageFromGallery(eq(call), any()); + verifyZeroInteractions(mockResult); + } + @Test public void onMethodCall_WhenSourceIsCamera_InvokesTakeImageWithCamera() { MethodCall call = buildMethodCall(PICK_IMAGE, SOURCE_CAMERA); @@ -143,10 +162,28 @@ 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); return new MethodCall(method, arguments); } + + private MethodCall buildMethodCall(String method) { + return new MethodCall(method, null); + } } 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/android/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java similarity index 98% rename from packages/image_picker/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImageResizerTest.java rename to packages/image_picker/image_picker/android/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/android/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/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/packages/image_picker/image_picker/android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker similarity index 100% rename from packages/image_picker/image_picker/example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker rename to packages/image_picker/image_picker/android/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/packages/image_picker/image_picker/example/android/app/src/test/resources/pngImage.png b/packages/image_picker/image_picker/android/src/test/resources/pngImage.png similarity index 100% rename from packages/image_picker/image_picker/example/android/app/src/test/resources/pngImage.png rename to packages/image_picker/image_picker/android/src/test/resources/pngImage.png 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..f7fbaae4c9fd 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 { @@ -36,6 +36,7 @@ android { applicationId "io.flutter.plugins.imagepicker.example" minSdkVersion 16 targetSdkVersion 28 + multiDexEnabled true versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -60,9 +61,7 @@ flutter { dependencies { 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' - testImplementation 'androidx.test:core:1.2.0' - testImplementation "org.robolectric:robolectric:4.3.1" + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + api 'androidx.test:core:1.2.0' } diff --git a/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java b/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java new file mode 100644 index 000000000000..91e068fa8043 --- /dev/null +++ b/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.imagepickerexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class FlutterActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerTest.java b/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerTest.java new file mode 100644 index 000000000000..c4a1532d940c --- /dev/null +++ b/packages/image_picker/image_picker/example/android/app/src/androidTest/java/io/flutter/plugins/imagepickerexample/ImagePickerTest.java @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.imagepickerexample; + +import static org.junit.Assert.assertTrue; + +import androidx.test.core.app.ActivityScenario; +import io.flutter.plugins.imagepicker.ImagePickerPlugin; +import org.junit.Test; + +public class ImagePickerTest { + @Test + public void imagePickerPluginIsAdded() { + final ActivityScenario scenario = + ActivityScenario.launch(ImagePickerTestActivity.class); + scenario.onActivity( + activity -> { + assertTrue(activity.engine.getPlugins().has(ImagePickerPlugin.class)); + }); + } +} diff --git a/packages/image_picker/image_picker/example/android/app/src/debug/AndroidManifest.xml b/packages/image_picker/image_picker/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..6f85cefded34 --- /dev/null +++ b/packages/image_picker/image_picker/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/packages/image_picker/image_picker/example/android/app/src/main/AndroidManifest.xml b/packages/image_picker/image_picker/example/android/app/src/main/AndroidManifest.xml index 597abd9b81ab..543fca922e1b 100755 --- a/packages/image_picker/image_picker/example/android/app/src/main/AndroidManifest.xml +++ b/packages/image_picker/image_picker/example/android/app/src/main/AndroidManifest.xml @@ -14,13 +14,6 @@ - - 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 deleted file mode 100644 index ddda89ca2e58..000000000000 --- a/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1Activity.java +++ /dev/null @@ -1,21 +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.imagepickerexample; - -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.imagepicker.ImagePickerPlugin; -import io.flutter.plugins.videoplayer.VideoPlayerPlugin; - -public class EmbeddingV1Activity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - ImagePickerPlugin.registerWith( - registrarFor("io.flutter.plugins.imagepicker.ImagePickerPlugin")); - VideoPlayerPlugin.registerWith( - registrarFor("io.flutter.plugins.videoplayer.VideoPlayerPlugin")); - } -} 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 deleted file mode 100644 index 924cf2fdb93c..000000000000 --- a/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1ActivityTest.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.imagepickerexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} 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 deleted file mode 100644 index df9794c8748f..000000000000 --- a/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.flutter.plugins.imagepickerexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import io.flutter.embedding.android.FlutterActivity; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class FlutterActivityTest { - @Rule - public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); -} diff --git a/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/ImagePickerTestActivity.java b/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/ImagePickerTestActivity.java new file mode 100644 index 000000000000..827687a10e79 --- /dev/null +++ b/packages/image_picker/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/ImagePickerTestActivity.java @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.imagepickerexample; + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; + +// Makes the FlutterEngine accessible for testing. +public class ImagePickerTestActivity extends FlutterActivity { + public FlutterEngine engine; + + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + engine = flutterEngine; + } +} diff --git a/packages/image_picker/image_picker/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/image_picker_test.dart b/packages/image_picker/image_picker/example/integration_test/image_picker_test.dart new file mode 100644 index 000000000000..2b82b4bda5e4 --- /dev/null +++ b/packages/image_picker/image_picker/example/integration_test/image_picker_test.dart @@ -0,0 +1,12 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('placeholder test', (WidgetTester tester) async {}); +} diff --git a/packages/image_picker/image_picker/example/ios/Flutter/AppFrameworkInfo.plist b/packages/image_picker/image_picker/example/ios/Flutter/AppFrameworkInfo.plist index 6c2de8086bcd..3a9c234f96d4 100755 --- a/packages/image_picker/image_picker/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/image_picker/image_picker/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 8.0 + 9.0 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..8979c25fea5e --- /dev/null +++ b/packages/image_picker/image_picker/example/ios/Podfile @@ -0,0 +1,48 @@ +# 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 + 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..192962839b24 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,37 @@ 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 */; }; + BE6173D826A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = BE6173D726A958B800D0974D /* 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; @@ -50,18 +60,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 +91,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; }; + BE6173D726A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImagePickerFromLimitedGalleryUITests.m; sourceTree = ""; }; + BE7AEE7026403C46006181AA /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + BE7AEE7826403CC8006181AA /* ImagePickerFromLimitedGalleryUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ImagePickerFromLimitedGalleryUITests.m; sourceTree = ""; }; + DC6FCAAD4E7580C9B3C2E21D /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; EC32F6993F4529982D9519F1 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - F78AF3172342D9D7008449C7 /* ImagePickerTestImages.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; 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; }; @@ -99,18 +130,18 @@ /* 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 +154,25 @@ path = TestImages; sourceTree = ""; }; + 6801C8372555D726009DAF8D /* RunnerUITests */ = { + isa = PBXGroup; + children = ( + BE6173D726A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m */, + 6801C8382555D726009DAF8D /* ImagePickerFromGalleryUITests.m */, + 6801C83A2555D726009DAF8D /* Info.plist */, + ); + path = RunnerUITests; + sourceTree = ""; + }; 840012C8B5EDBCF56B0E4AC1 /* Pods */ = { isa = PBXGroup; children = ( 6801632E632668F4349764C9 /* Pods-Runner.debug.xcconfig */, 5A9D31B91557877A0E8EF3E7 /* Pods-Runner.release.xcconfig */, + 15BE72415096DFE5D077E563 /* Pods-RunnerUITests.debug.xcconfig */, + 515A7EC9B4C971C01E672CF8 /* Pods-RunnerUITests.release.xcconfig */, + DC6FCAAD4E7580C9B3C2E21D /* Pods-RunnerTests.debug.xcconfig */, + 0C7B151765FD4249454C49AD /* Pods-RunnerTests.release.xcconfig */, ); name = Pods; sourceTree = ""; @@ -149,7 +194,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 +207,8 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, - 680049172280D368006DD6AB /* image_picker_exampleTests.xctest */, + 6801C8362555D726009DAF8D /* RunnerUITests.xctest */, + 334733F22668136400DCC49E /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -189,10 +237,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 +259,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 +307,6 @@ 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, - 95BB15E9E1769C0D146AA592 /* [CP] Embed Pods Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, ); buildRules = ( @@ -248,10 +326,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; }; @@ -279,17 +362,24 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, - 680049162280D368006DD6AB /* image_picker_exampleTests */, + 334733F12668136400DCC49E /* RunnerTests */, + 6801C8352555D726009DAF8D /* RunnerUITests */, ); }; /* 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 */, @@ -324,22 +414,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 +468,49 @@ 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 */, + BE6173D826A958B800D0974D /* ImagePickerFromLimitedGalleryUITests.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -402,10 +527,15 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 6800491D2280D368006DD6AB /* PBXTargetDependency */ = { + 334733F82668136400DCC49E /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 6800491C2280D368006DD6AB /* PBXContainerItemProxy */; + targetProxy = 334733F72668136400DCC49E /* PBXContainerItemProxy */; + }; + 6801C83C2555D726009DAF8D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 6801C83B2555D726009DAF8D /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ @@ -429,60 +559,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; @@ -529,7 +684,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -539,7 +694,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -580,7 +734,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -605,7 +759,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,7 +781,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.imagePickerExample; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.imagePickerExample; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; @@ -635,11 +789,20 @@ /* 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 = ( + 334733FA2668136400DCC49E /* Debug */, + 334733FB2668136400DCC49E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6801C83F2555D726009DAF8D /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { isa = XCConfigurationList; buildConfigurations = ( - 6800491F2280D368006DD6AB /* Debug */, - 680049202280D368006DD6AB /* Release */, + 6801C83D2555D726009DAF8D /* Debug */, + 6801C83E2555D726009DAF8D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; 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/example/ios/RunnerTests/ImagePickerPluginTests.m b/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m new file mode 100644 index 000000000000..cc901f084071 --- /dev/null +++ b/packages/image_picker/image_picker/example/ios/RunnerTests/ImagePickerPluginTests.m @@ -0,0 +1,258 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "ImagePickerTestImages.h" + +@import image_picker; +@import XCTest; +#import + +@interface MockViewController : UIViewController +@property(nonatomic, retain) UIViewController *mockPresented; +@end + +@implementation MockViewController +@synthesize mockPresented; + +- (UIViewController *)presentedViewController { + return mockPresented; +} + +@end + +@interface FLTImagePickerPlugin (Test) +@property(copy, nonatomic) FlutterResult result; +- (void)handleSavedPathList:(NSMutableArray *)pathList; +- (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker; +@end + +@interface ImagePickerPluginTests : XCTestCase +@property(readonly, nonatomic) id mockUIImagePicker; +@property(readonly, nonatomic) id mockAVCaptureDevice; +@end + +@implementation ImagePickerPluginTests + +- (void)setUp { + _mockUIImagePicker = OCMClassMock([UIImagePickerController class]); + _mockAVCaptureDevice = OCMClassMock([AVCaptureDevice class]); +} + +- (void)testPluginPickImageDeviceBack { + // UIImagePickerControllerSourceTypeCamera is supported + OCMStub(ClassMethod( + [_mockUIImagePicker isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])) + .andReturn(YES); + + // UIImagePickerControllerCameraDeviceRear is supported + OCMStub(ClassMethod( + [_mockUIImagePicker isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceRear])) + .andReturn(YES); + + // AVAuthorizationStatusAuthorized is supported + OCMStub([_mockAVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) + .andReturn(AVAuthorizationStatusAuthorized); + + // Run test + FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"pickImage" + arguments:@{@"source" : @(0), @"cameraDevice" : @(0)}]; + [plugin handleMethodCall:call + result:^(id _Nullable r){ + }]; + + XCTAssertEqual([plugin getImagePickerController].cameraDevice, + UIImagePickerControllerCameraDeviceRear); +} + +- (void)testPluginPickImageDeviceFront { + // UIImagePickerControllerSourceTypeCamera is supported + OCMStub(ClassMethod( + [_mockUIImagePicker isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])) + .andReturn(YES); + + // UIImagePickerControllerCameraDeviceFront is supported + OCMStub(ClassMethod([_mockUIImagePicker + isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceFront])) + .andReturn(YES); + + // AVAuthorizationStatusAuthorized is supported + OCMStub([_mockAVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) + .andReturn(AVAuthorizationStatusAuthorized); + + // Run test + FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"pickImage" + arguments:@{@"source" : @(0), @"cameraDevice" : @(1)}]; + [plugin handleMethodCall:call + result:^(id _Nullable r){ + }]; + + XCTAssertEqual([plugin getImagePickerController].cameraDevice, + UIImagePickerControllerCameraDeviceFront); +} + +- (void)testPluginPickVideoDeviceBack { + // UIImagePickerControllerSourceTypeCamera is supported + OCMStub(ClassMethod( + [_mockUIImagePicker isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])) + .andReturn(YES); + + // UIImagePickerControllerCameraDeviceRear is supported + OCMStub(ClassMethod( + [_mockUIImagePicker isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceRear])) + .andReturn(YES); + + // AVAuthorizationStatusAuthorized is supported + OCMStub([_mockAVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) + .andReturn(AVAuthorizationStatusAuthorized); + + // Run test + FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"pickVideo" + arguments:@{@"source" : @(0), @"cameraDevice" : @(0)}]; + [plugin handleMethodCall:call + result:^(id _Nullable r){ + }]; + + XCTAssertEqual([plugin getImagePickerController].cameraDevice, + UIImagePickerControllerCameraDeviceRear); +} + +- (void)testPluginPickVideoDeviceFront { + // UIImagePickerControllerSourceTypeCamera is supported + OCMStub(ClassMethod( + [_mockUIImagePicker isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera])) + .andReturn(YES); + + // UIImagePickerControllerCameraDeviceFront is supported + OCMStub(ClassMethod([_mockUIImagePicker + isCameraDeviceAvailable:UIImagePickerControllerCameraDeviceFront])) + .andReturn(YES); + + // AVAuthorizationStatusAuthorized is supported + OCMStub([_mockAVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]) + .andReturn(AVAuthorizationStatusAuthorized); + + // Run test + FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"pickVideo" + arguments:@{@"source" : @(0), @"cameraDevice" : @(1)}]; + [plugin handleMethodCall:call + result:^(id _Nullable r){ + }]; + + XCTAssertEqual([plugin getImagePickerController].cameraDevice, + UIImagePickerControllerCameraDeviceFront); +} + +#pragma mark - Test camera devices, no op on simulators + +- (void)testPluginPickImageDeviceCancelClickMultipleTimes { + if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { + return; + } + FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + FlutterMethodCall *call = + [FlutterMethodCall methodCallWithMethodName:@"pickImage" + arguments:@{@"source" : @(0), @"cameraDevice" : @(1)}]; + [plugin handleMethodCall:call + result:^(id _Nullable r){ + }]; + plugin.result = ^(id result) { + + }; + // To ensure the flow does not crash by multiple cancel call + [plugin imagePickerControllerDidCancel:[plugin getImagePickerController]]; + [plugin imagePickerControllerDidCancel:[plugin getImagePickerController]]; +} + +#pragma mark - Test video duration + +- (void)testPickingVideoWithDuration { + FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + FlutterMethodCall *call = [FlutterMethodCall + methodCallWithMethodName:@"pickVideo" + arguments:@{@"source" : @(0), @"cameraDevice" : @(0), @"maxDuration" : @95}]; + [plugin handleMethodCall:call + result:^(id _Nullable r){ + }]; + XCTAssertEqual([plugin getImagePickerController].videoMaximumDuration, 95); +} + +- (void)testViewController { + UIWindow *window = [UIWindow new]; + MockViewController *vc1 = [MockViewController new]; + window.rootViewController = vc1; + + UIViewController *vc2 = [UIViewController new]; + vc1.mockPresented = vc2; + + FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + XCTAssertEqual([plugin viewControllerWithWindow:window], vc2); +} + +- (void)testPluginMultiImagePathIsNil { + FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + + dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0); + __block FlutterError *pickImageResult = nil; + + plugin.result = ^(id _Nullable r) { + pickImageResult = r; + dispatch_semaphore_signal(resultSemaphore); + }; + [plugin handleSavedPathList:nil]; + + dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER); + + XCTAssertEqualObjects(pickImageResult.code, @"create_error"); +} + +- (void)testPluginMultiImagePathHasNullItem { + FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + NSMutableArray *pathList = [NSMutableArray new]; + + [pathList addObject:[NSNull null]]; + + dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0); + __block FlutterError *pickImageResult = nil; + + plugin.result = ^(id _Nullable r) { + pickImageResult = r; + dispatch_semaphore_signal(resultSemaphore); + }; + [plugin handleSavedPathList:pathList]; + + dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER); + + XCTAssertEqualObjects(pickImageResult.code, @"create_error"); +} + +- (void)testPluginMultiImagePathHasItem { + FLTImagePickerPlugin *plugin = [FLTImagePickerPlugin new]; + NSString *savedPath = @"test"; + NSMutableArray *pathList = [NSMutableArray new]; + + [pathList addObject:savedPath]; + + dispatch_semaphore_t resultSemaphore = dispatch_semaphore_create(0); + __block id pickImageResult = nil; + + plugin.result = ^(id _Nullable r) { + pickImageResult = r; + dispatch_semaphore_signal(resultSemaphore); + }; + [plugin handleSavedPathList:pathList]; + + dispatch_semaphore_wait(resultSemaphore, DISPATCH_TIME_FOREVER); + + XCTAssertEqual(pickImageResult, pathList); +} + +@end diff --git a/packages/image_picker/image_picker/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 88% 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..54f9469f2053 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. @@ -60,7 +60,7 @@ - (void)testWriteMetaData { NSString *tmpFile = [NSString stringWithFormat:@"image_picker_test.jpg"]; NSString *tmpDirectory = NSTemporaryDirectory(); NSString *tmpPath = [tmpDirectory stringByAppendingPathComponent:tmpFile]; - NSData *newData = [FLTImagePickerMetaDataUtil updateMetaData:metaData toImage:dataJPG]; + NSData *newData = [FLTImagePickerMetaDataUtil imageFromImage:dataJPG withMetaData:metaData]; if ([[NSFileManager defaultManager] createFileAtPath:tmpPath contents:newData attributes:nil]) { NSData *savedTmpImageData = [NSData dataWithContentsOfFile:tmpPath]; NSDictionary *tmpMetaData = @@ -71,6 +71,14 @@ - (void)testWriteMetaData { } } +- (void)testUpdateMetaDataBadData { + NSData *imageData = [NSData data]; + + NSDictionary *metaData = [FLTImagePickerMetaDataUtil getMetaDataFromImageData:imageData]; + NSData *newData = [FLTImagePickerMetaDataUtil imageFromImage:imageData withMetaData:metaData]; + XCTAssertNil(newData); +} + - (void)testConvertImageToData { UIImage *imageJPG = [UIImage imageWithData:ImagePickerTestImages.JPGTestData]; NSData *convertedDataJPG = [FLTImagePickerMetaDataUtil convertImage:imageJPG 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/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/ImagePickerFromLimitedGalleryUITests.m b/packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromLimitedGalleryUITests.m new file mode 100644 index 000000000000..802a494b0f5e --- /dev/null +++ b/packages/image_picker/image_picker/example/ios/RunnerUITests/ImagePickerFromLimitedGalleryUITests.m @@ -0,0 +1,171 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#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) { + 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 button with %@ seconds", + @(kLimitedElementWaitingTime)); + } + [limitedPhotoPermission tap]; + return YES; + }]; +} + +- (void)tearDown { + [super tearDown]; + [self.app terminate]; +} + +- (void)testSelectingFromGallery { + // Test the `Select Photos` button which is available after iOS 14. + if (@available(iOS 14, *)) { + [self launchPickerAndSelect]; + } else { + return; + } +} + +- (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/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 29781f7df449..0f5ba76db6df 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. @@ -7,9 +7,8 @@ import 'dart:async'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/src/widgets/basic.dart'; -import 'package:flutter/src/widgets/container.dart'; import 'package:image_picker/image_picker.dart'; import 'package:video_player/video_player.dart'; @@ -28,57 +27,101 @@ 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 { - File _imageFile; + List? _imageFileList; + + set _imageFile(XFile? value) { + _imageFileList = value == null ? null : [value]; + } + dynamic _pickImageError; bool isVideo = false; - VideoPlayerController _controller; - 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(File file) async { + Future _playVideo(XFile? file) async { if (file != null && mounted) { await _disposeVideoController(); - _controller = VideoPlayerController.file(file); - await _controller.setVolume(1.0); - await _controller.initialize(); - await _controller.setLooping(true); - await _controller.play(); + late VideoPlayerController controller; + if (kIsWeb) { + controller = VideoPlayerController.network(file.path); + } else { + controller = VideoPlayerController.file(File(file.path)); + } + _controller = controller; + // In web, most browsers won't honor a programmatic call to .play + // if the video has a sound track (and is not muted). + // Mute the video so it auto-plays in web! + // This is not needed if the call to .play is the result of user + // interaction (clicking on a "play" button, for example). + 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, bool isMultiImage = false}) async { if (_controller != null) { - await _controller.setVolume(0.0); + await _controller!.setVolume(0.0); } if (isVideo) { - final File file = await ImagePicker.pickVideo( + final XFile? file = await _picker.pickVideo( source: source, maxDuration: const Duration(seconds: 10)); await _playVideo(file); + } else if (isMultiImage) { + await _displayPickImageDialog(context!, + (double? maxWidth, double? maxHeight, int? quality) async { + try { + final pickedFileList = await _picker.pickMultiImage( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); + setState(() { + _imageFileList = pickedFileList; + }); + } catch (e) { + setState(() { + _pickImageError = e; + }); + } + }); } else { - await _displayPickImageDialog(context, - (double maxWidth, double maxHeight, int quality) async { + await _displayPickImageDialog(context!, + (double? maxWidth, double? maxHeight, int? quality) async { try { - _imageFile = await ImagePicker.pickImage( - source: source, - maxWidth: maxWidth, - maxHeight: maxHeight, - imageQuality: quality); - setState(() {}); + final pickedFile = await _picker.pickImage( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: quality, + ); + setState(() { + _imageFile = pickedFile; + }); } catch (e) { - _pickImageError = e; + setState(() { + _pickImageError = e; + }); } }); } @@ -87,8 +130,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(); } @@ -103,14 +146,15 @@ class _MyHomePageState extends State { } Future _disposeVideoController() async { - if (_controller != null) { - await _controller.dispose(); - _controller = null; + if (_toBeDisposed != null) { + await _toBeDisposed!.dispose(); } + _toBeDisposed = _controller; + _controller = null; } Widget _previewVideo() { - final Text retrieveError = _getRetrieveErrorWidget(); + final Text? retrieveError = _getRetrieveErrorWidget(); if (retrieveError != null) { return retrieveError; } @@ -126,13 +170,28 @@ class _MyHomePageState extends State { ); } - Widget _previewImage() { - final Text retrieveError = _getRetrieveErrorWidget(); + Widget _previewImages() { + final Text? retrieveError = _getRetrieveErrorWidget(); if (retrieveError != null) { return retrieveError; } - if (_imageFile != null) { - return Image.file(_imageFile); + if (_imageFileList != null) { + return Semantics( + child: ListView.builder( + key: UniqueKey(), + itemBuilder: (context, index) { + // Why network for web? + // See https://pub.dev/packages/image_picker#getting-ready-for-the-web-platform + return Semantics( + label: 'image_picker_example_picked_image', + child: kIsWeb + ? Image.network(_imageFileList![index].path) + : Image.file(File(_imageFileList![index].path)), + ); + }, + itemCount: _imageFileList!.length, + ), + label: 'image_picker_example_picked_images'); } else if (_pickImageError != null) { return Text( 'Pick image error: $_pickImageError', @@ -146,8 +205,16 @@ class _MyHomePageState extends State { } } + Widget _handlePreview() { + if (isVideo) { + return _previewVideo(); + } else { + return _previewImages(); + } + } + Future retrieveLostData() async { - final LostDataResponse response = await ImagePicker.retrieveLostData(); + final LostDataResponse response = await _picker.retrieveLostData(); if (response.isEmpty) { return; } @@ -159,10 +226,11 @@ class _MyHomePageState extends State { isVideo = false; setState(() { _imageFile = response.file; + _imageFileList = response.files; }); } } else { - _retrieveDataError = response.exception.code; + _retrieveDataError = response.exception!.code; } } @@ -170,10 +238,10 @@ class _MyHomePageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(widget.title), + title: Text(widget.title!), ), body: Center( - child: Platform.isAndroid + child: !kIsWeb && defaultTargetPlatform == TargetPlatform.android ? FutureBuilder( future: retrieveLostData(), builder: (BuildContext context, AsyncSnapshot snapshot) { @@ -185,7 +253,7 @@ class _MyHomePageState extends State { textAlign: TextAlign.center, ); case ConnectionState.done: - return isVideo ? _previewVideo() : _previewImage(); + return _handlePreview(); default: if (snapshot.hasError) { return Text( @@ -201,28 +269,47 @@ class _MyHomePageState extends State { } }, ) - : (isVideo ? _previewVideo() : _previewImage()), + : _handlePreview(), ), 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), + ), ), Padding( padding: const EdgeInsets.only(top: 16.0), child: FloatingActionButton( onPressed: () { isVideo = false; - _onImageButtonPressed(ImageSource.camera, context: context); + _onImageButtonPressed( + ImageSource.gallery, + context: context, + isMultiImage: true, + ); }, heroTag: 'image1', + tooltip: 'Pick Multiple Image from gallery', + child: const Icon(Icons.photo_library), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 16.0), + child: FloatingActionButton( + onPressed: () { + isVideo = false; + _onImageButtonPressed(ImageSource.camera, context: context); + }, + heroTag: 'image2', tooltip: 'Take a Photo', child: const Icon(Icons.camera_alt), ), @@ -258,9 +345,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; } @@ -297,22 +384,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); @@ -325,27 +412,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(() {}); } } @@ -353,12 +440,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(); } @@ -367,8 +454,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 d089161839bc..e11da82d5da8 100755 --- a/packages/image_picker/image_picker/example/pubspec.yaml +++ b/packages/image_picker/image_picker/example/pubspec.yaml @@ -1,24 +1,31 @@ name: image_picker_example description: Demonstrates how to use the image_picker plugin. -author: Flutter Team +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" dependencies: - video_player: ^0.10.3 + video_player: ^2.1.4 flutter: sdk: flutter + 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: ../ - flutter_plugin_android_lifecycle: ^1.0.2 dev_dependencies: + espresso: ^0.1.0+2 flutter_driver: sdk: flutter - e2e: ^0.2.1 - pedantic: ^1.8.0 + integration_test: + 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/image_picker_e2e_test.dart b/packages/image_picker/image_picker/example/test_driver/test/image_picker_e2e_test.dart deleted file mode 100644 index f3aa9e218d82..000000000000 --- a/packages/image_picker/image_picker/example/test_driver/test/image_picker_e2e_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 'dart:async'; -import 'dart:io'; -import 'package:flutter_driver/flutter_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); -} diff --git a/packages/image_picker/image_picker/example/web/favicon.png b/packages/image_picker/image_picker/example/web/favicon.png new file mode 100644 index 000000000000..8aaa46ac1ae2 Binary files /dev/null and b/packages/image_picker/image_picker/example/web/favicon.png differ diff --git a/packages/image_picker/image_picker/example/web/icons/Icon-192.png b/packages/image_picker/image_picker/example/web/icons/Icon-192.png new file mode 100644 index 000000000000..b749bfef0747 Binary files /dev/null and b/packages/image_picker/image_picker/example/web/icons/Icon-192.png differ diff --git a/packages/image_picker/image_picker/example/web/icons/Icon-512.png b/packages/image_picker/image_picker/example/web/icons/Icon-512.png new file mode 100644 index 000000000000..88cfd48dff11 Binary files /dev/null and b/packages/image_picker/image_picker/example/web/icons/Icon-512.png differ diff --git a/packages/image_picker/image_picker/example/web/index.html b/packages/image_picker/image_picker/example/web/index.html new file mode 100644 index 000000000000..b05fdf840323 --- /dev/null +++ b/packages/image_picker/image_picker/example/web/index.html @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + url_launcher web example + + + + + + + + diff --git a/packages/image_picker/image_picker/example/web/manifest.json b/packages/image_picker/image_picker/example/web/manifest.json new file mode 100644 index 000000000000..7d9c25627ebd --- /dev/null +++ b/packages/image_picker/image_picker/example/web/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "image_picker example", + "short_name": "image_picker", + "start_url": ".", + "display": "minimal-ui", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "An example of the image_picker on the web.", + "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/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..72a36a56d57d 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. @@ -27,7 +27,11 @@ extern const FLTImagePickerMIMEType kFLTImagePickerMIMETypeDefault; + (NSDictionary *)getMetaDataFromImageData:(NSData *)imageData; -+ (NSData *)updateMetaData:(NSDictionary *)metaData toImage:(NSData *)imageData; +// Creates and returns data for a new image based on imageData, but with the +// given metadata. +// +// If creating a new image fails, returns nil. ++ (nullable NSData *)imageFromImage:(NSData *)imageData withMetaData:(NSDictionary *)metadata; // Converting UIImage to a NSData with the type proveide. // diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.m b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerMetaDataUtil.m index 9786f61e1e67..45bcaa7191f7 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. @@ -49,16 +49,27 @@ + (NSDictionary *)getMetaDataFromImageData:(NSData *)imageData { return metadata; } -+ (NSData *)updateMetaData:(NSDictionary *)metaData toImage:(NSData *)imageData { - NSMutableData *mutableData = [NSMutableData data]; - CGImageSourceRef cgImage = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL); - CGImageDestinationRef destination = CGImageDestinationCreateWithData( - (__bridge CFMutableDataRef)mutableData, CGImageSourceGetType(cgImage), 1, nil); - CGImageDestinationAddImageFromSource(destination, cgImage, 0, (__bridge CFDictionaryRef)metaData); ++ (NSData *)imageFromImage:(NSData *)imageData withMetaData:(NSDictionary *)metadata { + NSMutableData *targetData = [NSMutableData data]; + CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL); + if (source == NULL) { + return nil; + } + CGImageDestinationRef destination = NULL; + CFStringRef sourceType = CGImageSourceGetType(source); + if (sourceType != NULL) { + destination = + CGImageDestinationCreateWithData((__bridge CFMutableDataRef)targetData, sourceType, 1, nil); + } + if (destination == NULL) { + CFRelease(source); + return nil; + } + CGImageDestinationAddImageFromSource(destination, source, 0, (__bridge CFDictionaryRef)metadata); CGImageDestinationFinalize(destination); - CFRelease(cgImage); + CFRelease(source); CFRelease(destination); - return mutableData; + return targetData; } + (NSData *)convertImage:(UIImage *)image 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..4c705fe54350 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 @@ -80,7 +86,11 @@ + (NSString *)saveImageWithMetaData:(NSDictionary *)metaData usingType:type quality:imageQuality]; if (metaData) { - data = [FLTImagePickerMetaDataUtil updateMetaData:metaData toImage:data]; + NSData *updatedData = [FLTImagePickerMetaDataUtil imageFromImage:data withMetaData:metaData]; + // If updating the metadata fails, just save the original. + if (updatedData) { + data = updatedData; + } } return [self createFile:data suffix:suffix]; @@ -92,11 +102,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 +115,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 38e5b56600f3..ffd23cd3df6a 100644 --- a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.h +++ b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.h @@ -1,13 +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 +#import @interface FLTImagePickerPlugin : NSObject // For testing only. -- (instancetype)initWithViewController:(UIViewController *)viewController; - (UIImagePickerController *)getImagePickerController; +- (UIViewController *)viewControllerWithWindow:(UIWindow *)window; @end diff --git a/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m b/packages/image_picker/image_picker/ios/Classes/FLTImagePickerPlugin.m index d01d0928089e..cf3103195482 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,49 +7,122 @@ #import #import #import +#import +#import #import #import "FLTImagePickerImageUtil.h" #import "FLTImagePickerMetaDataUtil.h" #import "FLTImagePickerPhotoAssetUtil.h" +#import "FLTPHPickerSaveImageToPathOperation.h" -@interface FLTImagePickerPlugin () +@interface FLTImagePickerPlugin () @property(copy, nonatomic) FlutterResult result; +@property(assign, nonatomic) int maxImagesAllowed; + +@property(copy, nonatomic) NSDictionary *arguments; + +@property(strong, nonatomic) PHPickerViewController *pickerViewController API_AVAILABLE(ios(14)); + @end static const int SOURCE_CAMERA = 0; static const int SOURCE_GALLERY = 1; +typedef NS_ENUM(NSInteger, ImagePickerClassType) { UIImagePickerClassType, PHPickerClassType }; + @implementation FLTImagePickerPlugin { - NSDictionary *_arguments; UIImagePickerController *_imagePickerController; - UIViewController *_viewController; - UIImagePickerControllerCameraDevice _device; } + (void)registerWithRegistrar:(NSObject *)registrar { FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/image_picker" binaryMessenger:[registrar messenger]]; - UIViewController *viewController = - [UIApplication sharedApplication].delegate.window.rootViewController; - FLTImagePickerPlugin *instance = - [[FLTImagePickerPlugin alloc] initWithViewController:viewController]; + FLTImagePickerPlugin *instance = [FLTImagePickerPlugin new]; [registrar addMethodCallDelegate:instance channel:channel]; } -- (instancetype)initWithViewController:(UIViewController *)viewController { - self = [super init]; - if (self) { - _viewController = viewController; +- (UIImagePickerController *)getImagePickerController { + return _imagePickerController; +} + +- (UIViewController *)viewControllerWithWindow:(UIWindow *)window { + UIWindow *windowToUse = window; + if (windowToUse == nil) { + for (UIWindow *window in [UIApplication sharedApplication].windows) { + if (window.isKeyWindow) { + windowToUse = window; + break; + } + } + } + + UIViewController *topController = windowToUse.rootViewController; + while (topController.presentedViewController) { + topController = topController.presentedViewController; } - return self; + return topController; } -- (UIImagePickerController *)getImagePickerController { - return _imagePickerController; +/** + * Returns the UIImagePickerControllerCameraDevice to use given [arguments]. + * + * If the cameraDevice value that is fetched from arguments is 1 then returns + * UIImagePickerControllerCameraDeviceFront. If the cameraDevice value that is fetched + * from arguments is 0 then returns UIImagePickerControllerCameraDeviceRear. + * + * @param arguments that should be used to get cameraDevice value. + */ +- (UIImagePickerControllerCameraDevice)getCameraDeviceFromArguments:(NSDictionary *)arguments { + NSInteger cameraDevice = [[arguments objectForKey:@"cameraDevice"] intValue]; + return (cameraDevice == 1) ? UIImagePickerControllerCameraDeviceFront + : UIImagePickerControllerCameraDeviceRear; +} + +- (void)pickImageWithPHPicker:(int)maxImagesAllowed API_AVAILABLE(ios(14)) { + PHPickerConfiguration *config = + [[PHPickerConfiguration alloc] initWithPhotoLibrary:PHPhotoLibrary.sharedPhotoLibrary]; + config.selectionLimit = maxImagesAllowed; // Setting to zero allow us to pick unlimited photos + config.filter = [PHPickerFilter imagesFilter]; + + _pickerViewController = [[PHPickerViewController alloc] initWithConfiguration:config]; + _pickerViewController.delegate = self; + _pickerViewController.presentationController.delegate = self; + + self.maxImagesAllowed = maxImagesAllowed; + + [self checkPhotoAuthorizationForAccessLevel]; +} + +- (void)pickImageWithUIImagePicker { + _imagePickerController = [[UIImagePickerController alloc] init]; + _imagePickerController.modalPresentationStyle = UIModalPresentationCurrentContext; + _imagePickerController.delegate = self; + _imagePickerController.mediaTypes = @[ (NSString *)kUTTypeImage ]; + + int imageSource = [[_arguments objectForKey:@"source"] intValue]; + + self.maxImagesAllowed = 1; + + switch (imageSource) { + case SOURCE_CAMERA: + [self checkCameraAuthorization]; + break; + case SOURCE_GALLERY: + [self checkPhotoAuthorization]; + break; + default: + self.result([FlutterError errorWithCode:@"invalid_source" + message:@"Invalid image source." + details:nil]); + break; + } } - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { @@ -61,32 +134,28 @@ - (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:1]; + } 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:0]; + } else { + [self pickImageWithUIImagePicker]; } } else if ([@"pickVideo" isEqualToString:call.method]) { _imagePickerController = [[UIImagePickerController alloc] init]; @@ -131,18 +200,30 @@ - (void)showCamera { return; } } + UIImagePickerControllerCameraDevice device = [self getCameraDeviceFromArguments:_arguments]; // Camera is not available on simulators if ([UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera] && - [UIImagePickerController isCameraDeviceAvailable:_device]) { + [UIImagePickerController isCameraDeviceAvailable:device]) { _imagePickerController.sourceType = UIImagePickerControllerSourceTypeCamera; - _imagePickerController.cameraDevice = _device; - [_viewController presentViewController:_imagePickerController animated:YES completion:nil]; + _imagePickerController.cameraDevice = device; + [[self viewControllerWithWindow:nil] presentViewController:_imagePickerController + 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; @@ -159,19 +240,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: @@ -185,18 +263,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: @@ -238,10 +347,96 @@ - (void)errorNoPhotoAccess:(PHAuthorizationStatus)status { } } -- (void)showPhotoLibrary { +- (void)showPhotoLibrary:(ImagePickerClassType)imagePickerClassType { // No need to check if SourceType is available. It always is. - _imagePickerController.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; - [_viewController 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)presentationControllerDidDismiss:(UIPresentationController *)presentationController { + if (self.result != nil) { + self.result(nil); + self.result = nil; + self->_arguments = nil; + } +} + +- (void)picker:(PHPickerViewController *)picker + didFinishPicking:(NSArray *)results API_AVAILABLE(ios(14)) { + [picker dismissViewControllerAnimated:YES completion:nil]; + if (results.count == 0) { + if (self.result != nil) { + self.result(nil); + self.result = nil; + self->_arguments = nil; + } + return; + } + dispatch_queue_t backgroundQueue = + dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0); + dispatch_async(backgroundQueue, ^{ + NSNumber *maxWidth = [self->_arguments objectForKey:@"maxWidth"]; + NSNumber *maxHeight = [self->_arguments objectForKey:@"maxHeight"]; + NSNumber *imageQuality = [self->_arguments objectForKey:@"imageQuality"]; + NSNumber *desiredImageQuality = [self getDesiredImageQuality:imageQuality]; + NSOperationQueue *operationQueue = [NSOperationQueue new]; + NSMutableArray *pathList = [self createNSMutableArrayWithSize:results.count]; + + for (int i = 0; i < results.count; i++) { + PHPickerResult *result = results[i]; + FLTPHPickerSaveImageToPathOperation *operation = + [[FLTPHPickerSaveImageToPathOperation alloc] initWithResult:result + maxHeight:maxHeight + maxWidth:maxWidth + desiredImageQuality:desiredImageQuality + savedPathBlock:^(NSString *savedPath) { + pathList[i] = savedPath; + }]; + [operationQueue addOperation:operation]; + } + [operationQueue waitUntilAllOperationsAreFinished]; + dispatch_async(dispatch_get_main_queue(), ^{ + [self handleSavedPathList:pathList]; + }); + }); +} + +/** + * Creates an NSMutableArray of a certain size filled with NSNull objects. + * + * The difference with initWithCapacity is that initWithCapacity still gives an empty array making + * it impossible to add objects on an index larger than the size. + * + * @param size The length of the required array + * @return NSMutableArray An array of a specified size + */ +- (NSMutableArray *)createNSMutableArrayWithSize:(NSUInteger)size { + NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithCapacity:size]; + for (int i = 0; i < size; [mutableArray addObject:[NSNull null]], i++) + ; + return mutableArray; } - (void)imagePickerController:(UIImagePickerController *)picker @@ -285,40 +480,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]; }]; } } @@ -326,8 +516,10 @@ - (void)imagePickerController:(UIImagePickerController *)picker - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { [_imagePickerController dismissViewControllerAnimated:YES completion:nil]; + if (!self.result) { + return; + } self.result(nil); - self.result = nil; _arguments = nil; } @@ -343,7 +535,7 @@ - (void)saveImageWithOriginalImageData:(NSData *)originalImageData maxWidth:maxWidth maxHeight:maxHeight imageQuality:imageQuality]; - [self handleSavedPath:savedPath]; + [self handleSavedPathList:@[ savedPath ]]; } - (void)saveImageWithPickerInfo:(NSDictionary *)info @@ -352,18 +544,43 @@ - (void)saveImageWithPickerInfo:(NSDictionary *)info NSString *savedPath = [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:info image:image imageQuality:imageQuality]; - [self handleSavedPath:savedPath]; + [self handleSavedPathList:@[ savedPath ]]; } -- (void)handleSavedPath:(NSString *)path { +/** + * Applies NSMutableArray on the FLutterResult. + * + * NSString must be returned by FlutterResult if the single image + * mode is active. It is checked by maxImagesAllowed and + * returns the first object of the pathlist. + * + * NSMutableArray must be returned by FlutterResult if the multi-image + * mode is active. After the pathlist count is checked then it returns + * the pathlist. + * + * @param pathList that should be applied to FlutterResult. + */ +- (void)handleSavedPathList:(NSArray *)pathList { if (!self.result) { return; } - if (path) { - self.result(path); + + if (pathList) { + if (![pathList containsObject:[NSNull null]]) { + if ((self.maxImagesAllowed == 1)) { + self.result(pathList.firstObject); + } else { + self.result(pathList); + } + } else { + self.result([FlutterError errorWithCode:@"create_error" + message:@"pathList's items should not be null" + details:nil]); + } } else { + // This should never happen. self.result([FlutterError errorWithCode:@"create_error" - message:@"Temporary file could not be created" + message:@"pathList should not be nil" details:nil]); } self.result = nil; diff --git a/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.h b/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.h new file mode 100644 index 000000000000..7ba3d28ef3dd --- /dev/null +++ b/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.h @@ -0,0 +1,31 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +#import "FLTImagePickerImageUtil.h" +#import "FLTImagePickerMetaDataUtil.h" +#import "FLTImagePickerPhotoAssetUtil.h" + +/*! + @class FLTPHPickerSaveImageToPathOperation + + @brief The FLTPHPickerSaveImageToPathOperation class + + @discussion This class was implemented to handle saved image paths and populate the pathList + with the final result by using GetSavedPath type block. + + @superclass SuperClass: NSOperation\n + @helps It helps FLTImagePickerPlugin class. + */ +@interface FLTPHPickerSaveImageToPathOperation : NSOperation + +- (instancetype)initWithResult:(PHPickerResult *)result + maxHeight:(NSNumber *)maxHeight + maxWidth:(NSNumber *)maxWidth + desiredImageQuality:(NSNumber *)desiredImageQuality + savedPathBlock:(void (^)(NSString *))savedPathBlock API_AVAILABLE(ios(14)); + +@end diff --git a/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.m b/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.m new file mode 100644 index 000000000000..30da22774d07 --- /dev/null +++ b/packages/image_picker/image_picker/ios/Classes/FLTPHPickerSaveImageToPathOperation.m @@ -0,0 +1,132 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTPHPickerSaveImageToPathOperation.h" + +API_AVAILABLE(ios(14)) +@interface FLTPHPickerSaveImageToPathOperation () + +@property(strong, nonatomic) PHPickerResult *result; +@property(assign, nonatomic) NSNumber *maxHeight; +@property(assign, nonatomic) NSNumber *maxWidth; +@property(assign, nonatomic) NSNumber *desiredImageQuality; + +@end + +typedef void (^GetSavedPath)(NSString *); + +@implementation FLTPHPickerSaveImageToPathOperation { + BOOL executing; + BOOL finished; + GetSavedPath getSavedPath; +} + +- (instancetype)initWithResult:(PHPickerResult *)result + maxHeight:(NSNumber *)maxHeight + maxWidth:(NSNumber *)maxWidth + desiredImageQuality:(NSNumber *)desiredImageQuality + savedPathBlock:(GetSavedPath)savedPathBlock API_AVAILABLE(ios(14)) { + if (self = [super init]) { + if (result) { + self.result = result; + self.maxHeight = maxHeight; + self.maxWidth = maxWidth; + self.desiredImageQuality = desiredImageQuality; + getSavedPath = savedPathBlock; + executing = NO; + finished = NO; + } else { + return nil; + } + return self; + } else { + return nil; + } +} + +- (BOOL)isConcurrent { + return YES; +} + +- (BOOL)isExecuting { + return executing; +} + +- (BOOL)isFinished { + return finished; +} + +- (void)setFinished:(BOOL)isFinished { + [self willChangeValueForKey:@"isFinished"]; + self->finished = isFinished; + [self didChangeValueForKey:@"isFinished"]; +} + +- (void)setExecuting:(BOOL)isExecuting { + [self willChangeValueForKey:@"isExecuting"]; + self->executing = isExecuting; + [self didChangeValueForKey:@"isExecuting"]; +} + +- (void)completeOperationWithPath:(NSString *)savedPath { + [self setExecuting:NO]; + [self setFinished:YES]; + getSavedPath(savedPath); +} + +- (void)start { + if ([self isCancelled]) { + [self setFinished:YES]; + return; + } + if (@available(iOS 14, *)) { + [self setExecuting:YES]; + [self.result.itemProvider + loadObjectOfClass:[UIImage class] + completionHandler:^(__kindof id _Nullable image, + NSError *_Nullable error) { + if ([image isKindOfClass:[UIImage class]]) { + __block UIImage *localImage = image; + PHAsset *originalAsset = + [FLTImagePickerPhotoAssetUtil getAssetFromPHPickerResult:self.result]; + + if (self.maxWidth != (id)[NSNull null] || self.maxHeight != (id)[NSNull null]) { + localImage = [FLTImagePickerImageUtil scaledImage:localImage + maxWidth:self.maxWidth + maxHeight:self.maxHeight + isMetadataAvailable:originalAsset != nil]; + } + __block NSString *savedPath; + if (!originalAsset) { + // Image picked without an original asset (e.g. User pick image without permission) + savedPath = + [FLTImagePickerPhotoAssetUtil saveImageWithPickerInfo:nil + image:localImage + imageQuality:self.desiredImageQuality]; + [self completeOperationWithPath:savedPath]; + } else { + [[PHImageManager defaultManager] + requestImageDataForAsset:originalAsset + options:nil + resultHandler:^( + NSData *_Nullable imageData, NSString *_Nullable dataUTI, + UIImageOrientation orientation, NSDictionary *_Nullable info) { + // maxWidth and maxHeight are used only for GIF images. + savedPath = [FLTImagePickerPhotoAssetUtil + saveImageWithOriginalImageData:imageData + image:localImage + maxWidth:self.maxWidth + maxHeight:self.maxHeight + imageQuality:self.desiredImageQuality]; + [self completeOperationWithPath:savedPath]; + }]; + } + } + }]; + } else { + [self setFinished:YES]; + } +} + +@end diff --git a/packages/image_picker/image_picker/ios/Tests/ImagePickerPluginTests.m b/packages/image_picker/image_picker/ios/Tests/ImagePickerPluginTests.m deleted file mode 100644 index b7f0d71fb95d..000000000000 --- a/packages/image_picker/image_picker/ios/Tests/ImagePickerPluginTests.m +++ /dev/null @@ -1,114 +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 FLTImagePickerPlugin (Test) -@property(copy, nonatomic) FlutterResult result; -- (void)handleSavedPath:(NSString *)path; -@end - -@interface ImagePickerPluginTests : XCTestCase -@end - -@implementation ImagePickerPluginTests - -#pragma mark - Test camera devices, no op on simulators -- (void)testPluginPickImageDeviceBack { - if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { - return; - } - FLTImagePickerPlugin *plugin = - [[FLTImagePickerPlugin alloc] initWithViewController:[UIViewController new]]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickImage" - arguments:@{@"source" : @(0), @"cameraDevice" : @(0)}]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; - XCTAssertEqual([plugin getImagePickerController].cameraDevice, - UIImagePickerControllerCameraDeviceRear); -} - -- (void)testPluginPickImageDeviceFront { - if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { - return; - } - FLTImagePickerPlugin *plugin = - [[FLTImagePickerPlugin alloc] initWithViewController:[UIViewController new]]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickImage" - arguments:@{@"source" : @(0), @"cameraDevice" : @(1)}]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; - XCTAssertEqual([plugin getImagePickerController].cameraDevice, - UIImagePickerControllerCameraDeviceFront); -} - -- (void)testPluginPickVideoDeviceBack { - if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { - return; - } - FLTImagePickerPlugin *plugin = - [[FLTImagePickerPlugin alloc] initWithViewController:[UIViewController new]]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickVideo" - arguments:@{@"source" : @(0), @"cameraDevice" : @(0)}]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; - XCTAssertEqual([plugin getImagePickerController].cameraDevice, - UIImagePickerControllerCameraDeviceRear); -} - -- (void)testPluginPickVideoDeviceFront { - if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) { - return; - } - FLTImagePickerPlugin *plugin = - [[FLTImagePickerPlugin alloc] initWithViewController:[UIViewController new]]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickVideo" - arguments:@{@"source" : @(0), @"cameraDevice" : @(1)}]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; - XCTAssertEqual([plugin getImagePickerController].cameraDevice, - UIImagePickerControllerCameraDeviceFront); -} - -#pragma mark - Test video duration -- (void)testPickingVideoWithDuration { - FLTImagePickerPlugin *plugin = - [[FLTImagePickerPlugin alloc] initWithViewController:[UIViewController new]]; - FlutterMethodCall *call = [FlutterMethodCall - methodCallWithMethodName:@"pickVideo" - arguments:@{@"source" : @(0), @"cameraDevice" : @(0), @"maxDuration" : @95}]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; - XCTAssertEqual([plugin getImagePickerController].videoMaximumDuration, 95); -} - -- (void)testPluginPickImageSelectMultipleTimes { - FLTImagePickerPlugin *plugin = - [[FLTImagePickerPlugin alloc] initWithViewController:[UIViewController new]]; - FlutterMethodCall *call = - [FlutterMethodCall methodCallWithMethodName:@"pickImage" - arguments:@{@"source" : @(0), @"cameraDevice" : @(0)}]; - [plugin handleMethodCall:call - result:^(id _Nullable r){ - }]; - plugin.result = ^(id result) { - - }; - [plugin handleSavedPath:@"test"]; - [plugin handleSavedPath:@"test"]; -} - -@end 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 47020f71711c..0d6cb0304723 100644 --- a/packages/image_picker/image_picker/ios/image_picker.podspec +++ b/packages/image_picker/image_picker/ios/image_picker.podspec @@ -17,10 +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' => 'armv7 arm64 x86_64' } - - s.test_spec 'Tests' do |test_spec| - test_spec.source_files = 'Tests/**/*' - end + s.platform = :ios, '9.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } end diff --git a/packages/image_picker/image_picker/lib/image_picker.dart b/packages/image_picker/image_picker/lib/image_picker.dart index 0dd9cac8d346..5bc99d7f0bb2 100755 --- a/packages/image_picker/image_picker/lib/image_picker.dart +++ b/packages/image_picker/image_picker/lib/image_picker.dart @@ -1,12 +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 'dart:async'; -import 'dart:io'; - import 'package:flutter/foundation.dart'; - import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; export 'package:image_picker_platform_interface/image_picker_platform_interface.dart' @@ -15,7 +12,10 @@ export 'package:image_picker_platform_interface/image_picker_platform_interface. kTypeVideo, ImageSource, CameraDevice, + LostData, LostDataResponse, + PickedFile, + XFile, RetrieveType; /// Provides an easy way to pick an image/video from the image library, @@ -23,50 +23,238 @@ 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; + static ImagePickerPlatform get platform => ImagePickerPlatform.instance; - /// Returns a [File] object pointing to the image that was picked. + /// Returns a [PickedFile] object wrapping 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 returned [PickedFile] 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]. /// + /// 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 - /// image types such as JPEG. If compression is not supported for the image that is picked, - /// an warning message will be logged. + /// the original quality will be returned. Compression is only supported for certain + /// image types such as JPEG and on Android PNG and WebP, too. If compression is not supported for the image that is picked, + /// a warning message will be logged. + /// + /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. + /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. + /// Defaults to [CameraDevice.rear]. Note that Android has no documented parameter for an intent to specify if + /// the front or rear camera should be opened, this function is not guaranteed + /// to work on an Android device. + /// + /// 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. + /// + /// See also [getMultiImage] to allow users to select multiple images at once. + /// + /// The method could throw [PlatformException] if the app does not have permission to access + /// the camera or photos gallery, no camera is available, plugin is already in use, + /// temporary file could not be created (iOS only), plugin activity could not + /// be allocated (Android only) or due to an unknown error. + @Deprecated('Switch to using pickImage instead') + Future getImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) { + return platform.pickImage( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice, + ); + } + + /// Returns a [List] object wrapping the images that were picked. + /// + /// The returned [List] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// + /// 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. + /// + /// This method is not supported in iOS versions lower than 14. + /// + /// If specified, the images will be at most `maxWidth` wide and + /// `maxHeight` tall. Otherwise the images will be returned at it's + /// original width and height. + /// The `imageQuality` argument modifies the quality of the images, ranging from 0-100 + /// where 100 is the original/max quality. If `imageQuality` is null, the images with + /// the original quality will be returned. Compression is only supported for certain + /// image types such as JPEG and on Android PNG and WebP, too. If compression is not supported for the image that is picked, + /// a warning message will be logged. + /// + /// The method could throw [PlatformException] if the app does not have permission to access + /// the camera or photos gallery, no camera is available, plugin is already in use, + /// temporary file could not be created (iOS only), plugin activity could not + /// be allocated (Android only) or due to an unknown error. + /// + /// See also [getImage] to allow users to only pick a single image. + @Deprecated('Switch to using pickMultiImage instead') + Future?> getMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) { + return platform.pickMultiImage( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + ); + } + + /// 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. + /// + /// 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 [getLostData] when your app relaunches to retrieve the lost data. + /// + /// The method could throw [PlatformException] if the app does not have permission to access + /// the camera or photos gallery, no camera is available, plugin is already in use, + /// temporary file could not be created and video could not be cached (iOS only), + /// plugin activity could not be allocated (Android only) or due to an unknown error. + /// + @Deprecated('Switch to using pickVideo instead') + Future getVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) { + return platform.pickVideo( + source: source, + preferredCameraDevice: preferredCameraDevice, + maxDuration: maxDuration, + ); + } + + /// 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. + /// Call this method to retrieve the lost data and process the data according to your APP's business logic. + /// + /// Returns a [LostData] object if successfully retrieved the lost data. The [LostData] object can represent either a + /// successful image/video selection, or a failure. + /// + /// Calling this on a non-Android platform will throw [UnimplementedError] exception. + /// + /// See also: + /// * [LostData], 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('Switch to using retrieveLostData instead') + Future getLostData() { + return platform.retrieveLostData(); + } + + /// Returns an [XFile] object wrapping the image that was picked. + /// + /// The returned [XFile] 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]. + /// + /// 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 supported for certain + /// image types such as JPEG and on Android PNG and WebP, too. If compression is not supported for the image that is picked, + /// a warning message will be logged. + /// + /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. + /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. + /// Defaults to [CameraDevice.rear]. Note that Android has no documented parameter for an intent to specify if + /// the front or rear camera should be opened, this function is not guaranteed + /// to work on an Android device. + /// /// 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. - static Future pickImage( - {@required ImageSource source, - double maxWidth, - double maxHeight, - int imageQuality, - CameraDevice preferredCameraDevice = CameraDevice.rear}) async { - String path = await platform.pickImagePath( + /// + /// See also [pickMultiImage] to allow users to select multiple images at once. + /// + /// The method could throw [PlatformException] if the app does not have permission to access + /// the camera or photos gallery, no camera is available, plugin is already in use, + /// temporary file could not be created (iOS only), plugin activity could not + /// be allocated (Android only) or due to an unknown error. + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) { + return platform.getImage( source: source, maxWidth: maxWidth, maxHeight: maxHeight, imageQuality: imageQuality, preferredCameraDevice: preferredCameraDevice, ); + } - return path == null ? null : File(path); + /// Returns a [List] object wrapping the images that were picked. + /// + /// The returned [List] is intended to be used within a single APP session. Do not save the file path and use it across sessions. + /// + /// 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. + /// + /// This method is not supported in iOS versions lower than 14. + /// + /// If specified, the images will be at most `maxWidth` wide and + /// `maxHeight` tall. Otherwise the images will be returned at it's + /// original width and height. + /// The `imageQuality` argument modifies the quality of the images, ranging from 0-100 + /// where 100 is the original/max quality. If `imageQuality` is null, the images with + /// the original quality will be returned. Compression is only supported for certain + /// image types such as JPEG and on Android PNG and WebP, too. If compression is not supported for the image that is picked, + /// a warning message will be logged. + /// + /// The method could throw [PlatformException] if the app does not have permission to access + /// the camera or photos gallery, no camera is available, plugin is already in use, + /// temporary file could not be created (iOS only), plugin activity could not + /// be allocated (Android only) or due to an unknown error. + /// + /// See also [pickImage] to allow users to only pick a single image. + Future?> pickMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) { + return platform.getMultiImage( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + ); } - /// Returns a [File] object pointing to the video that was picked. + /// Returns an [XFile] object wrapping 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 returned [XFile] 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]. @@ -80,33 +268,39 @@ 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 [retrieveLostData] when your app relaunches to retrieve the lost data. - static Future pickVideo( - {@required ImageSource source, - CameraDevice preferredCameraDevice = CameraDevice.rear, - Duration maxDuration}) async { - String path = await platform.pickVideoPath( + /// + /// The method could throw [PlatformException] if the app does not have permission to access + /// the camera or photos gallery, no camera is available, plugin is already in use, + /// temporary file could not be created and video could not be cached (iOS only), + /// plugin activity could not be allocated (Android only) or due to an unknown error. + /// + Future pickVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) { + return platform.getVideo( source: source, preferredCameraDevice: preferredCameraDevice, maxDuration: maxDuration, ); - - return path == null ? null : File(path); } - /// Retrieve the lost image file when [pickImage] or [pickVideo] failed because the MainActivity is destroyed. (Android only) + /// Retrieve the lost [XFile] when [pickImage], [pickMultiImage] 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. + /// Returns a [LostDataResponse] object if successfully retrieved the lost data. The [LostDataResponse] object 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(); + Future retrieveLostData() { + return platform.getLostData(); } } diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index 5cfba84dc3a5..ba5ce6635ed6 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.6+3 +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.4+2 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" flutter: plugin: @@ -12,20 +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.0.0 + flutter_plugin_android_lifecycle: ^2.0.1 + image_picker_for_web: ^2.1.0 + image_picker_platform_interface: ^2.3.0 dev_dependencies: - video_player: ^0.10.3 flutter_test: sdk: flutter - e2e: ^0.2.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 + plugin_platform_interface: ^2.0.0 diff --git a/packages/image_picker/image_picker/test/image_picker_deprecated_test.dart b/packages/image_picker/image_picker/test/image_picker_deprecated_test.dart new file mode 100644 index 000000000000..f295e3d02f66 --- /dev/null +++ b/packages/image_picker/image_picker/test/image_picker_deprecated_test.dart @@ -0,0 +1,458 @@ +// Copyright 2013 The Flutter 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_from_same_package + +// This file preserves the tests for the deprecated methods as they were before +// the migration. See image_picker_test.dart for the current tests. + +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(); + + group('$ImagePicker', () { + const MethodChannel channel = + MethodChannel('plugins.flutter.io/image_picker'); + + final List log = []; + + final picker = ImagePicker(); + + 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('#Single image/video', () { + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return ''; + }); + + log.clear(); + }); + + group('#pickImage', () { + test('passes the image source argument correctly', () async { + await picker.getImage(source: ImageSource.camera); + await picker.getImage(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 1, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + await picker.getImage(source: ImageSource.camera); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + ); + await picker.getImage( + source: ImageSource.camera, + maxHeight: 10.0, + ); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.getImage( + source: ImageSource.camera, maxWidth: 10.0, imageQuality: 70); + await picker.getImage( + source: ImageSource.camera, maxHeight: 10.0, imageQuality: 70); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + 'cameraDevice': 0 + }), + ], + ); + }); + + test('does not accept a negative width or height argument', () { + expect( + picker.getImage(source: ImageSource.camera, maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + picker.getImage(source: ImageSource.camera, maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.getImage(source: ImageSource.gallery), isNull); + expect(await picker.getImage(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.getImage(source: ImageSource.camera); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.getImage( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 1, + }), + ], + ); + }); + }); + + group('#pickVideo', () { + test('passes the image source argument correctly', () async { + await picker.getVideo(source: ImageSource.camera); + await picker.getVideo(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'cameraDevice': 0, + 'maxDuration': null, + }), + isMethodCall('pickVideo', arguments: { + 'source': 1, + 'cameraDevice': 0, + 'maxDuration': null, + }), + ], + ); + }); + + test('passes the duration argument correctly', () async { + await picker.getVideo(source: ImageSource.camera); + await picker.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(seconds: 10)); + await picker.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(minutes: 1)); + await picker.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(hours: 1)); + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': null, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 10, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 60, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 3600, + 'cameraDevice': 0, + }), + ], + ); + }); + + test('handles a null video path response gracefully', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.getVideo(source: ImageSource.gallery), isNull); + expect(await picker.getVideo(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.getVideo(source: ImageSource.camera); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'cameraDevice': 0, + 'maxDuration': null, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.getVideo( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': null, + 'cameraDevice': 1, + }), + ], + ); + }); + }); + + group('#retrieveLostData', () { + test('retrieveLostData get success response', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'image', + 'path': '/example/path', + }; + }); + final LostData response = await picker.getLostData(); + expect(response.type, RetrieveType.image); + expect(response.file!.path, '/example/path'); + }); + + test('retrieveLostData get error response', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'video', + 'errorCode': 'test_error_code', + 'errorMessage': 'test_error_message', + }; + }); + final LostData response = await picker.getLostData(); + expect(response.type, RetrieveType.video); + expect(response.exception!.code, 'test_error_code'); + expect(response.exception!.message, 'test_error_message'); + }); + + test('retrieveLostData get null response', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return null; + }); + expect((await picker.getLostData()).isEmpty, true); + }); + + test('retrieveLostData get both path and error should throw', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'video', + 'errorCode': 'test_error_code', + 'errorMessage': 'test_error_message', + 'path': '/example/path', + }; + }); + expect(picker.getLostData(), throwsAssertionError); + }); + }); + }); + + group('Multi images', () { + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return []; + }); + log.clear(); + }); + + group('#pickMultiImage', () { + test('passes the width and height arguments correctly', () async { + await picker.getMultiImage(); + await picker.getMultiImage( + maxWidth: 10.0, + ); + await picker.getMultiImage( + maxHeight: 10.0, + ); + await picker.getMultiImage( + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.getMultiImage( + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.getMultiImage( + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.getMultiImage( + maxWidth: 10.0, maxHeight: 20.0, imageQuality: 70); + + expect( + log, + [ + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + }), + ], + ); + }); + + test('does not accept a negative width or height argument', () { + expect( + picker.getMultiImage(maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + picker.getMultiImage(maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.getMultiImage(), isNull); + expect(await picker.getMultiImage(), isNull); + }); + }); + }); + }); +} + +class MockPlatform extends Mock + with MockPlatformInterfaceMixin + implements ImagePickerPlatform {} diff --git a/packages/image_picker/image_picker/test/image_picker_e2e.dart b/packages/image_picker/image_picker/test/image_picker_e2e.dart deleted file mode 100644 index b19e37dd6541..000000000000 --- a/packages/image_picker/image_picker/test/image_picker_e2e.dart +++ /dev/null @@ -1,5 +0,0 @@ -import 'package:e2e/e2e.dart'; - -void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); -} 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 8db71adcf778..10bc64082aca 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(); @@ -15,324 +18,454 @@ void main() { final List log = []; - setUp(() { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - log.add(methodCall); - return ''; - }); + final picker = ImagePicker(); - 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 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 - }), - ], - ); + group('#Single image/video', () { + setUp(() { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return ''; + }); + + log.clear(); }); - 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( + group('#pickImage', () { + test('passes the image source argument correctly', () async { + await picker.pickImage(source: ImageSource.camera); + await picker.pickImage(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 1, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + await picker.pickImage(source: ImageSource.camera); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + ); + await picker.pickImage( + source: ImageSource.camera, + maxHeight: 10.0, + ); + await picker.pickImage( source: ImageSource.camera, maxWidth: 10.0, maxHeight: 20.0, - 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 - }), - ], - ); - }); + ); + await picker.pickImage( + source: ImageSource.camera, maxWidth: 10.0, imageQuality: 70); + await picker.pickImage( + source: ImageSource.camera, maxHeight: 10.0, imageQuality: 70); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70); - test('does not accept a negative width or height argument', () { - expect( - ImagePicker.pickImage(source: ImageSource.camera, maxWidth: -1.0), - throwsArgumentError, - ); + 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 + }), + ], + ); + }); - expect( - ImagePicker.pickImage(source: ImageSource.camera, maxHeight: -1.0), - throwsArgumentError, - ); - }); + test('does not accept a negative width or height argument', () { + expect( + picker.pickImage(source: ImageSource.camera, maxWidth: -1.0), + throwsArgumentError, + ); - test('handles a null image path response gracefully', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) => null); + expect( + picker.pickImage(source: ImageSource.camera, maxHeight: -1.0), + throwsArgumentError, + ); + }); - expect( - await ImagePicker.pickImage(source: ImageSource.gallery), isNull); - expect(await ImagePicker.pickImage(source: ImageSource.camera), isNull); - }); + test('handles a null image path response gracefully', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) => null); - 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, - }), - ], - ); - }); + expect(await picker.pickImage(source: ImageSource.gallery), isNull); + expect(await picker.pickImage(source: ImageSource.camera), isNull); + }); - 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, - }), - ], - ); - }); - }); + test('camera position defaults to back', () async { + await picker.pickImage(source: ImageSource.camera); - 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, - }), - ], - ); - }); + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + }), + ], + ); + }); - 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('camera position can set to front', () async { + await picker.pickImage( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 1, + }), + ], + ); + }); }); - test('handles a null video path response gracefully', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) => null); + group('#pickVideo', () { + test('passes the image source argument correctly', () async { + await picker.pickVideo(source: ImageSource.camera); + await picker.pickVideo(source: ImageSource.gallery); - expect( - await ImagePicker.pickVideo(source: ImageSource.gallery), isNull); - expect(await ImagePicker.pickVideo(source: ImageSource.camera), isNull); - }); + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'cameraDevice': 0, + 'maxDuration': null, + }), + isMethodCall('pickVideo', arguments: { + 'source': 1, + 'cameraDevice': 0, + 'maxDuration': null, + }), + ], + ); + }); + + test('passes the duration argument correctly', () async { + await picker.pickVideo(source: ImageSource.camera); + await picker.pickVideo( + source: ImageSource.camera, + maxDuration: const Duration(seconds: 10)); + await picker.pickVideo( + source: ImageSource.camera, + maxDuration: const Duration(minutes: 1)); + await picker.pickVideo( + source: ImageSource.camera, + maxDuration: const Duration(hours: 1)); + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': null, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 10, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 60, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 3600, + 'cameraDevice': 0, + }), + ], + ); + }); + + test('handles a null video path response gracefully', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) => null); - test('camera position defaults to back', () async { - await ImagePicker.pickVideo(source: ImageSource.camera); - - expect( - log, - [ - isMethodCall('pickVideo', arguments: { - 'source': 0, - 'cameraDevice': 0, - 'maxDuration': null, - }), - ], - ); + expect(await picker.pickVideo(source: ImageSource.gallery), isNull); + expect(await picker.pickVideo(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.pickVideo(source: ImageSource.camera); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'cameraDevice': 0, + 'maxDuration': null, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.pickVideo( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': null, + 'cameraDevice': 1, + }), + ], + ); + }); }); - 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 picker.retrieveLostData(); + expect(response.type, RetrieveType.image); + expect(response.file!.path, '/example/path'); + }); + + test('retrieveLostData should successfully retrieve multiple files', + () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'image', + 'path': '/example/path1', + 'pathList': ['/example/path0', '/example/path1'], + }; + }); + + final LostDataResponse response = await picker.retrieveLostData(); + expect(response.type, RetrieveType.image); + expect(response.file, isNotNull); + expect(response.file!.path, '/example/path1'); + expect(response.files!.first.path, '/example/path0'); + expect(response.files!.length, 2); + }); + + test('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 picker.retrieveLostData(); + expect(response.type, RetrieveType.video); + expect(response.exception!.code, 'test_error_code'); + expect(response.exception!.message, 'test_error_message'); + }); + + test('retrieveLostData get null response', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return null; + }); + expect((await picker.retrieveLostData()).isEmpty, true); + }); + + test('retrieveLostData get both path and error should throw', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'video', + 'errorCode': 'test_error_code', + 'errorMessage': 'test_error_message', + 'path': '/example/path', + }; + }); + expect(picker.retrieveLostData(), throwsAssertionError); + }); }); }); - group('#retrieveLostData', () { - test('retrieveLostData get success response', () async { + group('#Multi images', () { + setUp(() { channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'image', - 'path': '/example/path', - }; + log.add(methodCall); + return []; }); - final LostDataResponse response = await ImagePicker.retrieveLostData(); - expect(response.type, RetrieveType.image); - expect(response.file.path, '/example/path'); + log.clear(); }); - test('retrieveLostData get error response', () async { - channel.setMockMethodCallHandler((MethodCall methodCall) async { - return { - 'type': 'video', - 'errorCode': 'test_error_code', - 'errorMessage': 'test_error_message', - }; + group('#pickMultiImage', () { + test('passes the width and height arguments correctly', () async { + 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, + }), + ], + ); }); - 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; + test('does not accept a negative width or height argument', () { + expect( + picker.pickMultiImage(maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + picker.pickMultiImage(maxHeight: -1.0), + throwsArgumentError, + ); }); - 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', - }; + test('handles a null image path response gracefully', () async { + channel.setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.pickMultiImage(), isNull); + expect(await picker.pickMultiImage(), isNull); }); - expect(ImagePicker.retrieveLostData(), throwsAssertionError); }); }); }); } + +class MockPlatform extends Mock + with MockPlatformInterfaceMixin + implements ImagePickerPlatform {} 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..d6ad42a677e5 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/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> +Balvinder Singh Gambhir diff --git a/packages/image_picker/image_picker_for_web/CHANGELOG.md b/packages/image_picker/image_picker_for_web/CHANGELOG.md new file mode 100644 index 000000000000..d11ead3bb64e --- /dev/null +++ b/packages/image_picker/image_picker_for_web/CHANGELOG.md @@ -0,0 +1,39 @@ +## 2.1.3 + +* Add `implements` to pubspec. + +## 2.1.2 + +* Updated installation instructions in README. + +# 2.1.1 + +* Implemented `getMultiImage`. +* Initialized the following `XFile` attributes for picked files: + * `name`, `length`, `mimeType` and `lastModified`. + +# 2.1.0 + +* Implemented `getImage`, `getVideo` and `getFile` methods that return `XFile` instances. +* Move tests to `example` directory, so they run as integration_tests with `flutter drive`. + +# 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 + +# 0.1.0+1 + +* Remove `android` directory. + +# 0.1.0 + +* Initial open-source release. diff --git a/packages/image_picker/image_picker_for_web/LICENSE b/packages/image_picker/image_picker_for_web/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/image_picker/image_picker_for_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/image_picker/image_picker_for_web/README.md b/packages/image_picker/image_picker_for_web/README.md new file mode 100644 index 000000000000..73f2dfc4b84f --- /dev/null +++ b/packages/image_picker/image_picker_for_web/README.md @@ -0,0 +1,88 @@ +# image\_picker\_for\_web + +A web implementation of [`image_picker`][1]. + +## 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 uniform +across platforms. + +The web version of the plugin puts network-accessible URIs as the `path` +in the returned `PickedFile`. + +### URL.createObjectURL() + +The `PickedFile` object in web is backed by [`URL.createObjectUrl` Web API](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL), +which is reasonably well supported across all browsers: + +![Data on support for the bloburls feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/bloburls.png) + +However, the returned `path` attribute of the `PickedFile` points to a `network` resource, and not a +local path in your users' drive. See **Use the plugin** below for some examples on how to use this +return value in a cross-platform way. + +### input file "accept" + +In order to filter only video/image content, some browsers offer an [`accept` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept) in their `input type="file"` form elements: + +![Data on support for the input-file-accept feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/input-file-accept.png) + +This feature is just a convenience for users, **not validation**. + +Users can override this setting on their browsers. You must validate in your app (or server) +that the user has picked the file type that you can handle. + +### input file "capture" + +In order to "take a photo", some mobile browsers offer a [`capture` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/capture): + +![Data on support for the html-media-capture feature across the major browsers from caniuse.com](https://caniuse.bitsofco.de/image/html-media-capture.png) + +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 + +This package is [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin), +which means you can simply use `image_picker` +normally. This package will be automatically included in your app when you do. + +### Use the plugin + +You should be able to use `package:image_picker` _almost_ as normal. + +Once the user has picked a file, the returned `PickedFile` instance will contain a +`network`-accessible URL (pointing to a location within the browser). + +The instace will also let you retrieve the bytes of the selected file across all platforms. + +If you want to use the path directly, your code would need look like this: + +```dart +... +if (kIsWeb) { + Image.network(pickedFile.path); +} else { + Image.file(File(pickedFile.path)); +} +... +``` + +Or, using bytes: + +```dart +... +Image.memory(await pickedFile.readAsBytes()) +... +``` + +[1]: https://pub.dev/packages/image_picker diff --git a/packages/image_picker/image_picker_for_web/example/README.md b/packages/image_picker/image_picker_for_web/example/README.md new file mode 100644 index 000000000000..4348451b14e2 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/example/README.md @@ -0,0 +1,9 @@ +# Testing + +This package uses `package:integration_test` to run its tests in a web browser. + +See [Plugin Tests > Web Tests](https://github.com/flutter/flutter/wiki/Plugin-Tests#web-tests) +in the Flutter wiki for instructions to setup and run the tests in this package. + +Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) +for more info. diff --git a/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart new file mode 100644 index 000000000000..c1025a9f07d3 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/example/integration_test/image_picker_for_web_test.dart @@ -0,0 +1,173 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:html' as html; +import 'dart:typed_data'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:image_picker_for_web/image_picker_for_web.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; +import 'package:integration_test/integration_test.dart'; + +final String expectedStringContents = 'Hello, world!'; +final String otherStringContents = 'Hello again, world!'; +final Uint8List bytes = utf8.encode(expectedStringContents) as Uint8List; +final Uint8List otherBytes = utf8.encode(otherStringContents) as Uint8List; +final Map options = { + 'type': 'text/plain', + 'lastModified': DateTime.utc(2017, 12, 13).millisecondsSinceEpoch, +}; +final html.File textFile = html.File([bytes], 'hello.txt', options); +final html.File secondTextFile = html.File([otherBytes], 'secondFile.txt'); + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // Under test... + late ImagePickerPlugin plugin; + + setUp(() { + plugin = ImagePickerPlugin(); + }); + + testWidgets('Can select a file (Deprecated)', (WidgetTester tester) async { + final mockInput = html.FileUploadInputElement(); + + final overrides = ImagePickerPluginTestOverrides() + ..createInputElement = ((_, __) => mockInput) + ..getMultipleFilesFromInput = ((_) => [textFile]); + + final plugin = ImagePickerPlugin(overrides: overrides); + + // Init the pick file dialog... + final file = plugin.pickFile(); + + // Mock the browser behavior of selecting a file... + mockInput.dispatchEvent(html.Event('change')); + + // Now the file should be available + expect(file, completes); + // And readable + expect((await file).readAsBytes(), completion(isNotEmpty)); + }); + + testWidgets('Can select a file', (WidgetTester tester) async { + final mockInput = html.FileUploadInputElement(); + + final overrides = ImagePickerPluginTestOverrides() + ..createInputElement = ((_, __) => mockInput) + ..getMultipleFilesFromInput = ((_) => [textFile]); + + final plugin = ImagePickerPlugin(overrides: overrides); + + // Init the pick file dialog... + final image = plugin.getImage(source: ImageSource.camera); + + // Mock the browser behavior of selecting a file... + mockInput.dispatchEvent(html.Event('change')); + + // Now the file should be available + expect(image, completes); + + // And readable + final XFile file = await image; + expect(file.readAsBytes(), completion(isNotEmpty)); + expect(file.name, textFile.name); + expect(file.length(), completion(textFile.size)); + expect(file.mimeType, textFile.type); + expect( + file.lastModified(), + completion( + DateTime.fromMillisecondsSinceEpoch(textFile.lastModified!), + )); + }); + + testWidgets('Can select multiple files', (WidgetTester tester) async { + final mockInput = html.FileUploadInputElement(); + + final overrides = ImagePickerPluginTestOverrides() + ..createInputElement = ((_, __) => mockInput) + ..getMultipleFilesFromInput = ((_) => [textFile, secondTextFile]); + + final plugin = ImagePickerPlugin(overrides: overrides); + + // Init the pick file dialog... + final files = plugin.getMultiImage(); + + // Mock the browser behavior of selecting a file... + mockInput.dispatchEvent(html.Event('change')); + + // Now the file should be available + expect(files, completes); + + // And readable + expect((await files).first.readAsBytes(), completion(isNotEmpty)); + + // Peek into the second file... + final XFile secondFile = (await files).elementAt(1); + expect(secondFile.readAsBytes(), completion(isNotEmpty)); + expect(secondFile.name, secondTextFile.name); + expect(secondFile.length(), completion(secondTextFile.size)); + }); + + // There's no good way of detecting when the user has "aborted" the selection. + + testWidgets('computeCaptureAttribute', (WidgetTester tester) async { + expect( + plugin.computeCaptureAttribute(ImageSource.gallery, CameraDevice.front), + isNull, + ); + expect( + plugin.computeCaptureAttribute(ImageSource.gallery, CameraDevice.rear), + isNull, + ); + expect( + plugin.computeCaptureAttribute(ImageSource.camera, CameraDevice.front), + 'user', + ); + expect( + plugin.computeCaptureAttribute(ImageSource.camera, CameraDevice.rear), + 'environment', + ); + }); + + group('createInputElement', () { + testWidgets('accept: any, capture: null', (WidgetTester tester) async { + html.Element input = plugin.createInputElement('any', null); + + expect(input.attributes, containsPair('accept', 'any')); + expect(input.attributes, isNot(contains('capture'))); + expect(input.attributes, isNot(contains('multiple'))); + }); + + testWidgets('accept: any, capture: something', (WidgetTester tester) async { + html.Element input = plugin.createInputElement('any', 'something'); + + expect(input.attributes, containsPair('accept', 'any')); + expect(input.attributes, containsPair('capture', 'something')); + expect(input.attributes, isNot(contains('multiple'))); + }); + + testWidgets('accept: any, capture: null, multi: true', + (WidgetTester tester) async { + html.Element input = + plugin.createInputElement('any', null, multiple: true); + + expect(input.attributes, containsPair('accept', 'any')); + expect(input.attributes, isNot(contains('capture'))); + expect(input.attributes, contains('multiple')); + }); + + testWidgets('accept: any, capture: something, multi: true', + (WidgetTester tester) async { + html.Element input = + plugin.createInputElement('any', 'something', multiple: true); + + expect(input.attributes, containsPair('accept', 'any')); + expect(input.attributes, containsPair('capture', 'something')); + expect(input.attributes, contains('multiple')); + }); + }); +} diff --git a/packages/image_picker/image_picker_for_web/example/lib/main.dart b/packages/image_picker/image_picker_for_web/example/lib/main.dart new file mode 100644 index 000000000000..e1a38dcdcd46 --- /dev/null +++ b/packages/image_picker/image_picker_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/image_picker/image_picker_for_web/example/pubspec.yaml b/packages/image_picker/image_picker_for_web/example/pubspec.yaml new file mode 100644 index 000000000000..8dadde267e8a --- /dev/null +++ b/packages/image_picker/image_picker_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: + image_picker_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/image_picker/image_picker_for_web/example/run_test.sh b/packages/image_picker/image_picker_for_web/example/run_test.sh new file mode 100755 index 000000000000..aa52974f310e --- /dev/null +++ b/packages/image_picker/image_picker_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/image_picker/image_picker_for_web/example/test_driver/integration_test.dart b/packages/image_picker/image_picker_for_web/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/image_picker/image_picker_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/image_picker/image_picker_for_web/example/web/index.html b/packages/image_picker/image_picker_for_web/example/web/index.html new file mode 100644 index 000000000000..7fb138cc90fa --- /dev/null +++ b/packages/image_picker/image_picker_for_web/example/web/index.html @@ -0,0 +1,13 @@ + + + + + + example + + + + + 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 new file mode 100644 index 000000000000..b170ee3256ab --- /dev/null +++ b/packages/image_picker/image_picker_for_web/lib/image_picker_for_web.dart @@ -0,0 +1,336 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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_web_plugins/flutter_web_plugins.dart'; +import 'package:meta/meta.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; + +final String _kImagePickerInputsDomId = '__image_picker_web-file-input'; +final String _kAcceptImageMimeType = 'image/*'; +final String _kAcceptVideoMimeType = 'video/3gpp,video/x-m4v,video/mp4,video/*'; + +/// The web implementation of [ImagePickerPlatform]. +/// +/// This class implements the `package:image_picker` functionality for the web. +class ImagePickerPlugin extends ImagePickerPlatform { + final ImagePickerPluginTestOverrides? _overrides; + + bool get _hasOverrides => _overrides != null; + + late html.Element _target; + + /// A constructor that allows tests to override the function that creates file inputs. + ImagePickerPlugin({ + @visibleForTesting ImagePickerPluginTestOverrides? overrides, + }) : _overrides = overrides { + _target = _ensureInitialized(_kImagePickerInputsDomId); + } + + /// Registers this class as the default instance of [ImagePickerPlatform]. + static void registerWith(Registrar registrar) { + 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, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) { + 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, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) { + String? capture = computeCaptureAttribute(source, preferredCameraDevice); + return pickFile(accept: _kAcceptVideoMimeType, capture: capture); + } + + /// Injects a file input with the specified accept+capture attributes, and + /// returns the PickedFile that the user selected locally. + /// + /// `capture` is only supported in mobile browsers. + /// See https://caniuse.com/#feat=html-media-capture + @visibleForTesting + Future pickFile({ + String? accept, + String? capture, + }) { + html.FileUploadInputElement input = + createInputElement(accept, capture) as html.FileUploadInputElement; + _injectAndActivate(input); + return _getSelectedFile(input); + } + + /// Returns an [XFile] with the image that was picked. + /// + /// The `source` argument controls where the image comes from. This can + /// be either [ImageSource.camera] or [ImageSource.gallery]. + /// + /// 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 getImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) async { + String? capture = computeCaptureAttribute(source, preferredCameraDevice); + List files = await getFiles( + accept: _kAcceptImageMimeType, + capture: capture, + ); + return files.first; + } + + /// Returns an [XFile] 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 getVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + String? capture = computeCaptureAttribute(source, preferredCameraDevice); + List files = await getFiles( + accept: _kAcceptVideoMimeType, + capture: capture, + ); + return files.first; + } + + /// Injects a file input, and returns a list of XFile that the user selected locally. + @override + Future> getMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) { + return getFiles(accept: _kAcceptImageMimeType, multiple: true); + } + + /// Injects a file input with the specified accept+capture attributes, and + /// returns a list of XFile that the user selected locally. + /// + /// `capture` is only supported in mobile browsers. + /// + /// `multiple` can be passed to allow for multiple selection of files. Defaults + /// to false. + /// + /// See https://caniuse.com/#feat=html-media-capture + @visibleForTesting + Future> getFiles({ + String? accept, + String? capture, + bool multiple = false, + }) { + html.FileUploadInputElement input = createInputElement( + accept, + capture, + multiple: multiple, + ) as html.FileUploadInputElement; + _injectAndActivate(input); + + return _getSelectedXFiles(input); + } + + // DOM methods + + /// Converts plugin configuration into a proper value for the `capture` attribute. + /// + /// See: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#capture + @visibleForTesting + String? computeCaptureAttribute(ImageSource source, CameraDevice device) { + if (source == ImageSource.camera) { + return (device == CameraDevice.front) ? 'user' : 'environment'; + } + return null; + } + + List? _getFilesFromInput(html.FileUploadInputElement input) { + if (_hasOverrides) { + return _overrides!.getMultipleFilesFromInput(input); + } + return input.files; + } + + /// Handles the OnChange event from a FileUploadInputElement object + /// Returns a list of selected files. + List? _handleOnChangeEvent(html.Event event) { + final html.FileUploadInputElement input = + event.target as html.FileUploadInputElement; + return _getFilesFromInput(input); + } + + /// Monitors an and returns the selected file. + Future _getSelectedFile(html.FileUploadInputElement input) { + final Completer _completer = Completer(); + // Observe the input until we can return something + input.onChange.first.then((event) { + final files = _handleOnChangeEvent(event); + if (!_completer.isCompleted && files != null) { + _completer.complete(PickedFile( + html.Url.createObjectUrl(files.first), + )); + } + }); + input.onError.first.then((event) { + if (!_completer.isCompleted) { + _completer.completeError(event); + } + }); + // Note that we don't bother detaching from these streams, since the + // "input" gets re-created in the DOM every time the user needs to + // pick a file. + return _completer.future; + } + + /// Monitors an and returns the selected file(s). + Future> _getSelectedXFiles(html.FileUploadInputElement input) { + final Completer> _completer = Completer>(); + // Observe the input until we can return something + input.onChange.first.then((event) { + final files = _handleOnChangeEvent(event); + if (!_completer.isCompleted && files != null) { + _completer.complete(files + .map((file) => XFile( + html.Url.createObjectUrl(file), + name: file.name, + length: file.size, + lastModified: DateTime.fromMillisecondsSinceEpoch( + file.lastModified ?? DateTime.now().millisecondsSinceEpoch, + ), + mimeType: file.type, + )) + .toList()); + } + }); + input.onError.first.then((event) { + if (!_completer.isCompleted) { + _completer.completeError(event); + } + }); + // Note that we don't bother detaching from these streams, since the + // "input" gets re-created in the DOM every time the user needs to + // pick a file. + return _completer.future; + } + + /// Initializes a DOM container where we can host input elements. + html.Element _ensureInitialized(String id) { + var target = html.querySelector('#${id}'); + if (target == null) { + final html.Element targetElement = + html.Element.tag('flt-image-picker-inputs')..id = id; + + html.querySelector('body')!.children.add(targetElement); + target = targetElement; + } + return target; + } + + /// 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, { + bool multiple = false, + }) { + if (_hasOverrides) { + return _overrides!.createInputElement(accept, capture); + } + + html.Element element = html.FileUploadInputElement() + ..accept = accept + ..multiple = multiple; + + if (capture != null) { + element.setAttribute('capture', capture); + } + + return element; + } + + /// Injects the file input element, and clicks on it + void _injectAndActivate(html.Element element) { + _target.children.clear(); + _target.children.add(element); + element.click(); + } +} + +// Some tools to override behavior for unit-testing +/// 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, +); + +/// A function that extracts list of files from the file `input` passed in. +@visibleForTesting +typedef OverrideExtractMultipleFilesFromInputFunction = List + Function(html.Element? input); + +/// Overrides for some of the functionality above. +@visibleForTesting +class ImagePickerPluginTestOverrides { + /// Override the creation of the input element. + late OverrideCreateInputFunction createInputElement; + + /// Override the extraction of the selected files from an input element. + late OverrideExtractMultipleFilesFromInputFunction getMultipleFilesFromInput; +} diff --git a/packages/image_picker/image_picker_for_web/pubspec.yaml b/packages/image_picker/image_picker_for_web/pubspec.yaml new file mode 100644 index 000000000000..895486f3de06 --- /dev/null +++ b/packages/image_picker/image_picker_for_web/pubspec.yaml @@ -0,0 +1,30 @@ +name: image_picker_for_web +description: Web platform implementation of image_picker +repository: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker_for_web +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+image_picker%22 +version: 2.1.3 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +flutter: + plugin: + implements: image_picker + platforms: + web: + pluginClass: ImagePickerPlugin + fileName: image_picker_for_web.dart + +dependencies: + flutter: + sdk: flutter + flutter_web_plugins: + sdk: flutter + image_picker_platform_interface: ^2.2.0 + meta: ^1.3.0 + +dev_dependencies: + flutter_test: + sdk: flutter + pedantic: ^1.10.0 diff --git a/packages/image_picker/image_picker_for_web/test/README.md b/packages/image_picker/image_picker_for_web/test/README.md new file mode 100644 index 000000000000..7c5b4ad682ba --- /dev/null +++ b/packages/image_picker/image_picker_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/image_picker/image_picker_for_web/test/tests_exist_elsewhere_test.dart b/packages/image_picker/image_picker_for_web/test/tests_exist_elsewhere_test.dart new file mode 100644 index 000000000000..442c50144727 --- /dev/null +++ b/packages/image_picker/image_picker_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/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 7708c34ffe8c..d637ac1a277e 100644 --- a/packages/image_picker/image_picker_platform_interface/CHANGELOG.md +++ b/packages/image_picker/image_picker_platform_interface/CHANGELOG.md @@ -1,3 +1,72 @@ +## 2.4.1 + +* Reverts the changes from 2.4.0, which was a breaking change that + was incorrectly marked as a non-breaking change. + +## 2.4.0 + +* Add `forceFullMetadata` option to `pickImage`. + * To keep this non-breaking `forceFullMetadata` defaults to `true`, so the plugin tries + to get the full image metadata which may require extra permission requests on certain platforms. + * If `forceFullMetadata` is set to `false`, the plugin fetches the image in a way that reduces + permission requests from the platform (e.g on iOS the plugin won’t ask for the `NSPhotoLibraryUsageDescription` permission). + +## 2.3.0 + +* Updated `LostDataResponse` to include a `files` property, in case more than one file was recovered. + +## 2.2.0 + +* Added new methods that return `XFile` (from `package:cross_file`) + * `getImage` (will deprecate `pickImage`) + * `getVideo` (will deprecate `pickVideo`) + * `getMultiImage` (will deprecate `pickMultiImage`) + +_`PickedFile` will also be marked as deprecated in an upcoming release._ + +## 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. + +## 1.1.1 + +* Update documentation of getImage() about Android's disability to preference front/rear camera. + +## 1.1.0 + +* Introduce PickedFile type for the new API. + ## 1.0.1 * Update lower bound of dart dependency to 2.1.0. diff --git a/packages/image_picker/image_picker_platform_interface/LICENSE b/packages/image_picker/image_picker_platform_interface/LICENSE index c89293372cf3..c6823b81eb84 100644 --- a/packages/image_picker/image_picker_platform_interface/LICENSE +++ b/packages/image_picker/image_picker_platform_interface/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/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..133c05ecfebf 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,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 'package:image_picker_platform_interface/src/platform_interface/image_picker_platform.dart'; export 'package:image_picker_platform_interface/src/types/types.dart'; +export 'package:cross_file/cross_file.dart'; diff --git a/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart b/packages/image_picker/image_picker_platform_interface/lib/src/method_channel/method_channel_image_picker.dart index 4d960517b73b..b02284e957fa 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,74 @@ class MethodChannelImagePicker extends ImagePickerPlatform { MethodChannel get channel => _channel; @override - Future pickImagePath({ - @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 _getImagePath( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice, + ); + return path != null ? PickedFile(path) : null; + } + + @override + Future?> pickMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) async { + final List? paths = await _getMultiImagePath( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + ); + if (paths == null) return null; + + return paths.map((path) => PickedFile(path)).toList(); + } + + Future?> _getMultiImagePath({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) { + if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { + throw ArgumentError.value( + imageQuality, 'imageQuality', 'must be between 0 and 100'); + } + + if (maxWidth != null && maxWidth < 0) { + throw ArgumentError.value(maxWidth, 'maxWidth', 'cannot be negative'); + } + + if (maxHeight != null && maxHeight < 0) { + throw ArgumentError.value(maxHeight, 'maxHeight', 'cannot be negative'); + } + + return _channel.invokeMethod?>( + 'pickMultiImage', + { + 'maxWidth': maxWidth, + 'maxHeight': maxHeight, + 'imageQuality': imageQuality, + }, + ); + } + + Future _getImagePath({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { - assert(source != null); if (imageQuality != null && (imageQuality < 0 || imageQuality > 100)) { throw ArgumentError.value( imageQuality, 'imageQuality', 'must be between 0 and 100'); @@ -54,12 +113,24 @@ class MethodChannelImagePicker extends ImagePickerPlatform { } @override - Future pickVideoPath({ - @required ImageSource source, + Future pickVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + final String? path = await _getVideoPath( + source: source, + maxDuration: maxDuration, + preferredCameraDevice: preferredCameraDevice, + ); + return path != null ? PickedFile(path) : null; + } + + Future _getVideoPath({ + required ImageSource source, CameraDevice preferredCameraDevice = CameraDevice.rear, - Duration maxDuration, + Duration? maxDuration, }) { - assert(source != null); return _channel.invokeMethod( 'pickVideo', { @@ -71,35 +142,133 @@ class MethodChannelImagePicker extends ImagePickerPlatform { } @override - Future retrieveLostDataAsDartIoFile() async { - final Map result = + Future retrieveLostData() async { + final Map? result = + await _channel.invokeMapMethod('retrieve'); + + if (result == null) { + return LostData.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']; + + return LostData( + file: path != null ? PickedFile(path) : null, + exception: exception, + type: retrieveType, + ); + } + + @override + Future getImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) async { + String? path = await _getImagePath( + source: source, + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + preferredCameraDevice: preferredCameraDevice, + ); + return path != null ? XFile(path) : null; + } + + @override + Future?> getMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) async { + final List? paths = await _getMultiImagePath( + maxWidth: maxWidth, + maxHeight: maxHeight, + imageQuality: imageQuality, + ); + if (paths == null) return null; + + return paths.map((path) => XFile(path)).toList(); + } + + @override + Future getVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) async { + final String? path = await _getVideoPath( + source: source, + maxDuration: maxDuration, + preferredCameraDevice: preferredCameraDevice, + ); + return path != null ? XFile(path) : null; + } + + @override + Future getLostData() async { + List? pickedFileList; + + Map? result = await _channel.invokeMapMethod('retrieve'); + if (result == null) { return LostDataResponse.empty(); } - assert(result.containsKey('path') ^ result.containsKey('errorCode')); - final String type = result['type']; + assert(result.containsKey('path') != result.containsKey('errorCode')); + + 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']; + + final pathList = result['pathList']; + if (pathList != null) { + pickedFileList = []; + for (String path in pathList) { + pickedFileList.add(XFile(path)); + } + } return LostDataResponse( - file: path == null ? null : File(path), - exception: exception, - type: retrieveType); + file: path != null ? XFile(path) : null, + exception: exception, + type: retrieveType, + files: pickedFileList, + ); } } 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 66e74dd95636..5c1c8b698442 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,12 +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:cross_file/cross_file.dart'; import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - import 'package:image_picker_platform_interface/src/method_channel/method_channel_image_picker.dart'; import 'package:image_picker_platform_interface/src/types/types.dart'; @@ -39,38 +38,73 @@ 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. /// /// 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 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. + /// 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. - Future pickImagePath({ - @required ImageSource source, - double maxWidth, - double maxHeight, - int imageQuality, + /// in this call. You can then call [retrieveLostData] when your app relaunches to retrieve the lost data. + /// + /// If no images were picked, the return value is null. + Future pickImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, CameraDevice preferredCameraDevice = CameraDevice.rear, }) { - throw UnimplementedError('legacyPickImage() has not been implemented.'); + throw UnimplementedError('pickImage() has not been implemented.'); + } + + /// Returns a [List] with the images that were picked. + /// + /// The images come from the [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 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, + /// a warning message will be logged. + /// + /// If no images were picked, the return value is null. + Future?> pickMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) { + throw UnimplementedError('pickMultiImage() has not been implemented.'); } - /// Returns a [String] containing a path to the video that was picked. + /// 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]. @@ -83,30 +117,138 @@ abstract class ImagePickerPlatform extends PlatformInterface { /// Defaults to [CameraDevice.rear]. /// /// In Android, the MainActivity can be destroyed for various fo reasons. If that happens, the result will be lost - /// in this call. You can then call [retrieveLostDataAsDartIoFile] when your app relaunches to retrieve the lost data. - Future pickVideoPath({ - @required ImageSource source, + /// in this call. You can then call [retrieveLostData] when your app relaunches to retrieve the lost data. + /// + /// 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('pickVideoPath() has not been implemented.'); + throw UnimplementedError('pickVideo() has not been implemented.'); } - /// Retrieve the lost image file when [pickImage] or [pickVideo] failed because the MainActivity is destroyed. (Android only) + /// Retrieves any previously picked file, that was lost due to the MainActivity being destroyed. + /// In case multiple files were lost, only the last file will be recovered. (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 + /// Returns a [LostData] object if successfully retrieved the lost data. The [LostData] object 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. + /// * [LostData], 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. - Future retrieveLostDataAsDartIoFile() { - throw UnimplementedError( - 'retrieveLostDataAsDartIoFile() has not been implemented.'); + Future retrieveLostData() { + throw UnimplementedError('retrieveLostData() has not been implemented.'); + } + + /// Returns an [XFile] with the image that was picked. + /// + /// The `source` argument controls where the image comes from. This can + /// be either [ImageSource.camera] or [ImageSource.gallery]. + /// + /// 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 supported for certain + /// image types such as JPEG. If compression is not supported for the image that is picked, + /// a warning message will be logged. + /// + /// 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. + /// + /// In Android, the MainActivity can be destroyed for various reasons. If that happens, the result will be lost + /// in this call. You can then call [getLostData] when your app relaunches to retrieve the lost data. + /// + /// If no images were picked, the return value is null. + Future getImage({ + required ImageSource source, + double? maxWidth, + double? maxHeight, + int? imageQuality, + CameraDevice preferredCameraDevice = CameraDevice.rear, + }) { + throw UnimplementedError('getImage() has not been implemented.'); + } + + /// Returns a [List] with the images that were picked. + /// + /// The images come from the [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 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, + /// a warning message will be logged. + /// + /// If no images were picked, the return value is null. + Future?> getMultiImage({ + double? maxWidth, + double? maxHeight, + int? imageQuality, + }) { + throw UnimplementedError('getMultiImage() has not been implemented.'); + } + + /// Returns a [XFile] containing the video that was picked. + /// + /// The [source] argument controls where the video comes from. This can + /// be either [ImageSource.camera] or [ImageSource.gallery]. + /// + /// The [maxDuration] argument specifies the maximum duration of the captured video. If no [maxDuration] is specified, + /// the maximum duration will be infinite. + /// + /// Use `preferredCameraDevice` to specify the camera to use when the `source` is [ImageSource.camera]. + /// The `preferredCameraDevice` is ignored when `source` is [ImageSource.gallery]. It is also ignored if the chosen camera is not supported on the device. + /// Defaults to [CameraDevice.rear]. + /// + /// In Android, the MainActivity can be destroyed for various fo reasons. If that happens, the result will be lost + /// in this call. You can then call [getLostData] when your app relaunches to retrieve the lost data. + /// + /// If no images were picked, the return value is null. + Future getVideo({ + required ImageSource source, + CameraDevice preferredCameraDevice = CameraDevice.rear, + Duration? maxDuration, + }) { + throw UnimplementedError('getVideo() has not been implemented.'); + } + + /// Retrieves any previously picked files, that were lost due to the MainActivity being 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] object if successfully retrieved the lost data. The [LostDataResponse] object 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. + Future getLostData() { + throw UnimplementedError('getLostData() 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 index 53e2decd123f..65f5d7e15c90 100644 --- a/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/lost_data_response.dart @@ -1,21 +1,25 @@ -// 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:io'; - +import 'package:cross_file/cross_file.dart'; import 'package:flutter/services.dart'; import 'package:image_picker_platform_interface/src/types/types.dart'; -/// The response object of [ImagePicker.retrieveLostData]. +/// The response object of [ImagePicker.getLostData]. /// /// Only applies to Android. /// See also: -/// * [ImagePicker.retrieveLostData] for more details on retrieving lost data. +/// * [ImagePicker.getLostData] for more details on retrieving lost data. 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}); + LostDataResponse({ + this.file, + this.exception, + this.type, + this.files, + }); /// Initializes an instance with all member params set to null and considered /// to be empty. @@ -23,29 +27,40 @@ class LostDataResponse { : file = null, exception = null, type = null, - _empty = true; + _empty = true, + files = null; /// 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. + /// The file that was lost in a previous [getImage], [getMultiImage] or [getVideo] call due to MainActivity being destroyed. /// /// Can be null if [exception] exists. - final File file; + final XFile? file; - /// The exception of the last [pickImage] or [pickVideo]. + /// The exception of the last [getImage], [getMultiImage] or [getVideo]. /// - /// 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. + /// If the last [getImage], [getMultiImage] or [getVideo] threw some exception before the MainActivity destruction, + /// this variable keeps that exception. + /// You should handle this exception as if the [getImage], [getMultiImage] or [getVideo] 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; + + /// The list of files that were lost in a previous [getMultiImage] call due to MainActivity being destroyed. + /// + /// When [files] is populated, [file] will refer to the last item in the [files] list. + /// + /// Can be null if [exception] exists. + final List? files; } 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 new file mode 100644 index 000000000000..de259e0611dd --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/base.dart @@ -0,0 +1,62 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:meta/meta.dart'; + +/// The interface for a PickedFile. +/// +/// A PickedFile 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. +@immutable +abstract class PickedFileBase { + /// Construct a PickedFile + PickedFileBase(String path); + + /// 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 PickedFile instance instead. + String get path { + throw UnimplementedError('.path 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.'); + } +} 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 new file mode 100644 index 000000000000..24e1931008b6 --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/html.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 'dart:convert'; +import 'dart:typed_data'; + +import 'package:http/http.dart' as http show readBytes; + +import './base.dart'; + +/// A PickedFile that works on web. +/// +/// It wraps the bytes of a selected file. +class PickedFile extends PickedFileBase { + final String path; + final Uint8List? _initBytes; + + /// Construct a PickedFile object from its ObjectUrl. + /// + /// Optionally, this can be initialized with `bytes` + /// so no http requests are performed to retrieve files later. + PickedFile(this.path, {Uint8List? bytes}) + : _initBytes = bytes, + super(path); + + Future get _bytes async { + if (_initBytes != null) { + return Future.value(UnmodifiableUint8ListView(_initBytes!)); + } + return http.readBytes(Uri.parse(path)); + } + + @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); + } +} 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 new file mode 100644 index 000000000000..7037b6b7121a --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/io.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:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import './base.dart'; + +/// A PickedFile backed by a dart:io File. +class PickedFile extends PickedFileBase { + final File _file; + + /// Construct a PickedFile object backed by a dart:io File. + PickedFile(String path) + : _file = File(path), + super(path); + + @override + String get path { + return _file.path; + } + + @override + Future readAsString({Encoding encoding = utf8}) { + return _file.readAsString(encoding: encoding); + } + + @override + Future readAsBytes() { + return _file.readAsBytes(); + } + + @override + 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 new file mode 100644 index 000000000000..64f6a1f27538 --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/lost_data.dart @@ -0,0 +1,51 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/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. +class LostData { + /// 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. + LostData({this.file, this.exception, this.type}); + + /// Initializes an instance with all member params set to null and considered + /// to be empty. + LostData.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 PickedFile? 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]; + /// + /// 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 new file mode 100644 index 000000000000..c8c9e5a0ac79 --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/picked_file.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. + +export 'lost_data.dart'; +export 'unsupported.dart' + if (dart.library.html) 'html.dart' + if (dart.library.io) 'io.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 new file mode 100644 index 000000000000..ad3ed6a4f86a --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/lib/src/types/picked_file/unsupported.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 './base.dart'; + +/// A PickedFile is a cross-platform, simplified File abstraction. +/// +/// It wraps the bytes of a selected file, and its (platform-dependant) path. +class PickedFile extends PickedFileBase { + /// Construct a PickedFile object, from its `bytes`. + /// + /// Optionally, you may pass a `path`. See caveats in [PickedFileBase.path]. + PickedFile(String path) : super(path) { + throw UnimplementedError( + 'PickedFile is not available in your current platform.'); + } +} 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 98418109dc09..ad7cd3fbcaab 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,7 +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. + export 'camera_device.dart'; export 'image_source.dart'; -export 'lost_data_response.dart'; export 'retrieve_type.dart'; +export 'picked_file/picked_file.dart'; +export 'lost_data_response.dart'; /// Denotes that an image is being picked. const String kTypeImage = 'image'; diff --git a/packages/image_picker/image_picker_platform_interface/pubspec.yaml b/packages/image_picker/image_picker_platform_interface/pubspec.yaml index a4ea5d1f959a..e41137fcb06b 100644 --- a/packages/image_picker/image_picker_platform_interface/pubspec.yaml +++ b/packages/image_picker/image_picker_platform_interface/pubspec.yaml @@ -1,22 +1,24 @@ 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.0.1 +version: 2.4.1 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" dependencies: flutter: sdk: flutter - meta: ^1.1.8 - plugin_platform_interface: ^1.0.2 + http: ^0.13.0 + meta: ^1.3.0 + plugin_platform_interface: ^2.0.0 + cross_file: ^0.3.1+1 dev_dependencies: flutter_test: sdk: flutter - mockito: ^4.1.1 - pedantic: ^1.8.0+1 - -environment: - sdk: ">=2.1.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/assets/hello.txt b/packages/image_picker/image_picker_platform_interface/test/assets/hello.txt new file mode 100644 index 000000000000..5dd01c177f5d --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/test/assets/hello.txt @@ -0,0 +1 @@ +Hello, world! \ No newline at end of file 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 701379b84aae..000000000000 --- a/packages/image_picker/image_picker_platform_interface/test/method_channel_image_picker_test.dart +++ /dev/null @@ -1,343 +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', - }; - }); - 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', - }; - }); - 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); - }); - }); - }); -} 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 new file mode 100644 index 000000000000..17caa8456621 --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/test/new_method_channel_image_picker_test.dart @@ -0,0 +1,983 @@ +// Copyright 2013 The Flutter Authors. All rights 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 = []; + dynamic returnValue = ''; + + setUp(() { + returnValue = ''; + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + return returnValue; + }); + + log.clear(); + }); + + group('#pickImage', () { + test('passes the image source argument correctly', () async { + await picker.pickImage(source: ImageSource.camera); + await picker.pickImage(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 1, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + await picker.pickImage(source: ImageSource.camera); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + ); + await picker.pickImage( + source: ImageSource.camera, + maxHeight: 10.0, + ); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.pickImage( + source: ImageSource.camera, + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.pickImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + 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 invalid imageQuality argument', () { + expect( + () => picker.pickImage(imageQuality: -1, source: ImageSource.gallery), + throwsArgumentError, + ); + + expect( + () => + picker.pickImage(imageQuality: 101, source: ImageSource.gallery), + throwsArgumentError, + ); + + expect( + () => picker.pickImage(imageQuality: -1, source: ImageSource.camera), + throwsArgumentError, + ); + + expect( + () => picker.pickImage(imageQuality: 101, source: ImageSource.camera), + throwsArgumentError, + ); + }); + + test('does not accept a negative width or height argument', () { + expect( + () => picker.pickImage(source: ImageSource.camera, maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.pickImage(source: ImageSource.camera, maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + picker.channel + .setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.pickImage(source: ImageSource.gallery), isNull); + expect(await picker.pickImage(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.pickImage(source: ImageSource.camera); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.pickImage( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 1, + }), + ], + ); + }); + }); + + 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('#pickVideo', () { + test('passes the image source argument correctly', () async { + await picker.pickVideo(source: ImageSource.camera); + await picker.pickVideo(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'cameraDevice': 0, + 'maxDuration': null, + }), + isMethodCall('pickVideo', arguments: { + 'source': 1, + 'cameraDevice': 0, + 'maxDuration': null, + }), + ], + ); + }); + + test('passes the duration argument correctly', () async { + await picker.pickVideo(source: ImageSource.camera); + await picker.pickVideo( + source: ImageSource.camera, + maxDuration: const Duration(seconds: 10), + ); + await picker.pickVideo( + source: ImageSource.camera, + maxDuration: const Duration(minutes: 1), + ); + await picker.pickVideo( + source: ImageSource.camera, + maxDuration: const Duration(hours: 1), + ); + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': null, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 10, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 60, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 3600, + 'cameraDevice': 0, + }), + ], + ); + }); + + test('handles a null video path response gracefully', () async { + picker.channel + .setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.pickVideo(source: ImageSource.gallery), isNull); + expect(await picker.pickVideo(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.pickVideo(source: ImageSource.camera); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'cameraDevice': 0, + 'maxDuration': null, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.pickVideo( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front, + ); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': null, + 'cameraDevice': 1, + }), + ], + ); + }); + }); + + group('#retrieveLostData', () { + test('retrieveLostData get success response', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'image', + 'path': '/example/path', + }; + }); + final LostData response = await picker.retrieveLostData(); + expect(response.type, RetrieveType.image); + expect(response.file, isNotNull); + expect(response.file!.path, '/example/path'); + }); + + test('retrieveLostData get error response', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'video', + 'errorCode': 'test_error_code', + 'errorMessage': 'test_error_message', + }; + }); + final LostData response = await picker.retrieveLostData(); + expect(response.type, RetrieveType.video); + expect(response.exception, isNotNull); + expect(response.exception!.code, 'test_error_code'); + expect(response.exception!.message, 'test_error_message'); + }); + + test('retrieveLostData get null response', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return null; + }); + expect((await picker.retrieveLostData()).isEmpty, true); + }); + + test('retrieveLostData get both path and error should throw', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'video', + 'errorCode': 'test_error_code', + 'errorMessage': 'test_error_message', + 'path': '/example/path', + }; + }); + expect(picker.retrieveLostData(), throwsAssertionError); + }); + }); + + group('#getImage', () { + test('passes the image source argument correctly', () async { + await picker.getImage(source: ImageSource.camera); + await picker.getImage(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + isMethodCall('pickImage', arguments: { + 'source': 1, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + await picker.getImage(source: ImageSource.camera); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + ); + await picker.getImage( + source: ImageSource.camera, + maxHeight: 10.0, + ); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.getImage( + source: ImageSource.camera, + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.getImage( + source: ImageSource.camera, + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0 + }), + 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 invalid imageQuality argument', () { + expect( + () => picker.getImage(imageQuality: -1, source: ImageSource.gallery), + throwsArgumentError, + ); + + expect( + () => picker.getImage(imageQuality: 101, source: ImageSource.gallery), + throwsArgumentError, + ); + + expect( + () => picker.getImage(imageQuality: -1, source: ImageSource.camera), + throwsArgumentError, + ); + + expect( + () => picker.getImage(imageQuality: 101, source: ImageSource.camera), + throwsArgumentError, + ); + }); + + test('does not accept a negative width or height argument', () { + expect( + () => picker.getImage(source: ImageSource.camera, maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.getImage(source: ImageSource.camera, maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + picker.channel + .setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.getImage(source: ImageSource.gallery), isNull); + expect(await picker.getImage(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.getImage(source: ImageSource.camera); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 0, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.getImage( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front); + + expect( + log, + [ + isMethodCall('pickImage', arguments: { + 'source': 0, + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + 'cameraDevice': 1, + }), + ], + ); + }); + }); + + group('#getMultiImage', () { + test('calls the method correctly', () async { + returnValue = ['0', '1']; + await picker.getMultiImage(); + + expect( + log, + [ + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + }), + ], + ); + }); + + test('passes the width and height arguments correctly', () async { + returnValue = ['0', '1']; + await picker.getMultiImage(); + await picker.getMultiImage( + maxWidth: 10.0, + ); + await picker.getMultiImage( + maxHeight: 10.0, + ); + await picker.getMultiImage( + maxWidth: 10.0, + maxHeight: 20.0, + ); + await picker.getMultiImage( + maxWidth: 10.0, + imageQuality: 70, + ); + await picker.getMultiImage( + maxHeight: 10.0, + imageQuality: 70, + ); + await picker.getMultiImage( + maxWidth: 10.0, + maxHeight: 20.0, + imageQuality: 70, + ); + + expect( + log, + [ + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': null, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': null, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': null, + 'imageQuality': 70, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': null, + 'maxHeight': 10.0, + 'imageQuality': 70, + }), + isMethodCall('pickMultiImage', arguments: { + 'maxWidth': 10.0, + 'maxHeight': 20.0, + 'imageQuality': 70, + }), + ], + ); + }); + + test('does not accept a negative width or height argument', () { + returnValue = ['0', '1']; + expect( + () => picker.getMultiImage(maxWidth: -1.0), + throwsArgumentError, + ); + + expect( + () => picker.getMultiImage(maxHeight: -1.0), + throwsArgumentError, + ); + }); + + test('does not accept a invalid imageQuality argument', () { + returnValue = ['0', '1']; + expect( + () => picker.getMultiImage(imageQuality: -1), + throwsArgumentError, + ); + + expect( + () => picker.getMultiImage(imageQuality: 101), + throwsArgumentError, + ); + }); + + test('handles a null image path response gracefully', () async { + picker.channel + .setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.getMultiImage(), isNull); + expect(await picker.getMultiImage(), isNull); + }); + }); + + group('#getVideo', () { + test('passes the image source argument correctly', () async { + await picker.getVideo(source: ImageSource.camera); + await picker.getVideo(source: ImageSource.gallery); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'cameraDevice': 0, + 'maxDuration': null, + }), + isMethodCall('pickVideo', arguments: { + 'source': 1, + 'cameraDevice': 0, + 'maxDuration': null, + }), + ], + ); + }); + + test('passes the duration argument correctly', () async { + await picker.getVideo(source: ImageSource.camera); + await picker.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(seconds: 10), + ); + await picker.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(minutes: 1), + ); + await picker.getVideo( + source: ImageSource.camera, + maxDuration: const Duration(hours: 1), + ); + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': null, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 10, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 60, + 'cameraDevice': 0, + }), + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': 3600, + 'cameraDevice': 0, + }), + ], + ); + }); + + test('handles a null video path response gracefully', () async { + picker.channel + .setMockMethodCallHandler((MethodCall methodCall) => null); + + expect(await picker.getVideo(source: ImageSource.gallery), isNull); + expect(await picker.getVideo(source: ImageSource.camera), isNull); + }); + + test('camera position defaults to back', () async { + await picker.getVideo(source: ImageSource.camera); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'cameraDevice': 0, + 'maxDuration': null, + }), + ], + ); + }); + + test('camera position can set to front', () async { + await picker.getVideo( + source: ImageSource.camera, + preferredCameraDevice: CameraDevice.front, + ); + + expect( + log, + [ + isMethodCall('pickVideo', arguments: { + 'source': 0, + 'maxDuration': null, + 'cameraDevice': 1, + }), + ], + ); + }); + }); + + group('#getLostData', () { + test('getLostData get success response', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'image', + 'path': '/example/path', + }; + }); + final LostDataResponse response = await picker.getLostData(); + expect(response.type, RetrieveType.image); + expect(response.file, isNotNull); + expect(response.file!.path, '/example/path'); + }); + + test('getLostData should successfully retrieve multiple files', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'image', + 'path': '/example/path1', + 'pathList': ['/example/path0', '/example/path1'], + }; + }); + final LostDataResponse response = await picker.getLostData(); + expect(response.type, RetrieveType.image); + expect(response.file, isNotNull); + expect(response.file!.path, '/example/path1'); + expect(response.files!.first.path, '/example/path0'); + expect(response.files!.length, 2); + }); + + test('getLostData get error response', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'video', + 'errorCode': 'test_error_code', + 'errorMessage': 'test_error_message', + }; + }); + final LostDataResponse response = await picker.getLostData(); + expect(response.type, RetrieveType.video); + expect(response.exception, isNotNull); + expect(response.exception!.code, 'test_error_code'); + expect(response.exception!.message, 'test_error_message'); + }); + + test('getLostData get null response', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return null; + }); + expect((await picker.getLostData()).isEmpty, true); + }); + + test('getLostData get both path and error should throw', () async { + picker.channel.setMockMethodCallHandler((MethodCall methodCall) async { + return { + 'type': 'video', + 'errorCode': 'test_error_code', + 'errorMessage': 'test_error_message', + 'path': '/example/path', + }; + }); + expect(picker.getLostData(), throwsAssertionError); + }); + }); + }); +} 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 new file mode 100644 index 000000000000..7721f66148e0 --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/test/picked_file_html_test.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. + +@TestOn('chrome') // Uses web-only Flutter SDK + +import 'dart:convert'; +import 'dart:html' as html; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:image_picker_platform_interface/image_picker_platform_interface.dart'; + +final String expectedStringContents = 'Hello, world!'; +final List 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 pickedFile = PickedFile(textFileUrl); + + test('Can be read as a string', () async { + expect(await pickedFile.readAsString(), equals(expectedStringContents)); + }); + test('Can be read as bytes', () async { + expect(await pickedFile.readAsBytes(), equals(bytes)); + }); + + test('Can be read as a stream', () async { + expect(await pickedFile.openRead().first, equals(bytes)); + }); + + test('Stream can be sliced', () async { + expect( + await pickedFile.openRead(2, 5).first, equals(bytes.sublist(2, 5))); + }); + }); +} 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 new file mode 100644 index 000000000000..d366204c36bf --- /dev/null +++ b/packages/image_picker/image_picker_platform_interface/test/picked_file_io_test.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. + +@TestOn('vm') // Uses dart:io + +import 'dart:convert'; +import 'dart:io'; +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 = 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 = PickedFile(textFilePath); + + test('Can be read as a string', () async { + expect(await pickedFile.readAsString(), equals(expectedStringContents)); + }); + test('Can be read as bytes', () async { + expect(await pickedFile.readAsBytes(), equals(bytes)); + }); + + test('Can be read as a stream', () async { + expect(await pickedFile.openRead().first, equals(bytes)); + }); + + test('Stream can be sliced', () async { + expect( + await pickedFile.openRead(2, 5).first, equals(bytes.sublist(2, 5))); + }); + }); +} diff --git a/packages/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/CHANGELOG.md deleted file mode 100644 index c50133d6899c..000000000000 --- a/packages/in_app_purchase/CHANGELOG.md +++ /dev/null @@ -1,281 +0,0 @@ -## 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 8940a4be1b58..000000000000 --- a/packages/in_app_purchase/LICENSE +++ /dev/null @@ -1,27 +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 f3a34a667c70..000000000000 --- a/packages/in_app_purchase/README.md +++ /dev/null @@ -1,176 +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 -// 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.isEmpty) { - // 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/gradle.properties b/packages/in_app_purchase/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/in_app_purchase/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/packages/in_app_purchase/android/gradle/wrapper/gradle-wrapper.properties b/packages/in_app_purchase/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 73eba353b126..000000000000 --- a/packages/in_app_purchase/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Mon Oct 29 10:30:44 PDT 2018 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip 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/AndroidManifest.xml b/packages/in_app_purchase/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 5ad1aa42bf98..000000000000 --- a/packages/in_app_purchase/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - 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 aa7352aaf357..000000000000 --- a/packages/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/EmbeddingV1Activity.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 android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; - -public class EmbeddingV1Activity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); - } -} 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 1cac72300228..000000000000 --- a/packages/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/EmbeddingV1ActivityTest.java +++ /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. - -package io.flutter.plugins.inapppurchaseexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -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/MainActivity.java b/packages/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/MainActivity.java deleted file mode 100644 index acb1bd566267..000000000000 --- a/packages/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/MainActivity.java +++ /dev/null @@ -1,25 +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 io.flutter.embedding.android.FlutterActivity; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistry; -import io.flutter.plugins.inapppurchase.InAppPurchasePlugin; -import io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin; - -public class MainActivity extends FlutterActivity { - - @Override - public void configureFlutterEngine(FlutterEngine flutterEngine) { - super.configureFlutterEngine(flutterEngine); - flutterEngine.getPlugins().add(new InAppPurchasePlugin()); - - ShimPluginRegistry shimPluginRegistry = new ShimPluginRegistry(flutterEngine); - SharedPreferencesPlugin.registerWith( - shimPluginRegistry.registrarFor( - "io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin")); - } -} diff --git a/packages/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/MainActivityTest.java b/packages/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/MainActivityTest.java deleted file mode 100644 index f3ba16447be4..000000000000 --- a/packages/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/inapppurchaseexample/MainActivityTest.java +++ /dev/null @@ -1,15 +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.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class MainActivityTest { - @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); -} diff --git a/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/InAppPurchasePluginTest.java b/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/InAppPurchasePluginTest.java deleted file mode 100644 index 0befa87e1d05..000000000000 --- a/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/InAppPurchasePluginTest.java +++ /dev/null @@ -1,40 +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 org.mockito.Mockito.when; - -import android.app.Activity; -import android.app.Application; -import android.content.Context; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.PluginRegistry; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -public class InAppPurchasePluginTest { - @Mock Activity activity; - @Mock Context context; - @Mock PluginRegistry.Registrar mockRegistrar; // For v1 embedding - @Mock BinaryMessenger mockMessenger; - @Mock Application mockApplication; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - when(mockRegistrar.activity()).thenReturn(activity); - when(mockRegistrar.messenger()).thenReturn(mockMessenger); - when(mockRegistrar.context()).thenReturn(context); - } - - @Test - public void registerWith_doNotCrashWhenRegisterContextIsActivity_V1Embedding() { - when(mockRegistrar.context()).thenReturn(activity); - when(activity.getApplicationContext()).thenReturn(mockApplication); - InAppPurchasePlugin.registerWith(mockRegistrar); - } -} diff --git a/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java deleted file mode 100644 index c6a9b4114a75..000000000000 --- a/packages/in_app_purchase/example/android/app/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java +++ /dev/null @@ -1,637 +0,0 @@ -package io.flutter.plugins.inapppurchase; - -import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.ACKNOWLEDGE_PURCHASE; -import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.CONSUME_PURCHASE_ASYNC; -import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.END_CONNECTION; -import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.IS_READY; -import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.LAUNCH_BILLING_FLOW; -import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.ON_DISCONNECT; -import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.ON_PURCHASES_UPDATED; -import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.QUERY_PURCHASES; -import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.QUERY_PURCHASE_HISTORY_ASYNC; -import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.QUERY_SKU_DETAILS; -import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.START_CONNECTION; -import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult; -import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList; -import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList; -import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesResult; -import static io.flutter.plugins.inapppurchase.Translator.fromSkuDetailsList; -import static java.util.Arrays.asList; -import static java.util.Collections.singletonList; -import static java.util.stream.Collectors.toList; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.contains; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.refEq; -import static org.mockito.Mockito.doNothing; -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.app.Activity; -import android.content.Context; -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.BillingClient.SkuType; -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.Purchase; -import com.android.billingclient.api.Purchase.PurchasesResult; -import com.android.billingclient.api.PurchaseHistoryRecord; -import com.android.billingclient.api.PurchaseHistoryResponseListener; -import com.android.billingclient.api.SkuDetails; -import com.android.billingclient.api.SkuDetailsParams; -import com.android.billingclient.api.SkuDetailsResponseListener; -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.Result; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import org.junit.Before; -import org.junit.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; -import org.mockito.Spy; - -public class MethodCallHandlerTest { - private MethodCallHandlerImpl methodChannelHandler; - private BillingClientFactory factory; - @Mock BillingClient mockBillingClient; - @Mock MethodChannel mockMethodChannel; - @Spy Result result; - @Mock Activity activity; - @Mock Context context; - @Mock ActivityPluginBinding mockActivityPluginBinding; - - @Before - public void setUp() { - MockitoAnnotations.initMocks(this); - factory = - (@NonNull Context context, - @NonNull MethodChannel channel, - boolean enablePendingPurchases) -> mockBillingClient; - methodChannelHandler = new MethodCallHandlerImpl(activity, context, mockMethodChannel, factory); - when(mockActivityPluginBinding.getActivity()).thenReturn(activity); - } - - @Test - public void invalidMethod() { - MethodCall call = new MethodCall("invalid", null); - methodChannelHandler.onMethodCall(call, result); - verify(result, times(1)).notImplemented(); - } - - @Test - public void isReady_true() { - mockStartConnection(); - MethodCall call = new MethodCall(IS_READY, null); - when(mockBillingClient.isReady()).thenReturn(true); - methodChannelHandler.onMethodCall(call, result); - verify(result).success(true); - } - - @Test - public void isReady_false() { - mockStartConnection(); - MethodCall call = new MethodCall(IS_READY, null); - when(mockBillingClient.isReady()).thenReturn(false); - methodChannelHandler.onMethodCall(call, result); - verify(result).success(false); - } - - @Test - public void isReady_clientDisconnected() { - MethodCall disconnectCall = new MethodCall(END_CONNECTION, null); - methodChannelHandler.onMethodCall(disconnectCall, mock(Result.class)); - MethodCall isReadyCall = new MethodCall(IS_READY, null); - - methodChannelHandler.onMethodCall(isReadyCall, result); - - verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); - verify(result, never()).success(any()); - } - - @Test - public void startConnection() { - ArgumentCaptor captor = mockStartConnection(); - verify(result, never()).success(any()); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); - captor.getValue().onBillingSetupFinished(billingResult); - - verify(result, times(1)).success(fromBillingResult(billingResult)); - } - - @Test - public void startConnection_multipleCalls() { - Map arguments = new HashMap<>(); - arguments.put("handle", 1); - arguments.put("enablePendingPurchases", true); - MethodCall call = new MethodCall(START_CONNECTION, arguments); - ArgumentCaptor captor = - ArgumentCaptor.forClass(BillingClientStateListener.class); - doNothing().when(mockBillingClient).startConnection(captor.capture()); - - methodChannelHandler.onMethodCall(call, result); - verify(result, never()).success(any()); - BillingResult billingResult1 = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); - BillingResult billingResult2 = - BillingResult.newBuilder() - .setResponseCode(200) - .setDebugMessage("dummy debug message") - .build(); - BillingResult billingResult3 = - BillingResult.newBuilder() - .setResponseCode(300) - .setDebugMessage("dummy debug message") - .build(); - - captor.getValue().onBillingSetupFinished(billingResult1); - captor.getValue().onBillingSetupFinished(billingResult2); - captor.getValue().onBillingSetupFinished(billingResult3); - - verify(result, times(1)).success(fromBillingResult(billingResult1)); - verify(result, times(1)).success(any()); - } - - @Test - public void endConnection() { - // Set up a connected BillingClient instance - final int disconnectCallbackHandle = 22; - Map arguments = new HashMap<>(); - arguments.put("handle", disconnectCallbackHandle); - arguments.put("enablePendingPurchases", true); - MethodCall connectCall = new MethodCall(START_CONNECTION, arguments); - ArgumentCaptor captor = - ArgumentCaptor.forClass(BillingClientStateListener.class); - doNothing().when(mockBillingClient).startConnection(captor.capture()); - methodChannelHandler.onMethodCall(connectCall, mock(Result.class)); - final BillingClientStateListener stateListener = captor.getValue(); - - // Disconnect the connected client - MethodCall disconnectCall = new MethodCall(END_CONNECTION, null); - methodChannelHandler.onMethodCall(disconnectCall, result); - - // Verify that the client is disconnected and that the OnDisconnect callback has - // been triggered - verify(result, times(1)).success(any()); - verify(mockBillingClient, times(1)).endConnection(); - stateListener.onBillingServiceDisconnected(); - Map expectedInvocation = new HashMap<>(); - expectedInvocation.put("handle", disconnectCallbackHandle); - verify(mockMethodChannel, times(1)).invokeMethod(ON_DISCONNECT, expectedInvocation); - } - - @Test - public void querySkuDetailsAsync() { - // Connect a billing client and set up the SKU query listeners - establishConnectedBillingClient(/* arguments= */ null, /* result= */ null); - String skuType = BillingClient.SkuType.INAPP; - List skusList = asList("id1", "id2"); - HashMap arguments = new HashMap<>(); - arguments.put("skuType", skuType); - arguments.put("skusList", skusList); - MethodCall queryCall = new MethodCall(QUERY_SKU_DETAILS, arguments); - - // Query for SKU details - methodChannelHandler.onMethodCall(queryCall, result); - - // Assert the arguments were forwarded correctly to BillingClient - ArgumentCaptor paramCaptor = ArgumentCaptor.forClass(SkuDetailsParams.class); - ArgumentCaptor listenerCaptor = - ArgumentCaptor.forClass(SkuDetailsResponseListener.class); - verify(mockBillingClient).querySkuDetailsAsync(paramCaptor.capture(), listenerCaptor.capture()); - assertEquals(paramCaptor.getValue().getSkuType(), skuType); - assertEquals(paramCaptor.getValue().getSkusList(), skusList); - - // Assert that we handed result BillingClient's response - int responseCode = 200; - List skuDetailsResponse = asList(buildSkuDetails("foo")); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); - listenerCaptor.getValue().onSkuDetailsResponse(billingResult, skuDetailsResponse); - ArgumentCaptor> resultCaptor = ArgumentCaptor.forClass(HashMap.class); - verify(result).success(resultCaptor.capture()); - HashMap resultData = resultCaptor.getValue(); - assertEquals(resultData.get("billingResult"), fromBillingResult(billingResult)); - assertEquals(resultData.get("skuDetailsList"), fromSkuDetailsList(skuDetailsResponse)); - } - - @Test - public void querySkuDetailsAsync_clientDisconnected() { - // Disconnect the Billing client and prepare a querySkuDetails call - MethodCall disconnectCall = new MethodCall(END_CONNECTION, null); - methodChannelHandler.onMethodCall(disconnectCall, mock(Result.class)); - String skuType = BillingClient.SkuType.INAPP; - List skusList = asList("id1", "id2"); - HashMap arguments = new HashMap<>(); - arguments.put("skuType", skuType); - arguments.put("skusList", skusList); - MethodCall queryCall = new MethodCall(QUERY_SKU_DETAILS, arguments); - - // Query for SKU details - methodChannelHandler.onMethodCall(queryCall, result); - - // Assert that we sent an error back. - verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); - verify(result, never()).success(any()); - } - - @Test - public void launchBillingFlow_ok_nullAccountId() { - // 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); - 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.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_Activity() { - methodChannelHandler.setActivity(null); - - // 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); - MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); - methodChannelHandler.onMethodCall(launchCall, result); - - // Verify we pass the response code to result - verify(result).error(contains("ACTIVITY_UNAVAILABLE"), contains("foreground"), any()); - verify(result, never()).success(any()); - } - - @Test - public void launchBillingFlow_ok_AccountId() { - // Fetch the sku details first and query the method call - String skuId = "foo"; - String accountId = "account"; - queryForSkus(singletonList(skuId)); - HashMap arguments = new HashMap<>(); - arguments.put("sku", skuId); - arguments.put("accountId", accountId); - MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); - - // Launch the billing flow - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); - when(mockBillingClient.launchBillingFlow(any(), any())).thenReturn(billingResult); - methodChannelHandler.onMethodCall(launchCall, result); - - // Verify we pass the arguments to the billing flow - ArgumentCaptor billingFlowParamsCaptor = - ArgumentCaptor.forClass(BillingFlowParams.class); - verify(mockBillingClient).launchBillingFlow(any(), billingFlowParamsCaptor.capture()); - BillingFlowParams params = billingFlowParamsCaptor.getValue(); - assertEquals(params.getSku(), skuId); - assertEquals(params.getAccountId(), accountId); - - // Verify we pass the response code to result - verify(result, never()).error(any(), any(), any()); - verify(result, times(1)).success(fromBillingResult(billingResult)); - } - - @Test - public void launchBillingFlow_clientDisconnected() { - // Prepare the launch call after disconnecting the client - MethodCall disconnectCall = new MethodCall(END_CONNECTION, null); - methodChannelHandler.onMethodCall(disconnectCall, mock(Result.class)); - String skuId = "foo"; - String accountId = "account"; - HashMap arguments = new HashMap<>(); - arguments.put("sku", skuId); - arguments.put("accountId", accountId); - MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); - - methodChannelHandler.onMethodCall(launchCall, result); - - // Assert that we sent an error back. - verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); - verify(result, never()).success(any()); - } - - @Test - public void launchBillingFlow_skuNotFound() { - // Try to launch the billing flow for a random sku ID - establishConnectedBillingClient(null, null); - String skuId = "foo"; - String accountId = "account"; - HashMap arguments = new HashMap<>(); - arguments.put("sku", skuId); - arguments.put("accountId", accountId); - MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); - - methodChannelHandler.onMethodCall(launchCall, result); - - // Assert that we sent an error back. - verify(result).error(contains("NOT_FOUND"), contains(skuId), any()); - verify(result, never()).success(any()); - } - - @Test - public void queryPurchases() { - establishConnectedBillingClient(null, null); - PurchasesResult purchasesResult = mock(PurchasesResult.class); - Purchase purchase = buildPurchase("foo"); - when(purchasesResult.getPurchasesList()).thenReturn(asList(purchase)); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); - when(purchasesResult.getBillingResult()).thenReturn(billingResult); - when(mockBillingClient.queryPurchases(SkuType.INAPP)).thenReturn(purchasesResult); - - HashMap arguments = new HashMap<>(); - arguments.put("skuType", SkuType.INAPP); - methodChannelHandler.onMethodCall(new MethodCall(QUERY_PURCHASES, arguments), result); - - // Verify we pass the response to result - ArgumentCaptor> resultCaptor = ArgumentCaptor.forClass(HashMap.class); - verify(result, never()).error(any(), any(), any()); - verify(result, times(1)).success(resultCaptor.capture()); - assertEquals(fromPurchasesResult(purchasesResult), resultCaptor.getValue()); - } - - @Test - public void queryPurchases_clientDisconnected() { - // Prepare the launch call after disconnecting the client - methodChannelHandler.onMethodCall(new MethodCall(END_CONNECTION, null), mock(Result.class)); - - HashMap arguments = new HashMap<>(); - arguments.put("skuType", SkuType.INAPP); - methodChannelHandler.onMethodCall(new MethodCall(QUERY_PURCHASES, arguments), result); - - // Assert that we sent an error back. - verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); - verify(result, never()).success(any()); - } - - @Test - public void queryPurchaseHistoryAsync() { - // Set up an established billing client and all our mocked responses - establishConnectedBillingClient(null, null); - ArgumentCaptor> resultCaptor = ArgumentCaptor.forClass(HashMap.class); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); - List purchasesList = asList(buildPurchaseHistoryRecord("foo")); - HashMap arguments = new HashMap<>(); - arguments.put("skuType", SkuType.INAPP); - ArgumentCaptor listenerCaptor = - ArgumentCaptor.forClass(PurchaseHistoryResponseListener.class); - - methodChannelHandler.onMethodCall( - new MethodCall(QUERY_PURCHASE_HISTORY_ASYNC, arguments), result); - - // Verify we pass the data to result - verify(mockBillingClient) - .queryPurchaseHistoryAsync(eq(SkuType.INAPP), listenerCaptor.capture()); - listenerCaptor.getValue().onPurchaseHistoryResponse(billingResult, purchasesList); - verify(result).success(resultCaptor.capture()); - HashMap resultData = resultCaptor.getValue(); - assertEquals(fromBillingResult(billingResult), resultData.get("billingResult")); - assertEquals( - fromPurchaseHistoryRecordList(purchasesList), resultData.get("purchaseHistoryRecordList")); - } - - @Test - public void queryPurchaseHistoryAsync_clientDisconnected() { - // Prepare the launch call after disconnecting the client - methodChannelHandler.onMethodCall(new MethodCall(END_CONNECTION, null), mock(Result.class)); - - HashMap arguments = new HashMap<>(); - arguments.put("skuType", SkuType.INAPP); - methodChannelHandler.onMethodCall( - new MethodCall(QUERY_PURCHASE_HISTORY_ASYNC, arguments), result); - - // Assert that we sent an error back. - verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); - verify(result, never()).success(any()); - } - - @Test - public void onPurchasesUpdatedListener() { - PluginPurchaseListener listener = new PluginPurchaseListener(mockMethodChannel); - - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); - List purchasesList = asList(buildPurchase("foo")); - ArgumentCaptor> resultCaptor = ArgumentCaptor.forClass(HashMap.class); - doNothing() - .when(mockMethodChannel) - .invokeMethod(eq(ON_PURCHASES_UPDATED), resultCaptor.capture()); - listener.onPurchasesUpdated(billingResult, purchasesList); - - HashMap resultData = resultCaptor.getValue(); - assertEquals(fromBillingResult(billingResult), resultData.get("billingResult")); - assertEquals(fromPurchasesList(purchasesList), resultData.get("purchasesList")); - } - - @Test - public void consumeAsync() { - establishConnectedBillingClient(null, null); - ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(BillingResult.class); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); - HashMap arguments = new HashMap<>(); - arguments.put("purchaseToken", "mockToken"); - arguments.put("developerPayload", "mockPayload"); - ArgumentCaptor listenerCaptor = - ArgumentCaptor.forClass(ConsumeResponseListener.class); - - methodChannelHandler.onMethodCall(new MethodCall(CONSUME_PURCHASE_ASYNC, arguments), result); - - ConsumeParams params = - ConsumeParams.newBuilder() - .setDeveloperPayload("mockPayload") - .setPurchaseToken("mockToken") - .build(); - - // Verify we pass the data to result - verify(mockBillingClient).consumeAsync(refEq(params), listenerCaptor.capture()); - - listenerCaptor.getValue().onConsumeResponse(billingResult, "mockToken"); - verify(result).success(resultCaptor.capture()); - - // 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 acknowledgePurchase() { - establishConnectedBillingClient(null, null); - ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(BillingResult.class); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); - HashMap arguments = new HashMap<>(); - arguments.put("purchaseToken", "mockToken"); - arguments.put("developerPayload", "mockPayload"); - ArgumentCaptor listenerCaptor = - ArgumentCaptor.forClass(AcknowledgePurchaseResponseListener.class); - - methodChannelHandler.onMethodCall(new MethodCall(ACKNOWLEDGE_PURCHASE, arguments), result); - - AcknowledgePurchaseParams params = - AcknowledgePurchaseParams.newBuilder() - .setDeveloperPayload("mockPayload") - .setPurchaseToken("mockToken") - .build(); - - // Verify we pass the data to result - verify(mockBillingClient).acknowledgePurchase(refEq(params), listenerCaptor.capture()); - - listenerCaptor.getValue().onAcknowledgePurchaseResponse(billingResult); - verify(result).success(resultCaptor.capture()); - - // 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 endConnection_if_activity_dettached() { - InAppPurchasePlugin plugin = new InAppPurchasePlugin(); - plugin.setMethodCallHandler(methodChannelHandler); - mockStartConnection(); - plugin.onDetachedFromActivity(); - verify(mockBillingClient).endConnection(); - } - - private ArgumentCaptor mockStartConnection() { - Map arguments = new HashMap<>(); - arguments.put("handle", 1); - arguments.put("enablePendingPurchases", true); - MethodCall call = new MethodCall(START_CONNECTION, arguments); - ArgumentCaptor captor = - ArgumentCaptor.forClass(BillingClientStateListener.class); - doNothing().when(mockBillingClient).startConnection(captor.capture()); - - methodChannelHandler.onMethodCall(call, result); - return captor; - } - - private void establishConnectedBillingClient( - @Nullable Map arguments, @Nullable Result result) { - if (arguments == null) { - arguments = new HashMap<>(); - arguments.put("handle", 1); - arguments.put("enablePendingPurchases", true); - } - if (result == null) { - result = mock(Result.class); - } - - MethodCall connectCall = new MethodCall(START_CONNECTION, arguments); - methodChannelHandler.onMethodCall(connectCall, result); - } - - private void queryForSkus(List skusList) { - // Set up the query method call - establishConnectedBillingClient(/* arguments= */ null, /* result= */ null); - HashMap arguments = new HashMap<>(); - String skuType = SkuType.INAPP; - arguments.put("skuType", skuType); - arguments.put("skusList", skusList); - MethodCall queryCall = new MethodCall(QUERY_SKU_DETAILS, arguments); - - // Call the method. - methodChannelHandler.onMethodCall(queryCall, mock(Result.class)); - - // Respond to the call with a matching set of Sku details. - ArgumentCaptor listenerCaptor = - ArgumentCaptor.forClass(SkuDetailsResponseListener.class); - verify(mockBillingClient).querySkuDetailsAsync(any(), listenerCaptor.capture()); - List skuDetailsResponse = - skusList.stream().map(this::buildSkuDetails).collect(toList()); - BillingResult billingResult = - BillingResult.newBuilder() - .setResponseCode(100) - .setDebugMessage("dummy debug message") - .build(); - listenerCaptor.getValue().onSkuDetailsResponse(billingResult, skuDetailsResponse); - } - - private SkuDetails buildSkuDetails(String id) { - SkuDetails details = mock(SkuDetails.class); - when(details.getSku()).thenReturn(id); - return details; - } - - private Purchase buildPurchase(String orderId) { - Purchase purchase = mock(Purchase.class); - when(purchase.getOrderId()).thenReturn(orderId); - return purchase; - } - - private PurchaseHistoryRecord buildPurchaseHistoryRecord(String purchaseToken) { - PurchaseHistoryRecord purchase = mock(PurchaseHistoryRecord.class); - when(purchase.getPurchaseToken()).thenReturn(purchaseToken); - return purchase; - } -} 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/android/gradle.properties b/packages/in_app_purchase/example/android/gradle.properties deleted file mode 100644 index 38c8d4544ff1..000000000000 --- a/packages/in_app_purchase/example/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true -android.useAndroidX=true -android.enableJetifier=true diff --git a/packages/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/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/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 83b4e743dc94..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 verity 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 0d595bd41fd4..000000000000 --- a/packages/in_app_purchase/example/pubspec.yaml +++ /dev/null @@ -1,25 +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: ../ - e2e: ^0.2.0 - 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/in_app_purchase_e2e_test.dart b/packages/in_app_purchase/example/test_driver/test/in_app_purchase_e2e_test.dart deleted file mode 100644 index 449af666b605..000000000000 --- a/packages/in_app_purchase/example/test_driver/test/in_app_purchase_e2e_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 'dart:io'; -import 'dart:async'; -import 'package:flutter_driver/flutter_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); -} 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..859d0bb6432f --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/CHANGELOG.md @@ -0,0 +1,502 @@ +## 1.0.9 + +* Handle purchases with `PurchaseStatus.restored` correctly in the example App. +* Updated dependencies on `in_app_purchase_android` and `in_app_purchase_ios` to their latest versions (version 0.1.5 and 0.1.3+5 respectively). + +## 1.0.8 + +* Fix repository link in pubspec.yaml. + +## 1.0.7 + +* Remove references to the Android V1 embedding. + +## 1.0.6 + +* Added import flutter foundation dependency in README.md to be able to use `defaultTargetPlatform`. + +## 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..61803e35ebdc --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/README.md @@ -0,0 +1,439 @@ +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'; +import 'package:flutter/foundation.dart'; + +void main() { + // Inform the plugin that this app supports pending purchases on Android. + // An error will occur on Android if you access the plugin `instance` + // without this call. + if (defaultTargetPlatform == TargetPlatform.android) { + InAppPurchaseAndroidPlatformAddition.enablePendingPurchases(); + } + runApp(MyApp()); +} +``` + +**Note:** It is not necessary to depend on `com.android.billingclient:billing` in your own app's `android/app/build.gradle` file. If you choose to do so know that conflicts might occur. + +### Listening to purchase updates + +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); +``` + +### Confirming subscription price changes + +When the price of a subscription is changed the consumer will need to confirm that price change. If the consumer does not +confirm the price change the subscription will not be auto-renewed. By default on both iOS and Android the consumer will +automatically get a popup to confirm the price change, but App developers can override this mechanism and show the popup on a later moment so it doesn't interrupt the critical flow of the App. This works different on the Apple App Store and on the Google Play Store. + +#### Google Play Store (Android) +When the subscription price is raised, the consumer should approve the price change within 7 days. The official +documentation can be found [here](https://support.google.com/googleplay/android-developer/answer/140504?hl=en#zippy=%2Cprice-changes). +When the price is lowered the consumer will automatically receive the lower price and does not have to approve the price change. + +After 7 days the consumer will be notified through email and notifications on Google Play to agree with the new price. App developers have 7 days to explain the consumer that the price is going to change and ask them to accept this change. App developers have to keep track of whether or not the price change is already accepted within the app or in the backend. The [Google Play API](https://developers.google.com/android-publisher/api-ref/rest/v3/purchases.subscriptions) can be used to check whether or not the price change is accepted by the consumer by reading the `priceChange` property on a subscription object. + +The `InAppPurchaseAndroidPlatformAddition` can be used to show the price change confirmation flow. The additions contain the function `launchPriceChangeConfirmationFlow` which needs the SKU code of the subscription. + +```dart +//import for InAppPurchaseAndroidPlatformAddition +import 'package:in_app_purchase_android/in_app_purchase_android.dart'; +//import for BillingResponse +import 'package:in_app_purchase_android/billing_client_wrappers.dart'; + +if (Platform.isAndroid) { + final InAppPurchaseAndroidPlatformAddition androidAddition = + _inAppPurchase + .getPlatformAddition(); + var priceChangeConfirmationResult = + await androidAddition.launchPriceChangeConfirmationFlow( + sku: 'purchaseId', + ); + if (priceChangeConfirmationResult.responseCode == BillingResponse.ok){ + // TODO acknowledge price change + }else{ + // TODO show error + } +} +``` + +#### Apple App Store (iOS) + +When the price of a subscription is raised iOS will also show a popup in the app. +The StoreKit Payment Queue will notify the app that it wants to show a price change confirmation popup. +By default the queue will get the response that it can continue and show the popup. +However, it is possible to prevent this popup via the InAppPurchaseIosPlatformAddition and show the +popup at a different time, for example after clicking a button. + +To know when the App Store wants to show a popup and prevent this from happening a queue delegate can be registered. +The `InAppPurchaseIosPlatformAddition` contains a `setDelegate(SKPaymentQueueDelegateWrapper? delegate)` function that +can be used to set a delegate or remove one by setting it to `null`. +```dart +//import for InAppPurchaseIosPlatformAddition +import 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; + +Future initStoreInfo() async { + if (Platform.isIOS) { + var iosPlatformAddition = _inAppPurchase + .getPlatformAddition(); + await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate()); + } +} + +@override +Future disposeStore() { + if (Platform.isIOS) { + var iosPlatformAddition = _inAppPurchase + .getPlatformAddition(); + await iosPlatformAddition.setDelegate(null); + } +} +``` +The delegate that is set should implement `SKPaymentQueueDelegateWrapper` and handle `shouldContinueTransaction` and +`shouldShowPriceConsent`. When setting `shouldShowPriceConsent` to false the default popup will not be shown and the app +needs to show this later. + +```dart +// import for SKPaymentQueueDelegateWrapper +import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; + +class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper { + @override + bool shouldContinueTransaction( + SKPaymentTransactionWrapper transaction, SKStorefrontWrapper storefront) { + return true; + } + + @override + bool shouldShowPriceConsent() { + return false; + } +} +``` + +The dialog can be shown by calling `showPriceConsentIfNeeded` on the `InAppPurchaseIosPlatformAddition`. This future +will complete immediately when the dialog is shown. A confirmed transaction will be delivered on the `purchaseStream`. +```dart +if (Platform.isIOS) { + var iapIosPlatformAddition = _inAppPurchase + .getPlatformAddition(); + await iapIosPlatformAddition.showPriceConsentIfNeeded(); +} +``` + +### 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/device_info/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/device_info/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/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 new file mode 100644 index 000000000000..027375c09e04 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + diff --git a/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/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..03e4066de85e --- /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,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.inapppurchaseexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class FlutterActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/e2e/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/e2e/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/device_info/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 similarity index 100% rename from packages/device_info/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/device_info/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 similarity index 100% rename from packages/device_info/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/device_info/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 similarity index 100% rename from packages/device_info/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/device_info/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 similarity index 100% rename from packages/device_info/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/device_info/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 similarity index 100% rename from packages/device_info/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/e2e/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/e2e/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/google_sign_in/google_sign_in/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/google_sign_in/google_sign_in/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..0b4cf534e0aa --- /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:4.1.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/device_info/example/android/gradle.properties b/packages/in_app_purchase/in_app_purchase/example/android/gradle.properties similarity index 100% rename from packages/device_info/example/android/gradle.properties rename to packages/in_app_purchase/in_app_purchase/example/android/gradle.properties diff --git a/packages/in_app_purchase/in_app_purchase/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/in_app_purchase/in_app_purchase/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..bc6a58afdda2 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/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-6.7-all.zip 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/e2e/example/android/settings.gradle b/packages/in_app_purchase/in_app_purchase/example/android/settings.gradle similarity index 100% rename from packages/e2e/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/battery/example/ios/Flutter/Debug.xcconfig b/packages/in_app_purchase/in_app_purchase/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/battery/example/ios/Flutter/Debug.xcconfig rename to packages/in_app_purchase/in_app_purchase/example/ios/Flutter/Debug.xcconfig diff --git a/packages/battery/example/ios/Flutter/Release.xcconfig b/packages/in_app_purchase/in_app_purchase/example/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/battery/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/battery/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/battery/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/camera/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/camera/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/e2e/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/e2e/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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/camera/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..3cf7229fdbc2 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase/example/lib/main.dart @@ -0,0 +1,525 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; +import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; +import 'consumable_store.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + + if (defaultTargetPlatform == TargetPlatform.android) { + // For play billing library 2.0 on Android, it is mandatory to call + // [enablePendingPurchases](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.Builder.html#enablependingpurchases) + // as part of initializing the app. + InAppPurchaseAndroidPlatformAddition.enablePendingPurchases(); + } + + runApp(_MyApp()); +} + +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; + } + + if (Platform.isIOS) { + var iosPlatformAddition = _inAppPurchase + .getPlatformAddition(); + await iosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate()); + } + + 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() { + if (Platform.isIOS) { + var iosPlatformAddition = _inAppPurchase + .getPlatformAddition(); + iosPlatformAddition.setDelegate(null); + } + _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 + ? IconButton( + onPressed: () => confirmPriceChange(context), + icon: Icon(Icons.upgrade)) + : TextButton( + child: Text(productDetails.price), + style: TextButton.styleFrom( + backgroundColor: Colors.green[800], + primary: Colors.white, + ), + onPressed: () { + late PurchaseParam purchaseParam; + + if (Platform.isAndroid) { + // NOTE: If you are making a subscription purchase/upgrade/downgrade, we recommend you to + // verify the latest status of you your subscription by using server side receipt validation + // and update the UI accordingly. The subscription purchase status shown + // inside the app may not be accurate. + final oldSubscription = + _getOldSubscription(productDetails, purchases); + + purchaseParam = GooglePlayPurchaseParam( + productDetails: productDetails, + applicationUserName: null, + changeSubscriptionParam: (oldSubscription != null) + ? ChangeSubscriptionParam( + oldPurchaseDetails: oldSubscription, + prorationMode: ProrationMode + .immediateWithTimeProration, + ) + : null); + } else { + purchaseParam = PurchaseParam( + 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 || + purchaseDetails.status == PurchaseStatus.restored) { + 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); + } + } + }); + } + + Future confirmPriceChange(BuildContext context) async { + if (Platform.isAndroid) { + final InAppPurchaseAndroidPlatformAddition androidAddition = + _inAppPurchase + .getPlatformAddition(); + var priceChangeConfirmationResult = + await androidAddition.launchPriceChangeConfirmationFlow( + sku: 'purchaseId', + ); + if (priceChangeConfirmationResult.responseCode == BillingResponse.ok) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text('Price change accepted'), + )); + } else { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text( + priceChangeConfirmationResult.debugMessage ?? + "Price change failed with code ${priceChangeConfirmationResult.responseCode}", + ), + )); + } + } + if (Platform.isIOS) { + var iapIosPlatformAddition = _inAppPurchase + .getPlatformAddition(); + await iapIosPlatformAddition.showPriceConsentIfNeeded(); + } + } + + 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; + } +} + +/// Example implementation of the +/// [`SKPaymentQueueDelegate`](https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate?language=objc). +/// +/// The payment queue delegate can be implementated to provide information +/// needed to complete transactions. +class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper { + @override + bool shouldContinueTransaction( + SKPaymentTransactionWrapper transaction, SKStorefrontWrapper storefront) { + return true; + } + + @override + bool shouldShowPriceConsent() { + return false; + } +} 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..96570f7aa168 --- /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/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.9 + +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.5 + in_app_purchase_ios: ^0.1.3+5 + +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/AUTHORS b/packages/in_app_purchase/in_app_purchase_android/AUTHORS new file mode 100644 index 000000000000..78f9e5ad9f6b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/AUTHORS @@ -0,0 +1,67 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Maurits van Beusekom 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..a01eb9f8b70a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md @@ -0,0 +1,64 @@ +## 0.1.5+1 + +* Fix a broken link in the README. +## 0.1.5 + +* Introduced the `SkuDetailsWrapper.introductoryPriceAmountMicros` field of the correct type (`int`) and deprecated the `SkuDetailsWrapper.introductoryPriceMicros` field. +* Update dev_dependency `build_runner` to ^2.0.0 and `json_serializable` to ^5.0.2. + +## 0.1.4+7 + +* Ensure that the `SkuDetailsWrapper.introductoryPriceMicros` is populated correctly. + +## 0.1.4+6 + +* Ensure that purchases correctly indicate whether they are acknowledged or not. The `PurchaseDetails.pendingCompletePurchase` field now correctly indicates if the purchase still needs to be completed. + +## 0.1.4+5 + +* Add `implements` to pubspec. +* Updated Android lint settings. + +## 0.1.4+4 + +* Removed dependency on the `test` package. + +## 0.1.4+3 + +* Updated installation instructions in README. + +## 0.1.4+2 + +* Added price currency symbol to SkuDetailsWrapper. + +## 0.1.4+1 + +* Fixed typos. + +## 0.1.4 + +* Added support for launchPriceChangeConfirmationFlow in the BillingClientWrapper and in InAppPurchaseAndroidPlatformAddition. + +## 0.1.3+1 + +* Add payment proxy. + +## 0.1.3 + +* Added support for isFeatureSupported in the BillingClientWrapper and in InAppPurchaseAndroidPlatformAddition. + +## 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..d64fbfb8c49a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/README.md @@ -0,0 +1,29 @@ +# in\_app\_purchase\_android + +The Android implementation of [`in_app_purchase`][1]. + +## Usage + +This package has been [endorsed][2], meaning that you only need to add `in_app_purchase` +as a dependency in your `pubspec.yaml`. This package will be automatically included in your app +when you do. + +If you wish to use the Android package only, you can [add `in_app_purchase_android` directly][3]. + +## Contributing + +This plugin uses +[json_serializable](https://pub.dev/packages/json_serializable) for the +many data structs passed between the underlying platform layers and Dart. After +editing any of the serialized data structs, rebuild the serializers by running +`flutter packages pub run build_runner build --delete-conflicting-outputs`. +`flutter packages pub run build_runner watch --delete-conflicting-outputs` will +watch the filesystem for changes. + +If you would like to contribute to the plugin, check out our +[contribution guide](https://github.com/flutter/plugins/blob/master/CONTRIBUTING.md). + + +[1]: https://pub.dev/packages/in_app_purchase +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin +[3]: https://pub.dev/packages/in_app_purchase_android/install diff --git a/packages/in_app_purchase/in_app_purchase_android/analysis_options.yaml b/packages/in_app_purchase/in_app_purchase_android/analysis_options.yaml 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..656f7c34bf7a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/android/build.gradle @@ -0,0 +1,62 @@ +group 'io.flutter.plugins.inapppurchase' +version '1.0-SNAPSHOT' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:4.1.0' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 29 + + defaultConfig { + minSdkVersion 16 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + lintOptions { + disable 'InvalidPackage' + disable 'GradleDependency' + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} + +dependencies { + implementation 'androidx.annotation:annotation:1.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/in_app_purchase_android/android/gradle/wrapper/gradle-wrapper.properties b/packages/in_app_purchase/in_app_purchase_android/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..baf2285f8c53 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Oct 29 10:30:44 PDT 2018 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip 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 78% 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 de2080cfebf2..b21ab6992608 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. @@ -14,11 +14,17 @@ import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.PluginRegistry.Registrar; /** Wraps a {@link BillingClient} instance and responds to Dart calls for it. */ public class InAppPurchasePlugin implements FlutterPlugin, ActivityAware { + static final String PROXY_PACKAGE_KEY = "PROXY_PACKAGE"; + // The proxy value has to match the value in library's AndroidManifest.xml. + // This is important that the is not changed, so we hard code the value here then having + // a unit test to make sure. If there is a strong reason to change the value, please inform the + // code owner of this package. + static final String PROXY_VALUE = "io.flutter.plugins.inapppurchase"; + @VisibleForTesting static final class MethodNames { static final String IS_READY = "BillingClient#isReady()"; @@ -39,6 +45,9 @@ static final class MethodNames { "BillingClient#consumeAsync(String, ConsumeResponseListener)"; static final String ACKNOWLEDGE_PURCHASE = "BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)"; + static final String IS_FEATURE_SUPPORTED = "BillingClient#isFeatureSupported(String)"; + static final String LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW = + "BillingClient#launchPriceChangeConfirmationFlow (Activity, PriceChangeFlowParams, PriceChangeConfirmationListener)"; private MethodNames() {}; } @@ -47,9 +56,10 @@ static final class MethodNames { private MethodCallHandlerImpl methodCallHandler; /** Plugin registration. */ - public static void registerWith(Registrar registrar) { + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { InAppPurchasePlugin plugin = new InAppPurchasePlugin(); - plugin.setupMethodChannel(registrar.activity(), registrar.messenger(), registrar.context()); + registrar.activity().getIntent().putExtra(PROXY_PACKAGE_KEY, PROXY_VALUE); ((Application) registrar.context().getApplicationContext()) .registerActivityLifecycleCallbacks(plugin.methodCallHandler); } @@ -67,6 +77,7 @@ public void onDetachedFromEngine(FlutterPlugin.FlutterPluginBinding binding) { @Override public void onAttachedToActivity(ActivityPluginBinding binding) { + binding.getActivity().getIntent().putExtra(PROXY_PACKAGE_KEY, PROXY_VALUE); methodCallHandler.setActivity(binding.getActivity()); } 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..5b58808b2b49 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java @@ -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. + +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.PriceChangeFlowParams; +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/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; + case InAppPurchasePlugin.MethodNames.IS_FEATURE_SUPPORTED: + isFeatureSupported((String) call.argument("feature"), result); + break; + case InAppPurchasePlugin.MethodNames.LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW: + launchPriceChangeConfirmationFlow((String) call.argument("sku"), 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 onBillingSetupFinished 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 void launchPriceChangeConfirmationFlow(String sku, MethodChannel.Result result) { + if (activity == null) { + result.error( + "ACTIVITY_UNAVAILABLE", + "launchPriceChangeConfirmationFlow is not available. " + + "This method must be run with the app in foreground.", + null); + return; + } + if (billingClientError(result)) { + return; + } + // Note that assert doesn't work on Android (see https://stackoverflow.com/a/6176529/5167831 and https://stackoverflow.com/a/8164195/5167831) + // and that this assert is only added to silence the analyser. The actual null check + // is handled by the `billingClientError()` call. + assert billingClient != null; + + 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; + } + + PriceChangeFlowParams params = + new PriceChangeFlowParams.Builder().setSkuDetails(skuDetails).build(); + billingClient.launchPriceChangeConfirmationFlow( + activity, + params, + billingResult -> { + result.success(Translator.fromBillingResult(billingResult)); + }); + } + + private boolean billingClientError(MethodChannel.Result result) { + if (billingClient != null) { + return false; + } + + result.error("UNAVAILABLE", "BillingClient is unset. Try reconnecting.", null); + return true; + } + + private void isFeatureSupported(String feature, MethodChannel.Result result) { + if (billingClientError(result)) { + return; + } + assert billingClient != null; + BillingResult billingResult = billingClient.isFeatureSupported(feature); + result.success(billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK); + } +} 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 76% 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..7546fe7db58d 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; @@ -12,8 +13,10 @@ import com.android.billingclient.api.SkuDetails; import java.util.ArrayList; import java.util.Collections; +import java.util.Currency; import java.util.HashMap; import java.util.List; +import java.util.Locale; /** Handles serialization of {@link com.android.billingclient.api.BillingClient} related objects. */ /*package*/ class Translator { @@ -29,9 +32,9 @@ static HashMap fromSkuDetail(SkuDetails detail) { info.put("price", detail.getPrice()); info.put("priceAmountMicros", detail.getPriceAmountMicros()); info.put("priceCurrencyCode", detail.getPriceCurrencyCode()); + info.put("priceCurrencySymbol", currencySymbolFromCode(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 +67,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; } @@ -118,4 +126,21 @@ static HashMap fromBillingResult(BillingResult billingResult) { info.put("debugMessage", billingResult.getDebugMessage()); return info; } + + /** + * Gets the symbol of for the given currency code for the default {@link Locale.Category#DISPLAY + * DISPLAY} locale. For example, for the US Dollar, the symbol is "$" if the default locale is the + * US, while for other locales it may be "US$". If no symbol can be determined, the ISO 4217 + * currency code is returned. + * + * @param currencyCode the ISO 4217 code of the currency + * @return the symbol of this currency code for the default {@link Locale.Category#DISPLAY + * DISPLAY} locale + * @exception NullPointerException if currencyCode is null + * @exception IllegalArgumentException if currencyCode is not a supported ISO 4217 + * code. + */ + static String currencySymbolFromCode(String currencyCode) { + return Currency.getInstance(currencyCode).getSymbol(); + } } 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/in_app_purchase_android/android/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 new file mode 100644 index 000000000000..ad7633903275 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/InAppPurchasePluginTest.java @@ -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. + +package io.flutter.plugins.inapppurchase; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.app.Application; +import android.content.Context; +import android.content.Intent; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.PluginRegistry; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +public class InAppPurchasePluginTest { + + static final String PROXY_PACKAGE_KEY = "PROXY_PACKAGE"; + + @Mock Activity activity; + @Mock Context context; + @Mock PluginRegistry.Registrar mockRegistrar; // For v1 embedding + @Mock BinaryMessenger mockMessenger; + @Mock Application mockApplication; + @Mock Intent mockIntent; + @Mock ActivityPluginBinding activityPluginBinding; + @Mock FlutterPlugin.FlutterPluginBinding flutterPluginBinding; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mockRegistrar.activity()).thenReturn(activity); + when(mockRegistrar.messenger()).thenReturn(mockMessenger); + when(mockRegistrar.context()).thenReturn(context); + when(activity.getIntent()).thenReturn(mockIntent); + when(activityPluginBinding.getActivity()).thenReturn(activity); + when(flutterPluginBinding.getBinaryMessenger()).thenReturn(mockMessenger); + when(flutterPluginBinding.getApplicationContext()).thenReturn(context); + } + + @Test + public void registerWith_doNotCrashWhenRegisterContextIsActivity_V1Embedding() { + when(mockRegistrar.context()).thenReturn(activity); + when(activity.getApplicationContext()).thenReturn(mockApplication); + InAppPurchasePlugin.registerWith(mockRegistrar); + } + + // The PROXY_PACKAGE_KEY value of this test (io.flutter.plugins.inapppurchase) should never be changed. + // In case there's a strong reason to change it, please inform the current code owner of the plugin. + @Test + public void registerWith_proxyIsSet_V1Embedding() { + when(mockRegistrar.context()).thenReturn(activity); + when(activity.getApplicationContext()).thenReturn(mockApplication); + InAppPurchasePlugin.registerWith(mockRegistrar); + // The `PROXY_PACKAGE_KEY` value is hard coded in the plugin code as "io.flutter.plugins.inapppurchase". + // We cannot use `BuildConfig.LIBRARY_PACKAGE_NAME` directly in the plugin code because whether to read BuildConfig.APPLICATION_ID or LIBRARY_PACKAGE_NAME + // depends on the "APP's" Android Gradle plugin version. Newer versions of AGP use LIBRARY_PACKAGE_NAME, whereas older ones use BuildConfig.APPLICATION_ID. + Mockito.verify(mockIntent).putExtra(PROXY_PACKAGE_KEY, "io.flutter.plugins.inapppurchase"); + assertEquals("io.flutter.plugins.inapppurchase", BuildConfig.LIBRARY_PACKAGE_NAME); + } + + // The PROXY_PACKAGE_KEY value of this test (io.flutter.plugins.inapppurchase) should never be changed. + // In case there's a strong reason to change it, please inform the current code owner of the plugin. + @Test + public void attachToActivity_proxyIsSet_V2Embedding() { + InAppPurchasePlugin plugin = new InAppPurchasePlugin(); + plugin.onAttachedToEngine(flutterPluginBinding); + plugin.onAttachedToActivity(activityPluginBinding); + // The `PROXY_PACKAGE_KEY` value is hard coded in the plugin code as "io.flutter.plugins.inapppurchase". + // We cannot use `BuildConfig.LIBRARY_PACKAGE_NAME` directly in the plugin code because whether to read BuildConfig.APPLICATION_ID or LIBRARY_PACKAGE_NAME + // depends on the "APP's" Android Gradle plugin version. Newer versions of AGP use LIBRARY_PACKAGE_NAME, whereas older ones use BuildConfig.APPLICATION_ID. + Mockito.verify(mockIntent).putExtra(PROXY_PACKAGE_KEY, "io.flutter.plugins.inapppurchase"); + assertEquals("io.flutter.plugins.inapppurchase", BuildConfig.LIBRARY_PACKAGE_NAME); + } +} +// We cannot use `BuildConfig.LIBRARY_PACKAGE_NAME` directly in the plugin code because whether to read BuildConfig.APPLICATION_ID or LIBRARY_PACKAGE_NAME +// depends on the "APP's" Android Gradle plugin version. Newer versions of AGP use LIBRARY_PACKAGE_NAME, whereas older ones use BuildConfig.APPLICATION_ID. diff --git a/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java new file mode 100644 index 000000000000..6f9256cd07bd --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/android/src/test/java/io/flutter/plugins/inapppurchase/MethodCallHandlerTest.java @@ -0,0 +1,945 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.inapppurchase; + +import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.ACKNOWLEDGE_PURCHASE; +import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.CONSUME_PURCHASE_ASYNC; +import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.END_CONNECTION; +import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.IS_FEATURE_SUPPORTED; +import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.IS_READY; +import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.LAUNCH_BILLING_FLOW; +import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW; +import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.ON_DISCONNECT; +import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.ON_PURCHASES_UPDATED; +import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.QUERY_PURCHASES; +import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.QUERY_PURCHASE_HISTORY_ASYNC; +import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.QUERY_SKU_DETAILS; +import static io.flutter.plugins.inapppurchase.InAppPurchasePlugin.MethodNames.START_CONNECTION; +import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult; +import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList; +import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList; +import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesResult; +import static io.flutter.plugins.inapppurchase.Translator.fromSkuDetailsList; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.Collections.unmodifiableList; +import static java.util.stream.Collectors.toList; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.contains; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.refEq; +import static org.mockito.Mockito.doNothing; +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.app.Activity; +import android.content.Context; +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.BillingClient.SkuType; +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.PriceChangeConfirmationListener; +import com.android.billingclient.api.PriceChangeFlowParams; +import com.android.billingclient.api.Purchase; +import com.android.billingclient.api.Purchase.PurchasesResult; +import com.android.billingclient.api.PurchaseHistoryRecord; +import com.android.billingclient.api.PurchaseHistoryResponseListener; +import com.android.billingclient.api.SkuDetails; +import com.android.billingclient.api.SkuDetailsParams; +import com.android.billingclient.api.SkuDetailsResponseListener; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.Result; +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; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.Spy; + +public class MethodCallHandlerTest { + private MethodCallHandlerImpl methodChannelHandler; + private BillingClientFactory factory; + @Mock BillingClient mockBillingClient; + @Mock MethodChannel mockMethodChannel; + @Spy Result result; + @Mock Activity activity; + @Mock Context context; + @Mock ActivityPluginBinding mockActivityPluginBinding; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + factory = + (@NonNull Context context, + @NonNull MethodChannel channel, + boolean enablePendingPurchases) -> mockBillingClient; + methodChannelHandler = new MethodCallHandlerImpl(activity, context, mockMethodChannel, factory); + when(mockActivityPluginBinding.getActivity()).thenReturn(activity); + } + + @Test + public void invalidMethod() { + MethodCall call = new MethodCall("invalid", null); + methodChannelHandler.onMethodCall(call, result); + verify(result, times(1)).notImplemented(); + } + + @Test + public void isReady_true() { + mockStartConnection(); + MethodCall call = new MethodCall(IS_READY, null); + when(mockBillingClient.isReady()).thenReturn(true); + methodChannelHandler.onMethodCall(call, result); + verify(result).success(true); + } + + @Test + public void isReady_false() { + mockStartConnection(); + MethodCall call = new MethodCall(IS_READY, null); + when(mockBillingClient.isReady()).thenReturn(false); + methodChannelHandler.onMethodCall(call, result); + verify(result).success(false); + } + + @Test + public void isReady_clientDisconnected() { + MethodCall disconnectCall = new MethodCall(END_CONNECTION, null); + methodChannelHandler.onMethodCall(disconnectCall, mock(Result.class)); + MethodCall isReadyCall = new MethodCall(IS_READY, null); + + methodChannelHandler.onMethodCall(isReadyCall, result); + + verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); + verify(result, never()).success(any()); + } + + @Test + public void startConnection() { + ArgumentCaptor captor = mockStartConnection(); + verify(result, never()).success(any()); + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + captor.getValue().onBillingSetupFinished(billingResult); + + verify(result, times(1)).success(fromBillingResult(billingResult)); + } + + @Test + public void startConnection_multipleCalls() { + Map arguments = new HashMap<>(); + arguments.put("handle", 1); + arguments.put("enablePendingPurchases", true); + MethodCall call = new MethodCall(START_CONNECTION, arguments); + ArgumentCaptor captor = + ArgumentCaptor.forClass(BillingClientStateListener.class); + doNothing().when(mockBillingClient).startConnection(captor.capture()); + + methodChannelHandler.onMethodCall(call, result); + verify(result, never()).success(any()); + BillingResult billingResult1 = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + BillingResult billingResult2 = + BillingResult.newBuilder() + .setResponseCode(200) + .setDebugMessage("dummy debug message") + .build(); + BillingResult billingResult3 = + BillingResult.newBuilder() + .setResponseCode(300) + .setDebugMessage("dummy debug message") + .build(); + + captor.getValue().onBillingSetupFinished(billingResult1); + captor.getValue().onBillingSetupFinished(billingResult2); + captor.getValue().onBillingSetupFinished(billingResult3); + + verify(result, times(1)).success(fromBillingResult(billingResult1)); + verify(result, times(1)).success(any()); + } + + @Test + public void endConnection() { + // Set up a connected BillingClient instance + final int disconnectCallbackHandle = 22; + Map arguments = new HashMap<>(); + arguments.put("handle", disconnectCallbackHandle); + arguments.put("enablePendingPurchases", true); + MethodCall connectCall = new MethodCall(START_CONNECTION, arguments); + ArgumentCaptor captor = + ArgumentCaptor.forClass(BillingClientStateListener.class); + doNothing().when(mockBillingClient).startConnection(captor.capture()); + methodChannelHandler.onMethodCall(connectCall, mock(Result.class)); + final BillingClientStateListener stateListener = captor.getValue(); + + // Disconnect the connected client + MethodCall disconnectCall = new MethodCall(END_CONNECTION, null); + methodChannelHandler.onMethodCall(disconnectCall, result); + + // Verify that the client is disconnected and that the OnDisconnect callback has + // been triggered + verify(result, times(1)).success(any()); + verify(mockBillingClient, times(1)).endConnection(); + stateListener.onBillingServiceDisconnected(); + Map expectedInvocation = new HashMap<>(); + expectedInvocation.put("handle", disconnectCallbackHandle); + verify(mockMethodChannel, times(1)).invokeMethod(ON_DISCONNECT, expectedInvocation); + } + + @Test + public void querySkuDetailsAsync() { + // Connect a billing client and set up the SKU query listeners + establishConnectedBillingClient(/* arguments= */ null, /* result= */ null); + String skuType = BillingClient.SkuType.INAPP; + List skusList = asList("id1", "id2"); + HashMap arguments = new HashMap<>(); + arguments.put("skuType", skuType); + arguments.put("skusList", skusList); + MethodCall queryCall = new MethodCall(QUERY_SKU_DETAILS, arguments); + + // Query for SKU details + methodChannelHandler.onMethodCall(queryCall, result); + + // Assert the arguments were forwarded correctly to BillingClient + ArgumentCaptor paramCaptor = ArgumentCaptor.forClass(SkuDetailsParams.class); + ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(SkuDetailsResponseListener.class); + verify(mockBillingClient).querySkuDetailsAsync(paramCaptor.capture(), listenerCaptor.capture()); + assertEquals(paramCaptor.getValue().getSkuType(), skuType); + assertEquals(paramCaptor.getValue().getSkusList(), skusList); + + // Assert that we handed result BillingClient's response + int responseCode = 200; + List skuDetailsResponse = asList(buildSkuDetails("foo")); + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + listenerCaptor.getValue().onSkuDetailsResponse(billingResult, skuDetailsResponse); + ArgumentCaptor> resultCaptor = ArgumentCaptor.forClass(HashMap.class); + verify(result).success(resultCaptor.capture()); + HashMap resultData = resultCaptor.getValue(); + assertEquals(resultData.get("billingResult"), fromBillingResult(billingResult)); + assertEquals(resultData.get("skuDetailsList"), fromSkuDetailsList(skuDetailsResponse)); + } + + @Test + public void querySkuDetailsAsync_clientDisconnected() { + // Disconnect the Billing client and prepare a querySkuDetails call + MethodCall disconnectCall = new MethodCall(END_CONNECTION, null); + methodChannelHandler.onMethodCall(disconnectCall, mock(Result.class)); + String skuType = BillingClient.SkuType.INAPP; + List skusList = asList("id1", "id2"); + HashMap arguments = new HashMap<>(); + arguments.put("skuType", skuType); + arguments.put("skusList", skusList); + MethodCall queryCall = new MethodCall(QUERY_SKU_DETAILS, arguments); + + // Query for SKU details + methodChannelHandler.onMethodCall(queryCall, result); + + // Assert that we sent an error back. + verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); + 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_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 + 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); + + // 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); + + // 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); + MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); + methodChannelHandler.onMethodCall(launchCall, result); + + // Verify we pass the response code to result + verify(result).error(contains("ACTIVITY_UNAVAILABLE"), contains("foreground"), any()); + 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 + String skuId = "foo"; + String accountId = "account"; + queryForSkus(singletonList(skuId)); + HashMap arguments = new HashMap<>(); + arguments.put("sku", skuId); + arguments.put("accountId", accountId); + 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); + + // 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 + MethodCall disconnectCall = new MethodCall(END_CONNECTION, null); + methodChannelHandler.onMethodCall(disconnectCall, mock(Result.class)); + String skuId = "foo"; + String accountId = "account"; + HashMap arguments = new HashMap<>(); + arguments.put("sku", skuId); + arguments.put("accountId", accountId); + MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); + + methodChannelHandler.onMethodCall(launchCall, result); + + // Assert that we sent an error back. + verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); + verify(result, never()).success(any()); + } + + @Test + public void launchBillingFlow_skuNotFound() { + // Try to launch the billing flow for a random sku ID + establishConnectedBillingClient(null, null); + String skuId = "foo"; + String accountId = "account"; + HashMap arguments = new HashMap<>(); + arguments.put("sku", skuId); + arguments.put("accountId", accountId); + MethodCall launchCall = new MethodCall(LAUNCH_BILLING_FLOW, arguments); + + methodChannelHandler.onMethodCall(launchCall, result); + + // Assert that we sent an error back. + verify(result).error(contains("NOT_FOUND"), contains(skuId), any()); + 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); + PurchasesResult purchasesResult = mock(PurchasesResult.class); + Purchase purchase = buildPurchase("foo"); + when(purchasesResult.getPurchasesList()).thenReturn(asList(purchase)); + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + when(purchasesResult.getBillingResult()).thenReturn(billingResult); + when(mockBillingClient.queryPurchases(SkuType.INAPP)).thenReturn(purchasesResult); + + HashMap arguments = new HashMap<>(); + arguments.put("skuType", SkuType.INAPP); + methodChannelHandler.onMethodCall(new MethodCall(QUERY_PURCHASES, arguments), result); + + // Verify we pass the response to result + ArgumentCaptor> resultCaptor = ArgumentCaptor.forClass(HashMap.class); + verify(result, never()).error(any(), any(), any()); + verify(result, times(1)).success(resultCaptor.capture()); + assertEquals(fromPurchasesResult(purchasesResult), resultCaptor.getValue()); + } + + @Test + public void queryPurchases_clientDisconnected() { + // Prepare the launch call after disconnecting the client + methodChannelHandler.onMethodCall(new MethodCall(END_CONNECTION, null), mock(Result.class)); + + HashMap arguments = new HashMap<>(); + arguments.put("skuType", SkuType.INAPP); + methodChannelHandler.onMethodCall(new MethodCall(QUERY_PURCHASES, arguments), result); + + // Assert that we sent an error back. + verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); + verify(result, never()).success(any()); + } + + @Test + public void queryPurchaseHistoryAsync() { + // Set up an established billing client and all our mocked responses + establishConnectedBillingClient(null, null); + ArgumentCaptor> resultCaptor = ArgumentCaptor.forClass(HashMap.class); + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + List purchasesList = asList(buildPurchaseHistoryRecord("foo")); + HashMap arguments = new HashMap<>(); + arguments.put("skuType", SkuType.INAPP); + ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(PurchaseHistoryResponseListener.class); + + methodChannelHandler.onMethodCall( + new MethodCall(QUERY_PURCHASE_HISTORY_ASYNC, arguments), result); + + // Verify we pass the data to result + verify(mockBillingClient) + .queryPurchaseHistoryAsync(eq(SkuType.INAPP), listenerCaptor.capture()); + listenerCaptor.getValue().onPurchaseHistoryResponse(billingResult, purchasesList); + verify(result).success(resultCaptor.capture()); + HashMap resultData = resultCaptor.getValue(); + assertEquals(fromBillingResult(billingResult), resultData.get("billingResult")); + assertEquals( + fromPurchaseHistoryRecordList(purchasesList), resultData.get("purchaseHistoryRecordList")); + } + + @Test + public void queryPurchaseHistoryAsync_clientDisconnected() { + // Prepare the launch call after disconnecting the client + methodChannelHandler.onMethodCall(new MethodCall(END_CONNECTION, null), mock(Result.class)); + + HashMap arguments = new HashMap<>(); + arguments.put("skuType", SkuType.INAPP); + methodChannelHandler.onMethodCall( + new MethodCall(QUERY_PURCHASE_HISTORY_ASYNC, arguments), result); + + // Assert that we sent an error back. + verify(result).error(contains("UNAVAILABLE"), contains("BillingClient"), any()); + verify(result, never()).success(any()); + } + + @Test + public void onPurchasesUpdatedListener() { + PluginPurchaseListener listener = new PluginPurchaseListener(mockMethodChannel); + + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + List purchasesList = asList(buildPurchase("foo")); + ArgumentCaptor> resultCaptor = ArgumentCaptor.forClass(HashMap.class); + doNothing() + .when(mockMethodChannel) + .invokeMethod(eq(ON_PURCHASES_UPDATED), resultCaptor.capture()); + listener.onPurchasesUpdated(billingResult, purchasesList); + + HashMap resultData = resultCaptor.getValue(); + assertEquals(fromBillingResult(billingResult), resultData.get("billingResult")); + assertEquals(fromPurchasesList(purchasesList), resultData.get("purchasesList")); + } + + @Test + public void consumeAsync() { + establishConnectedBillingClient(null, null); + ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(BillingResult.class); + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + HashMap arguments = new HashMap<>(); + arguments.put("purchaseToken", "mockToken"); + arguments.put("developerPayload", "mockPayload"); + ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(ConsumeResponseListener.class); + + methodChannelHandler.onMethodCall(new MethodCall(CONSUME_PURCHASE_ASYNC, arguments), result); + + ConsumeParams params = ConsumeParams.newBuilder().setPurchaseToken("mockToken").build(); + + // Verify we pass the data to result + verify(mockBillingClient).consumeAsync(refEq(params), listenerCaptor.capture()); + + listenerCaptor.getValue().onConsumeResponse(billingResult, "mockToken"); + verify(result).success(resultCaptor.capture()); + + // 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 acknowledgePurchase() { + establishConnectedBillingClient(null, null); + ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(BillingResult.class); + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + HashMap arguments = new HashMap<>(); + arguments.put("purchaseToken", "mockToken"); + arguments.put("developerPayload", "mockPayload"); + ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(AcknowledgePurchaseResponseListener.class); + + methodChannelHandler.onMethodCall(new MethodCall(ACKNOWLEDGE_PURCHASE, arguments), result); + + AcknowledgePurchaseParams params = + AcknowledgePurchaseParams.newBuilder().setPurchaseToken("mockToken").build(); + + // Verify we pass the data to result + verify(mockBillingClient).acknowledgePurchase(refEq(params), listenerCaptor.capture()); + + listenerCaptor.getValue().onAcknowledgePurchaseResponse(billingResult); + verify(result).success(resultCaptor.capture()); + + // 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 endConnection_if_activity_detached() { + InAppPurchasePlugin plugin = new InAppPurchasePlugin(); + plugin.setMethodCallHandler(methodChannelHandler); + mockStartConnection(); + plugin.onDetachedFromActivity(); + verify(mockBillingClient).endConnection(); + } + + @Test + public void isFutureSupported_true() { + mockStartConnection(); + final String feature = "subscriptions"; + Map arguments = new HashMap<>(); + arguments.put("feature", feature); + + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(BillingClient.BillingResponseCode.OK) + .setDebugMessage("dummy debug message") + .build(); + + MethodCall call = new MethodCall(IS_FEATURE_SUPPORTED, arguments); + when(mockBillingClient.isFeatureSupported(feature)).thenReturn(billingResult); + methodChannelHandler.onMethodCall(call, result); + verify(result).success(true); + } + + @Test + public void isFutureSupported_false() { + mockStartConnection(); + final String feature = "subscriptions"; + Map arguments = new HashMap<>(); + arguments.put("feature", feature); + + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED) + .setDebugMessage("dummy debug message") + .build(); + + MethodCall call = new MethodCall(IS_FEATURE_SUPPORTED, arguments); + when(mockBillingClient.isFeatureSupported(feature)).thenReturn(billingResult); + methodChannelHandler.onMethodCall(call, result); + verify(result).success(false); + } + + @Test + public void launchPriceChangeConfirmationFlow() { + // Set up the sku details + establishConnectedBillingClient(null, null); + String skuId = "foo"; + queryForSkus(singletonList(skuId)); + + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(BillingClient.BillingResponseCode.OK) + .setDebugMessage("dummy debug message") + .build(); + + // Set up the mock billing client + ArgumentCaptor priceChangeConfirmationListenerArgumentCaptor = + ArgumentCaptor.forClass(PriceChangeConfirmationListener.class); + ArgumentCaptor priceChangeFlowParamsArgumentCaptor = + ArgumentCaptor.forClass(PriceChangeFlowParams.class); + doNothing() + .when(mockBillingClient) + .launchPriceChangeConfirmationFlow( + any(), + priceChangeFlowParamsArgumentCaptor.capture(), + priceChangeConfirmationListenerArgumentCaptor.capture()); + + // Call the methodChannelHandler + HashMap arguments = new HashMap<>(); + arguments.put("sku", skuId); + methodChannelHandler.onMethodCall( + new MethodCall(LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW, arguments), result); + + // Verify the price change params. + PriceChangeFlowParams priceChangeFlowParams = priceChangeFlowParamsArgumentCaptor.getValue(); + assertEquals(skuId, priceChangeFlowParams.getSkuDetails().getSku()); + + // Set the response in the callback + PriceChangeConfirmationListener priceChangeConfirmationListener = + priceChangeConfirmationListenerArgumentCaptor.getValue(); + priceChangeConfirmationListener.onPriceChangeConfirmationResult(billingResult); + + // Verify we pass the response to result + verify(result, never()).error(any(), any(), any()); + ArgumentCaptor resultCaptor = ArgumentCaptor.forClass(HashMap.class); + verify(result, times(1)).success(resultCaptor.capture()); + assertEquals(fromBillingResult(billingResult), resultCaptor.getValue()); + } + + @Test + public void launchPriceChangeConfirmationFlow_withoutActivity_returnsActivityUnavailableError() { + // Set up the sku details + establishConnectedBillingClient(null, null); + String skuId = "foo"; + queryForSkus(singletonList(skuId)); + + methodChannelHandler.setActivity(null); + + // Call the methodChannelHandler + HashMap arguments = new HashMap<>(); + arguments.put("sku", skuId); + methodChannelHandler.onMethodCall( + new MethodCall(LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW, arguments), result); + verify(result, times(1)).error(eq("ACTIVITY_UNAVAILABLE"), any(), any()); + } + + @Test + public void launchPriceChangeConfirmationFlow_withoutSkuQuery_returnsNotFoundError() { + // Set up the sku details + establishConnectedBillingClient(null, null); + String skuId = "foo"; + + // Call the methodChannelHandler + HashMap arguments = new HashMap<>(); + arguments.put("sku", skuId); + methodChannelHandler.onMethodCall( + new MethodCall(LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW, arguments), result); + verify(result, times(1)).error(eq("NOT_FOUND"), contains("sku"), any()); + } + + @Test + public void launchPriceChangeConfirmationFlow_withoutBillingClient_returnsUnavailableError() { + // Set up the sku details + String skuId = "foo"; + + // Call the methodChannelHandler + HashMap arguments = new HashMap<>(); + arguments.put("sku", skuId); + methodChannelHandler.onMethodCall( + new MethodCall(LAUNCH_PRICE_CHANGE_CONFIRMATION_FLOW, arguments), result); + verify(result, times(1)).error(eq("UNAVAILABLE"), contains("BillingClient"), any()); + } + + private ArgumentCaptor mockStartConnection() { + Map arguments = new HashMap<>(); + arguments.put("handle", 1); + arguments.put("enablePendingPurchases", true); + MethodCall call = new MethodCall(START_CONNECTION, arguments); + ArgumentCaptor captor = + ArgumentCaptor.forClass(BillingClientStateListener.class); + doNothing().when(mockBillingClient).startConnection(captor.capture()); + + methodChannelHandler.onMethodCall(call, result); + return captor; + } + + private void establishConnectedBillingClient( + @Nullable Map arguments, @Nullable Result result) { + if (arguments == null) { + arguments = new HashMap<>(); + arguments.put("handle", 1); + arguments.put("enablePendingPurchases", true); + } + if (result == null) { + result = mock(Result.class); + } + + MethodCall connectCall = new MethodCall(START_CONNECTION, arguments); + methodChannelHandler.onMethodCall(connectCall, result); + } + + private void queryForSkus(List skusList) { + // Set up the query method call + establishConnectedBillingClient(/* arguments= */ null, /* result= */ null); + HashMap arguments = new HashMap<>(); + String skuType = SkuType.INAPP; + arguments.put("skuType", skuType); + arguments.put("skusList", skusList); + MethodCall queryCall = new MethodCall(QUERY_SKU_DETAILS, arguments); + + // Call the method. + methodChannelHandler.onMethodCall(queryCall, mock(Result.class)); + + // Respond to the call with a matching set of Sku details. + ArgumentCaptor listenerCaptor = + ArgumentCaptor.forClass(SkuDetailsResponseListener.class); + verify(mockBillingClient).querySkuDetailsAsync(any(), listenerCaptor.capture()); + List skuDetailsResponse = + skusList.stream().map(this::buildSkuDetails).collect(toList()); + + BillingResult billingResult = + BillingResult.newBuilder() + .setResponseCode(100) + .setDebugMessage("dummy debug message") + .build(); + listenerCaptor.getValue().onSkuDetailsResponse(billingResult, skuDetailsResponse); + } + + private SkuDetails buildSkuDetails(String 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; + } + + private Purchase buildPurchase(String orderId) { + Purchase purchase = mock(Purchase.class); + when(purchase.getOrderId()).thenReturn(orderId); + return purchase; + } + + private PurchaseHistoryRecord buildPurchaseHistoryRecord(String purchaseToken) { + PurchaseHistoryRecord purchase = mock(PurchaseHistoryRecord.class); + when(purchase.getPurchaseToken()).thenReturn(purchaseToken); + return purchase; + } +} 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 82% 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..2837dceea652 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,19 @@ -// Copyright 2019 The Chromium Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import androidx.annotation.NonNull; +import com.android.billingclient.api.AccountIdentifiers; import com.android.billingclient.api.BillingClient; import com.android.billingclient.api.BillingResult; import com.android.billingclient.api.Purchase; @@ -18,15 +24,23 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import org.json.JSONException; +import org.junit.Before; import org.junit.Test; 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\"}"; + + @Before + public void setup() { + Locale locale = new Locale("en", "us"); + Locale.setDefault(locale); + } @Test public void fromSkuDetail() throws JSONException { @@ -63,6 +77,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 = @@ -168,6 +192,17 @@ public void fromBillingResult_debugMessageNull() throws JSONException { assertEquals(billingResultMap.get("debugMessage"), newBillingResult.getDebugMessage()); } + @Test + public void currencyCodeFromSymbol() { + assertEquals("$", Translator.currencySymbolFromCode("USD")); + try { + Translator.currencySymbolFromCode("EUROPACOIN"); + fail("Translator should throw an exception"); + } catch (Exception e) { + assertTrue(e instanceof IllegalArgumentException); + } + } + private void assertSerialized(SkuDetails expected, Map serialized) { assertEquals(expected.getDescription(), serialized.get("description")); assertEquals(expected.getFreeTrialPeriod(), serialized.get("freeTrialPeriod")); @@ -180,6 +215,7 @@ private void assertSerialized(SkuDetails expected, Map serialize assertEquals(expected.getPrice(), serialized.get("price")); assertEquals(expected.getPriceAmountMicros(), serialized.get("priceAmountMicros")); assertEquals(expected.getPriceCurrencyCode(), serialized.get("priceCurrencyCode")); + assertEquals("$", serialized.get("priceCurrencySymbol")); assertEquals(expected.getSku(), serialized.get("sku")); assertEquals(expected.getSubscriptionPeriod(), serialized.get("subscriptionPeriod")); assertEquals(expected.getTitle(), serialized.get("title")); @@ -200,6 +236,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 +255,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..1185a05b3530 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + diff --git a/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/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..03e4066de85e --- /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,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.inapppurchaseexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class FlutterActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/in_app_purchase/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/in_app_purchase/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/e2e/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/e2e/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/e2e/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/e2e/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/e2e/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/e2e/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/e2e/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/e2e/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/e2e/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/e2e/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/in_app_purchase/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/in_app_purchase/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/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 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_android/example/android/app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker 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..0b4cf534e0aa --- /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:4.1.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/e2e/e2e_macos/android/gradle.properties b/packages/in_app_purchase/in_app_purchase_android/example/android/gradle.properties similarity index 100% rename from packages/e2e/e2e_macos/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..bc6a58afdda2 --- /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-6.7-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/in_app_purchase/example/android/settings.gradle b/packages/in_app_purchase/in_app_purchase_android/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_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..126734187380 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/example/lib/main.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: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_silver1'; +const String _kGoldSubscriptionId = 'subscription_gold1'; +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(), + _FeatureCard(), + ], + ), + ); + } 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 + ? IconButton( + onPressed: () { + final InAppPurchaseAndroidPlatformAddition addition = + InAppPurchasePlatformAddition.instance + as InAppPurchaseAndroidPlatformAddition; + var skuDetails = + (productDetails as GooglePlayProductDetails) + .skuDetails; + addition + .launchPriceChangeConfirmationFlow( + sku: skuDetails.sku) + .then((value) => print( + "confirmationResponse: ${value.responseCode}")); + }, + icon: Icon(Icons.upgrade)) + : 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; + } +} + +class _FeatureCard extends StatelessWidget { + final InAppPurchaseAndroidPlatformAddition addition = + InAppPurchasePlatformAddition.instance + as InAppPurchaseAndroidPlatformAddition; + + _FeatureCard({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ListTile(title: Text('Available features')), + Divider(), + for (BillingClientFeature feature in BillingClientFeature.values) + _buildFeatureWidget(feature), + ])); + } + + Widget _buildFeatureWidget(BillingClientFeature feature) { + return FutureBuilder( + future: addition.isFeatureSupported(feature), + builder: (context, snapshot) { + Color color = Colors.grey; + bool? data = snapshot.data; + if (data != null) { + color = data ? Colors.green : Colors.red; + } + return Padding( + padding: const EdgeInsets.fromLTRB(16.0, 4.0, 16.0, 4.0), + child: Text( + _featureToString(feature), + style: TextStyle(color: color), + ), + ); + }, + ); + } + + String _featureToString(BillingClientFeature feature) { + switch (feature) { + case BillingClientFeature.inAppItemsOnVR: + return 'inAppItemsOnVR'; + case BillingClientFeature.priceChangeConfirmation: + return 'priceChangeConfirmation'; + case BillingClientFeature.subscriptions: + return 'subscriptions'; + case BillingClientFeature.subscriptionsOnVR: + return 'subscriptionsOnVR'; + case BillingClientFeature.subscriptionsUpdate: + return 'subscriptionsUpdate'; + } + } +} 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..4393d1d72eaf --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/billing_client_wrapper.dart @@ -0,0 +1,506 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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, + })) ?? + {}); + } + + /// Checks if the specified feature or capability is supported by the Play Store. + /// Call this to check if a [BillingClientFeature] is supported by the device. + Future isFeatureSupported(BillingClientFeature feature) async { + var result = await channel.invokeMethod( + 'BillingClient#isFeatureSupported(String)', { + 'feature': BillingClientFeatureConverter().toJson(feature), + }); + return result ?? false; + } + + /// Initiates a flow to confirm the change of price for an item subscribed by the user. + /// + /// When the price of a user subscribed item has changed, launch this flow to take users to + /// a screen with price change information. User can confirm the new price or cancel the flow. + /// + /// The skuDetails needs to have already been fetched in a [querySkuDetails] + /// call. + Future launchPriceChangeConfirmationFlow( + {required String sku}) async { + assert(sku != null); + final Map arguments = { + 'sku': sku, + }; + return BillingResultWrapper.fromJson((await channel.invokeMapMethod( + 'BillingClient#launchPriceChangeConfirmationFlow (Activity, PriceChangeFlowParams, PriceChangeConfirmationListener)', + arguments)) ?? + {}); + } + + /// 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, +} + +/// Features/capabilities supported by [BillingClient.isFeatureSupported()](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.FeatureType). +enum BillingClientFeature { + // WARNING: Changes to this class need to be reflected in our generated code. + // Run `flutter packages pub run build_runner watch` to rebuild and watch for + // further changes. + + // JsonValues need to match constant values defined in https://developer.android.com/reference/com/android/billingclient/api/BillingClient.FeatureType#summary + /// Purchase/query for in-app items on VR. + @JsonValue('inAppItemsOnVr') + inAppItemsOnVR, + + /// Launch a price change confirmation flow. + @JsonValue('priceChangeConfirmation') + priceChangeConfirmation, + + /// Purchase/query for subscriptions. + @JsonValue('subscriptions') + subscriptions, + + /// Purchase/query for subscriptions on VR. + @JsonValue('subscriptionsOnVr') + subscriptionsOnVR, + + /// Subscriptions update/replace. + @JsonValue('subscriptionsUpdate') + subscriptionsUpdate +} diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.dart new file mode 100644 index 000000000000..931d92f7b1e7 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.dart @@ -0,0 +1,143 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; +import 'package:json_annotation/json_annotation.dart'; + +import '../../billing_client_wrappers.dart'; + +part 'enum_converters.g.dart'; + +/// Serializer for [BillingResponse]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@BillingResponseConverter()`. +class BillingResponseConverter implements JsonConverter { + /// Default const constructor. + const BillingResponseConverter(); + + @override + BillingResponse fromJson(int? json) { + if (json == null) { + return BillingResponse.error; + } + return _$enumDecode( + _$BillingResponseEnumMap.cast(), json); + } + + @override + int toJson(BillingResponse object) => _$BillingResponseEnumMap[object]!; +} + +/// Serializer for [SkuType]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@SkuTypeConverter()`. +class SkuTypeConverter implements JsonConverter { + /// Default const constructor. + const SkuTypeConverter(); + + @override + SkuType fromJson(String? json) { + if (json == null) { + return SkuType.inapp; + } + return _$enumDecode( + _$SkuTypeEnumMap.cast(), json); + } + + @override + String toJson(SkuType object) => _$SkuTypeEnumMap[object]!; +} + +/// Serializer for [ProrationMode]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@ProrationModeConverter()`. +class ProrationModeConverter implements JsonConverter { + /// Default const constructor. + const ProrationModeConverter(); + + @override + ProrationMode fromJson(int? json) { + if (json == null) { + return ProrationMode.unknownSubscriptionUpgradeDowngradePolicy; + } + return _$enumDecode( + _$ProrationModeEnumMap.cast(), json); + } + + @override + int toJson(ProrationMode object) => _$ProrationModeEnumMap[object]!; +} + +/// Serializer for [PurchaseStateWrapper]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@PurchaseStateConverter()`. +class PurchaseStateConverter + implements JsonConverter { + /// Default const constructor. + const PurchaseStateConverter(); + + @override + PurchaseStateWrapper fromJson(int? json) { + if (json == null) { + return PurchaseStateWrapper.unspecified_state; + } + return _$enumDecode( + _$PurchaseStateWrapperEnumMap.cast(), + json); + } + + @override + int toJson(PurchaseStateWrapper object) => + _$PurchaseStateWrapperEnumMap[object]!; + + /// Converts the purchase state stored in `object` to a [PurchaseStatus]. + /// + /// [PurchaseStateWrapper.unspecified_state] is mapped to [PurchaseStatus.error]. + PurchaseStatus toPurchaseStatus(PurchaseStateWrapper object) { + switch (object) { + case PurchaseStateWrapper.pending: + return PurchaseStatus.pending; + case PurchaseStateWrapper.purchased: + return PurchaseStatus.purchased; + case PurchaseStateWrapper.unspecified_state: + return PurchaseStatus.error; + } + } +} + +/// Serializer for [BillingClientFeature]. +/// +/// Use these in `@JsonSerializable()` classes by annotating them with +/// `@BillingClientFeatureConverter()`. +class BillingClientFeatureConverter + implements JsonConverter { + /// Default const constructor. + const BillingClientFeatureConverter(); + + @override + BillingClientFeature fromJson(String json) { + return _$enumDecode( + _$BillingClientFeatureEnumMap.cast(), + json); + } + + @override + String toJson(BillingClientFeature object) => + _$BillingClientFeatureEnumMap[object]!; +} + +// Define a class so we generate serializer helper methods for the enums +// See https://github.com/google/json_serializable.dart/issues/778 +@JsonSerializable() +class _SerializedEnums { + late BillingResponse response; + late SkuType type; + late PurchaseStateWrapper purchaseState; + late ProrationMode prorationMode; + late BillingClientFeature billingClientFeature; +} diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.g.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.g.dart new file mode 100644 index 000000000000..fe92f56653e4 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/enum_converters.g.dart @@ -0,0 +1,94 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'enum_converters.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_SerializedEnums _$SerializedEnumsFromJson(Map json) => _SerializedEnums() + ..response = _$enumDecode(_$BillingResponseEnumMap, json['response']) + ..type = _$enumDecode(_$SkuTypeEnumMap, json['type']) + ..purchaseState = + _$enumDecode(_$PurchaseStateWrapperEnumMap, json['purchaseState']) + ..prorationMode = _$enumDecode(_$ProrationModeEnumMap, json['prorationMode']) + ..billingClientFeature = + _$enumDecode(_$BillingClientFeatureEnumMap, json['billingClientFeature']); + +Map _$SerializedEnumsToJson(_SerializedEnums instance) => + { + 'response': _$BillingResponseEnumMap[instance.response], + 'type': _$SkuTypeEnumMap[instance.type], + 'purchaseState': _$PurchaseStateWrapperEnumMap[instance.purchaseState], + 'prorationMode': _$ProrationModeEnumMap[instance.prorationMode], + 'billingClientFeature': + _$BillingClientFeatureEnumMap[instance.billingClientFeature], + }; + +K _$enumDecode( + Map enumValues, + Object? source, { + K? unknownValue, +}) { + if (source == null) { + throw ArgumentError( + 'A value must be provided. Supported values: ' + '${enumValues.values.join(', ')}', + ); + } + + return enumValues.entries.singleWhere( + (e) => e.value == source, + orElse: () { + if (unknownValue == null) { + throw ArgumentError( + '`$source` is not one of the supported values: ' + '${enumValues.values.join(', ')}', + ); + } + return MapEntry(unknownValue, enumValues.values.first); + }, + ).key; +} + +const _$BillingResponseEnumMap = { + BillingResponse.serviceTimeout: -3, + BillingResponse.featureNotSupported: -2, + BillingResponse.serviceDisconnected: -1, + BillingResponse.ok: 0, + BillingResponse.userCanceled: 1, + BillingResponse.serviceUnavailable: 2, + BillingResponse.billingUnavailable: 3, + BillingResponse.itemUnavailable: 4, + BillingResponse.developerError: 5, + BillingResponse.error: 6, + BillingResponse.itemAlreadyOwned: 7, + BillingResponse.itemNotOwned: 8, +}; + +const _$SkuTypeEnumMap = { + SkuType.inapp: 'inapp', + SkuType.subs: 'subs', +}; + +const _$PurchaseStateWrapperEnumMap = { + PurchaseStateWrapper.unspecified_state: 0, + PurchaseStateWrapper.purchased: 1, + PurchaseStateWrapper.pending: 2, +}; + +const _$ProrationModeEnumMap = { + ProrationMode.unknownSubscriptionUpgradeDowngradePolicy: 0, + ProrationMode.immediateWithTimeProration: 1, + ProrationMode.immediateAndChargeProratedPrice: 2, + ProrationMode.immediateWithoutProration: 3, + ProrationMode.deferred: 4, +}; + +const _$BillingClientFeatureEnumMap = { + BillingClientFeature.inAppItemsOnVR: 'inAppItemsOnVr', + BillingClientFeature.priceChangeConfirmation: 'priceChangeConfirmation', + BillingClientFeature.subscriptions: 'subscriptions', + BillingClientFeature.subscriptionsOnVR: 'subscriptionsOnVr', + BillingClientFeature.subscriptionsUpdate: 'subscriptionsUpdate', +}; diff --git a/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.dart 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..b5d9fe8cd3af --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/purchase_wrapper.g.dart @@ -0,0 +1,108 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'purchase_wrapper.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PurchaseWrapper _$PurchaseWrapperFromJson(Map json) => PurchaseWrapper( + orderId: json['orderId'] as String? ?? '', + packageName: json['packageName'] as String? ?? '', + purchaseTime: json['purchaseTime'] as int? ?? 0, + purchaseToken: json['purchaseToken'] as String? ?? '', + signature: json['signature'] as String? ?? '', + 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) => + 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) => + 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) => + 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..754f7a352f1c --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.dart @@ -0,0 +1,262 @@ +// Copyright 2013 The Flutter Authors. All 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, + @Deprecated('Use `introductoryPriceAmountMicros` parameter instead') + String introductoryPriceMicros = '', + this.introductoryPriceAmountMicros = 0, + required this.introductoryPriceCycles, + required this.introductoryPricePeriod, + required this.price, + required this.priceAmountMicros, + required this.priceCurrencyCode, + required this.priceCurrencySymbol, + required this.sku, + required this.subscriptionPeriod, + required this.title, + required this.type, + required this.originalPrice, + required this.originalPriceAmountMicros, + }) : _introductoryPriceMicros = introductoryPriceMicros; + + final String _introductoryPriceMicros; + + /// 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. + /// + /// Returns 0 if the SKU is not a subscription or doesn't have an introductory + /// period. + @JsonKey(name: 'introductoryPriceAmountMicros', defaultValue: 0) + final int introductoryPriceAmountMicros; + + /// String representation of [introductoryPrice] in micro-units 990000 + @Deprecated('Use `introductoryPriceAmountMicros` instead.') + @JsonKey(ignore: true) + String get introductoryPriceMicros => _introductoryPriceMicros.isEmpty + ? introductoryPriceAmountMicros.toString() + : _introductoryPriceMicros; + + /// The number of subscription billing periods for which the user will be given the introductory price, such as 3. + /// Returns 0 if the SKU is not a subscription or doesn't have an introductory period. + @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; + + /// [price] localized currency symbol + /// For example, for the US Dollar, the symbol is "$" if the locale + /// is the US, while for other locales it may be "US$". + @JsonKey(defaultValue: '') + final String priceCurrencySymbol; + + /// 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; + } + + return other is SkuDetailsWrapper && + other.description == description && + other.freeTrialPeriod == freeTrialPeriod && + other.introductoryPrice == introductoryPrice && + other.introductoryPriceAmountMicros == introductoryPriceAmountMicros && + other.introductoryPriceCycles == introductoryPriceCycles && + other.introductoryPricePeriod == introductoryPricePeriod && + other.price == price && + other.priceAmountMicros == priceAmountMicros && + other.sku == sku && + other.subscriptionPeriod == subscriptionPeriod && + other.title == title && + other.type == type && + other.originalPrice == originalPrice && + other.originalPriceAmountMicros == originalPriceAmountMicros; + } + + @override + int get hashCode { + return hashValues( + description.hashCode, + freeTrialPeriod.hashCode, + introductoryPrice.hashCode, + introductoryPriceAmountMicros.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; + } + + return other is SkuDetailsResponseWrapper && + other.billingResult == billingResult && + other.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; + } + + return other is BillingResultWrapper && + other.responseCode == responseCode && + other.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..53d5931ecb56 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/billing_client_wrappers/sku_details_wrapper.g.dart @@ -0,0 +1,82 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sku_details_wrapper.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SkuDetailsWrapper _$SkuDetailsWrapperFromJson(Map json) => SkuDetailsWrapper( + description: json['description'] as String? ?? '', + freeTrialPeriod: json['freeTrialPeriod'] as String? ?? '', + introductoryPrice: json['introductoryPrice'] as String? ?? '', + introductoryPriceAmountMicros: + json['introductoryPriceAmountMicros'] as int? ?? 0, + introductoryPriceCycles: json['introductoryPriceCycles'] as int? ?? 0, + introductoryPricePeriod: json['introductoryPricePeriod'] as String? ?? '', + price: json['price'] as String? ?? '', + priceAmountMicros: json['priceAmountMicros'] as int? ?? 0, + priceCurrencyCode: json['priceCurrencyCode'] as String? ?? '', + priceCurrencySymbol: json['priceCurrencySymbol'] as String? ?? '', + sku: json['sku'] as String? ?? '', + subscriptionPeriod: json['subscriptionPeriod'] as String? ?? '', + title: json['title'] as String? ?? '', + type: const SkuTypeConverter().fromJson(json['type'] as String?), + originalPrice: json['originalPrice'] as String? ?? '', + originalPriceAmountMicros: json['originalPriceAmountMicros'] as int? ?? 0, + ); + +Map _$SkuDetailsWrapperToJson(SkuDetailsWrapper instance) => + { + 'description': instance.description, + 'freeTrialPeriod': instance.freeTrialPeriod, + 'introductoryPrice': instance.introductoryPrice, + 'introductoryPriceAmountMicros': instance.introductoryPriceAmountMicros, + 'introductoryPriceCycles': instance.introductoryPriceCycles, + 'introductoryPricePeriod': instance.introductoryPricePeriod, + 'price': instance.price, + 'priceAmountMicros': instance.priceAmountMicros, + 'priceCurrencyCode': instance.priceCurrencyCode, + 'priceCurrencySymbol': instance.priceCurrencySymbol, + 'sku': instance.sku, + 'subscriptionPeriod': instance.subscriptionPeriod, + 'title': instance.title, + 'type': const SkuTypeConverter().toJson(instance.type), + 'originalPrice': instance.originalPrice, + 'originalPriceAmountMicros': instance.originalPriceAmountMicros, + }; + +SkuDetailsResponseWrapper _$SkuDetailsResponseWrapperFromJson(Map json) => + 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) => + 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..11b105aba96c --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/in_app_purchase_android_platform_addition.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: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); + } + + /// Checks if the specified feature or capability is supported by the Play Store. + /// Call this to check if a [BillingClientFeature] is supported by the device. + Future isFeatureSupported(BillingClientFeature feature) async { + return _billingClient.isFeatureSupported(feature); + } + + /// Initiates a flow to confirm the change of price for an item subscribed by the user. + /// + /// When the price of a user subscribed item has changed, launch this flow to take users to + /// a screen with price change information. User can confirm the new price or cancel the flow. + /// + /// The skuDetails needs to have already been fetched in a + /// [InAppPurchaseAndroidPlatform.queryProductDetails] call. + Future launchPriceChangeConfirmationFlow( + {required String sku}) { + return _billingClient.launchPriceChangeConfirmationFlow(sku: sku); + } +} 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..59d33fe26223 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_product_details.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: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, + required String currencySymbol, + }) : super( + id: id, + title: title, + description: description, + price: price, + rawPrice: rawPrice, + currencyCode: currencyCode, + currencySymbol: currencySymbol, + ); + + /// Points back to the [SkuDetailsWrapper] object that was used to generate + /// this [GooglePlayProductDetails] object. + final SkuDetailsWrapper skuDetails; + + /// Generate a [GooglePlayProductDetails] object based on an Android + /// [SkuDetailsWrapper] object. + factory GooglePlayProductDetails.fromSkuDetails( + 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, + currencySymbol: skuDetails.priceCurrencySymbol, + 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..53b58bd664fd --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/lib/src/types/google_play_purchase_details.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: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.pendingCompletePurchase = !billingClientPurchase.isAcknowledged; + } + + /// Points back to the [PurchaseWrapper] which was used to generate this + /// [GooglePlayPurchaseDetails] object. + final PurchaseWrapper billingClientPurchase; + + /// Generate a [PurchaseDetails] object based on an Android [Purchase] object. + factory GooglePlayPurchaseDetails.fromPurchase(PurchaseWrapper purchase) { + final GooglePlayPurchaseDetails purchaseDetails = GooglePlayPurchaseDetails( + 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..d51abbb43edc --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/pubspec.yaml @@ -0,0 +1,32 @@ +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.5+1 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +flutter: + plugin: + implements: in_app_purchase + platforms: + android: + package: io.flutter.plugins.inapppurchase + pluginClass: InAppPurchasePlugin + +dependencies: + collection: ^1.15.0 + flutter: + sdk: flutter + in_app_purchase_platform_interface: ^1.1.0 + json_annotation: ^4.0.1 + meta: ^1.3.0 + +dev_dependencies: + build_runner: ^2.0.0 + flutter_test: + sdk: flutter + json_serializable: ^5.0.2 + test: ^1.16.0 diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart new file mode 100644 index 000000000000..02ae9ba33564 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/billing_client_wrapper_test.dart @@ -0,0 +1,617 @@ +// Copyright 2013 The Flutter Authors. All rights 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))); + }); + }); + + group('isFeatureSupported', () { + const String isFeatureSupportedMethodName = + 'BillingClient#isFeatureSupported(String)'; + test('isFeatureSupported returns false', () async { + late Map arguments; + stubPlatform.addResponse( + name: isFeatureSupportedMethodName, + value: false, + additionalStepBeforeReturn: (value) => arguments = value, + ); + final bool isSupported = await billingClient + .isFeatureSupported(BillingClientFeature.subscriptions); + expect(isSupported, isFalse); + expect(arguments['feature'], equals('subscriptions')); + }); + + test('isFeatureSupported returns true', () async { + late Map arguments; + stubPlatform.addResponse( + name: isFeatureSupportedMethodName, + value: true, + additionalStepBeforeReturn: (value) => arguments = value, + ); + final bool isSupported = await billingClient + .isFeatureSupported(BillingClientFeature.subscriptions); + expect(isSupported, isTrue); + expect(arguments['feature'], equals('subscriptions')); + }); + }); + + group('launchPriceChangeConfirmationFlow', () { + const String launchPriceChangeConfirmationFlowMethodName = + 'BillingClient#launchPriceChangeConfirmationFlow (Activity, PriceChangeFlowParams, PriceChangeConfirmationListener)'; + + final expectedBillingResultPriceChangeConfirmation = BillingResultWrapper( + responseCode: BillingResponse.ok, + debugMessage: 'dummy message', + ); + + test('serializes and deserializes data', () async { + stubPlatform.addResponse( + name: launchPriceChangeConfirmationFlowMethodName, + value: + buildBillingResultMap(expectedBillingResultPriceChangeConfirmation), + ); + + expect( + await billingClient.launchPriceChangeConfirmationFlow( + sku: dummySkuDetails.sku, + ), + equals(expectedBillingResultPriceChangeConfirmation), + ); + }); + + test('passes sku to launchPriceChangeConfirmationFlow', () async { + stubPlatform.addResponse( + name: launchPriceChangeConfirmationFlowMethodName, + value: + buildBillingResultMap(expectedBillingResultPriceChangeConfirmation), + ); + await billingClient.launchPriceChangeConfirmationFlow( + sku: dummySkuDetails.sku, + ); + final MethodCall call = stubPlatform + .previousCallMatching(launchPriceChangeConfirmationFlowMethodName); + expect(call.arguments, + equals({'sku': dummySkuDetails.sku})); + }); + }); +} 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..70b9fcad4da7 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/purchase_wrapper_test.dart @@ -0,0 +1,238 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +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('fromPurchase() 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, false); + }); + + test( + 'fromPurchase() should return set pendingCompletePurchase to true for unacknowledged purchase', + () { + final GooglePlayPurchaseDetails details = + GooglePlayPurchaseDetails.fromPurchase(dummyUnacknowledgedPurchase); + + 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, dummyUnacknowledgedPurchase); + 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_deprecated_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_deprecated_test.dart new file mode 100644 index 000000000000..3e29d92724ad --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_deprecated_test.dart @@ -0,0 +1,69 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// TODO(mvanbeusekom): Remove this file when the deprecated +// `SkuDetailsWrapper.introductoryPriceMicros` field is +// removed. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:in_app_purchase_android/billing_client_wrappers.dart'; + +void main() { + test( + 'Deprecated `introductoryPriceMicros` field reflects parameter from constructor', + () { + final SkuDetailsWrapper skuDetails = SkuDetailsWrapper( + description: 'description', + freeTrialPeriod: 'freeTrialPeriod', + introductoryPrice: 'introductoryPrice', + // ignore: deprecated_member_use_from_same_package + introductoryPriceMicros: '990000', + introductoryPriceCycles: 1, + introductoryPricePeriod: 'introductoryPricePeriod', + price: 'price', + priceAmountMicros: 1000, + priceCurrencyCode: 'priceCurrencyCode', + priceCurrencySymbol: r'$', + sku: 'sku', + subscriptionPeriod: 'subscriptionPeriod', + title: 'title', + type: SkuType.inapp, + originalPrice: 'originalPrice', + originalPriceAmountMicros: 1000, + ); + + expect(skuDetails, isNotNull); + expect(skuDetails.introductoryPriceAmountMicros, 0); + // ignore: deprecated_member_use_from_same_package + expect(skuDetails.introductoryPriceMicros, '990000'); + }); + + test( + '`introductoryPriceAmoutMicros` constructor parameter is reflected by deprecated `introductoryPriceMicros` and `introductoryPriceAmountMicros` fields', + () { + final SkuDetailsWrapper skuDetails = SkuDetailsWrapper( + description: 'description', + freeTrialPeriod: 'freeTrialPeriod', + introductoryPrice: 'introductoryPrice', + introductoryPriceAmountMicros: 990000, + introductoryPriceCycles: 1, + introductoryPricePeriod: 'introductoryPricePeriod', + price: 'price', + priceAmountMicros: 1000, + priceCurrencyCode: 'priceCurrencyCode', + priceCurrencySymbol: r'$', + sku: 'sku', + subscriptionPeriod: 'subscriptionPeriod', + title: 'title', + type: SkuType.inapp, + originalPrice: 'originalPrice', + originalPriceAmountMicros: 1000, + ); + + expect(skuDetails, isNotNull); + expect(skuDetails.introductoryPriceAmountMicros, 990000); + // ignore: deprecated_member_use_from_same_package + expect(skuDetails.introductoryPriceMicros, '990000'); + }); +} diff --git a/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_test.dart b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_test.dart new file mode 100644 index 000000000000..18804a41940e --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/test/billing_client_wrappers/sku_details_wrapper_test.dart @@ -0,0 +1,151 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package: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', + introductoryPriceAmountMicros: 990000, + introductoryPriceCycles: 1, + introductoryPricePeriod: 'introductoryPricePeriod', + price: 'price', + priceAmountMicros: 1000, + priceCurrencyCode: 'priceCurrencyCode', + priceCurrencySymbol: r'$', + sku: 'sku', + subscriptionPeriod: 'subscriptionPeriod', + title: 'title', + type: SkuType.inapp, + originalPrice: 'originalPrice', + originalPriceAmountMicros: 1000, +); + +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, + 'introductoryPriceAmountMicros': original.introductoryPriceAmountMicros, + 'introductoryPriceCycles': original.introductoryPriceCycles, + 'introductoryPricePeriod': original.introductoryPricePeriod, + 'price': original.price, + 'priceAmountMicros': original.priceAmountMicros, + 'priceCurrencyCode': original.priceCurrencyCode, + 'priceCurrencySymbol': original.priceCurrencySymbol, + '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..a478cabac89b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_addition_test.dart @@ -0,0 +1,214 @@ +// Copyright 2013 The Flutter Authors. All rights 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'}); + }); + }); + }); + + group('isFeatureSupported', () { + const String isFeatureSupportedMethodName = + 'BillingClient#isFeatureSupported(String)'; + test('isFeatureSupported returns false', () async { + late Map arguments; + stubPlatform.addResponse( + name: isFeatureSupportedMethodName, + value: false, + additionalStepBeforeReturn: (value) => arguments = value, + ); + final bool isSupported = await iapAndroidPlatformAddition + .isFeatureSupported(BillingClientFeature.subscriptions); + expect(isSupported, isFalse); + expect(arguments['feature'], equals('subscriptions')); + }); + + test('isFeatureSupported returns true', () async { + late Map arguments; + stubPlatform.addResponse( + name: isFeatureSupportedMethodName, + value: true, + additionalStepBeforeReturn: (value) => arguments = value, + ); + final bool isSupported = await iapAndroidPlatformAddition + .isFeatureSupported(BillingClientFeature.subscriptions); + expect(isSupported, isTrue); + expect(arguments['feature'], equals('subscriptions')); + }); + }); + + group('launchPriceChangeConfirmationFlow', () { + const String launchPriceChangeConfirmationFlowMethodName = + 'BillingClient#launchPriceChangeConfirmationFlow (Activity, PriceChangeFlowParams, PriceChangeConfirmationListener)'; + const dummySku = 'sku'; + + final expectedBillingResultPriceChangeConfirmation = BillingResultWrapper( + responseCode: BillingResponse.ok, + debugMessage: 'dummy message', + ); + + test('serializes and deserializes data', () async { + stubPlatform.addResponse( + name: launchPriceChangeConfirmationFlowMethodName, + value: + buildBillingResultMap(expectedBillingResultPriceChangeConfirmation), + ); + + expect( + await iapAndroidPlatformAddition.launchPriceChangeConfirmationFlow( + sku: dummySku, + ), + equals(expectedBillingResultPriceChangeConfirmation), + ); + }); + + test('passes sku to launchPriceChangeConfirmationFlow', () async { + stubPlatform.addResponse( + name: launchPriceChangeConfirmationFlowMethodName, + value: + buildBillingResultMap(expectedBillingResultPriceChangeConfirmation), + ); + await iapAndroidPlatformAddition.launchPriceChangeConfirmationFlow( + sku: dummySku, + ); + final MethodCall call = stubPlatform + .previousCallMatching(launchPriceChangeConfirmationFlowMethodName); + expect(call.arguments, equals({'sku': dummySku})); + }); + }); +} 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..52ec08bea07a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_android/test/in_app_purchase_android_platform_test.dart @@ -0,0 +1,647 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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); + expect(response.productDetails.first.currencySymbol, r'$'); + }); + + 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/AUTHORS b/packages/in_app_purchase/in_app_purchase_ios/AUTHORS new file mode 100644 index 000000000000..78f9e5ad9f6b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/AUTHORS @@ -0,0 +1,67 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Maurits van Beusekom 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..76cafa9201cc --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/CHANGELOG.md @@ -0,0 +1,57 @@ +## 0.1.3+5 + +* Updated example app to handle restored purchases properly. +* Update dev_dependency `build_runner` to ^2.0.0 and `json_serializable` to ^5.0.2. + +## 0.1.3+4 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. + +## 0.1.3+3 + +* Add `implements` to pubspec. + +# 0.1.3+2 + +* Removed dependency on the `test` package. + +# 0.1.3+1 + +- Updated installation instructions in README. + +## 0.1.3 + +* Add price symbol to platform interface object ProductDetail. + +## 0.1.2+2 + +* Fix crash when retrieveReceiptWithError gives an error. + +## 0.1.2+1 + +* Fix wrong data type when cancelling user credentials dialog. + +## 0.1.2 + +* Added countryCode to the SKPriceLocaleWrapper. + +## 0.1.1+1 + +* iOS: Fix treating missing App Store receipt as an exception. + +## 0.1.1 + +* Added support to register a `SKPaymentQueueDelegateWrapper` and handle changes to active subscriptions accordingly (see also Store Kit's [SKPaymentQueueDelegate](https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate?language=objc)). + +## 0.1.0+2 + +* Changed the iOS payment queue handler in such a way that it only adds a listener to the `SKPaymentQueue` when there + is a listener to the Dart `purchaseStream`. + +## 0.1.0+1 + +* Added a "Restore purchases" button to conform to Apple's StoreKit guidelines on [restoring products](https://developer.apple.com/documentation/storekit/in-app_purchase/restoring_purchased_products?language=objc); + +## 0.1.0 + +* Initial open-source release. diff --git a/packages/in_app_purchase/in_app_purchase_ios/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..7ac21c495f7b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/README.md @@ -0,0 +1,29 @@ +# in\_app\_purchase\_ios + +The iOS implementation of [`in_app_purchase`][1]. + +## Usage + +This package has been [endorsed][2], meaning that you only need to add `in_app_purchase` +as a dependency in your `pubspec.yaml`. This package will be automatically included in your app +when you do. + +If you wish to use the iOS package only, you can [add `in_app_purchase_ios` directly][3]. + +## Contributing + +This plugin uses +[json_serializable](https://pub.dev/packages/json_serializable) for the +many data structs passed between the underlying platform layers and Dart. After +editing any of the serialized data structs, rebuild the serializers by running +`flutter packages pub run build_runner build --delete-conflicting-outputs`. +`flutter packages pub run build_runner watch --delete-conflicting-outputs` will +watch the filesystem for changes. + +If you would like to contribute to the plugin, check out our +[contribution guide](https://github.com/flutter/plugins/blob/master/CONTRIBUTING.md). + + +[1]: ../in_app_purchase +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin +[3]: https://pub.dev/packages/in_app_purchase_ios/install diff --git a/packages/in_app_purchase/in_app_purchase_ios/analysis_options.yaml b/packages/in_app_purchase/in_app_purchase_ios/analysis_options.yaml 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..8d4492f977ad --- /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 + 9.0 + + diff --git a/packages/camera/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/camera/example/ios/Flutter/Debug.xcconfig rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/Flutter/Debug.xcconfig diff --git a/packages/camera/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/camera/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..5200b9fa5045 --- /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.6' + 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..a88050193053 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,676 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 0FFCF66105590202CD84C7AA /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 1630769A874F9381BC761FE1 /* libPods-Runner.a */; }; + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 688DE35121F2A5A100EA2684 /* TranslatorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 688DE35021F2A5A100EA2684 /* TranslatorTests.m */; }; + 6896B34621E9363700D37AEF /* ProductRequestHandlerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 6896B34521E9363700D37AEF /* ProductRequestHandlerTests.m */; }; + 6896B34C21EEB4B800D37AEF /* Stubs.m in Sources */ = {isa = PBXBuildFile; fileRef = 6896B34B21EEB4B800D37AEF /* Stubs.m */; }; + 7E34217B7715B1918134647A /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 18D02AB334F1C07BB9A4374A /* libPods-RunnerTests.a */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + A5279298219369C600FF69E6 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5279297219369C600FF69E6 /* StoreKit.framework */; }; + A59001A721E69658004A3E5E /* InAppPurchasePluginTests.m in Sources */ = {isa = PBXBuildFile; fileRef = A59001A621E69658004A3E5E /* InAppPurchasePluginTests.m */; }; + F67646F82681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F67646F72681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m */; }; + F78AF3142342BC89008449C7 /* PaymentQueueTests.m in Sources */ = {isa = PBXBuildFile; fileRef = F78AF3132342BC89008449C7 /* PaymentQueueTests.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + A59001A921E69658004A3E5E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 10B860DFD91A1DF639D7BE1D /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1630769A874F9381BC761FE1 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 18D02AB334F1C07BB9A4374A /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 2550EB3A5A3E749A54ADCA2D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 688DE35021F2A5A100EA2684 /* TranslatorTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = TranslatorTests.m; sourceTree = ""; }; + 6896B34521E9363700D37AEF /* ProductRequestHandlerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ProductRequestHandlerTests.m; sourceTree = ""; }; + 6896B34A21EEB4B800D37AEF /* Stubs.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Stubs.h; sourceTree = ""; }; + 6896B34B21EEB4B800D37AEF /* Stubs.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Stubs.m; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9D681E092EB0D20D652F69FC /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + A5279297219369C600FF69E6 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; + A59001A421E69658004A3E5E /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + A59001A621E69658004A3E5E /* InAppPurchasePluginTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = InAppPurchasePluginTests.m; sourceTree = ""; }; + A59001A821E69658004A3E5E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E4F9651425A612301059769C /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + F67646F72681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FIAPPaymentQueueDeleteTests.m; sourceTree = ""; }; + F6E5D5F926131C4800C68BED /* Configuration.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Configuration.storekit; sourceTree = ""; }; + F78AF3132342BC89008449C7 /* PaymentQueueTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PaymentQueueTests.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + A5279298219369C600FF69E6 /* StoreKit.framework in Frameworks */, + 0FFCF66105590202CD84C7AA /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A59001A121E69658004A3E5E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7E34217B7715B1918134647A /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0B4403AC68C3196AECF5EF89 /* Pods */ = { + isa = PBXGroup; + children = ( + E4F9651425A612301059769C /* Pods-Runner.debug.xcconfig */, + 2550EB3A5A3E749A54ADCA2D /* Pods-Runner.release.xcconfig */, + 9D681E092EB0D20D652F69FC /* Pods-RunnerTests.debug.xcconfig */, + 10B860DFD91A1DF639D7BE1D /* Pods-RunnerTests.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 334733E826680E5900DCC49E /* Temp */ = { + isa = PBXGroup; + children = ( + ); + path = Temp; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 334733E826680E5900DCC49E /* Temp */, + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + A59001A521E69658004A3E5E /* RunnerTests */, + 97C146EF1CF9000F007C117D /* Products */, + E4DB99639FAD8ADED6B572FC /* Frameworks */, + 0B4403AC68C3196AECF5EF89 /* Pods */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + A59001A421E69658004A3E5E /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + F6E5D5F926131C4800C68BED /* Configuration.storekit */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + A59001A521E69658004A3E5E /* RunnerTests */ = { + isa = PBXGroup; + children = ( + A59001A821E69658004A3E5E /* Info.plist */, + 6896B34A21EEB4B800D37AEF /* Stubs.h */, + 6896B34B21EEB4B800D37AEF /* Stubs.m */, + A59001A621E69658004A3E5E /* InAppPurchasePluginTests.m */, + 6896B34521E9363700D37AEF /* ProductRequestHandlerTests.m */, + F78AF3132342BC89008449C7 /* PaymentQueueTests.m */, + 688DE35021F2A5A100EA2684 /* TranslatorTests.m */, + F67646F72681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m */, + ); + path = RunnerTests; + sourceTree = ""; + }; + E4DB99639FAD8ADED6B572FC /* Frameworks */ = { + isa = PBXGroup; + children = ( + A5279297219369C600FF69E6 /* StoreKit.framework */, + 1630769A874F9381BC761FE1 /* libPods-Runner.a */, + 18D02AB334F1C07BB9A4374A /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + EDD921296E29F853F7B69716 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; + A59001A321E69658004A3E5E /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = A59001AD21E69658004A3E5E /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 95C7A5986B77A8DF76F6DF3A /* [CP] Check Pods Manifest.lock */, + A59001A021E69658004A3E5E /* Sources */, + A59001A121E69658004A3E5E /* Frameworks */, + A59001A221E69658004A3E5E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + A59001AA21E69658004A3E5E /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = A59001A421E69658004A3E5E /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + DefaultBuildSystemTypeForWorkspace = Original; + LastUpgradeCheck = 1100; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + SystemCapabilities = { + com.apple.InAppPurchase = { + enabled = 1; + }; + }; + }; + A59001A321E69658004A3E5E = { + CreatedOnToolsVersion = 10.0; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + A59001A321E69658004A3E5E /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A59001A221E69658004A3E5E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 95C7A5986B77A8DF76F6DF3A /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + EDD921296E29F853F7B69716 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A59001A021E69658004A3E5E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F78AF3142342BC89008449C7 /* PaymentQueueTests.m in Sources */, + F67646F82681D9A80048C2EA /* FIAPPaymentQueueDeleteTests.m in Sources */, + 6896B34621E9363700D37AEF /* ProductRequestHandlerTests.m in Sources */, + 688DE35121F2A5A100EA2684 /* TranslatorTests.m in Sources */, + A59001A721E69658004A3E5E /* InAppPurchasePluginTests.m in Sources */, + 6896B34C21EEB4B800D37AEF /* Stubs.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + A59001AA21E69658004A3E5E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = A59001A921E69658004A3E5E /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.inAppPurchaseExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.inAppPurchaseExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + A59001AB21E69658004A3E5E /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9D681E092EB0D20D652F69FC /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Debug; + }; + A59001AC21E69658004A3E5E /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 10B860DFD91A1DF639D7BE1D /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A59001AD21E69658004A3E5E /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A59001AB21E69658004A3E5E /* Debug */, + A59001AC21E69658004A3E5E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner.xcodeproj/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/espresso/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/espresso/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/webview_flutter/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 similarity index 100% rename from packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/packages/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/e2e/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/e2e/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/e2e/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/e2e/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/e2e/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/e2e/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/e2e/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/e2e/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/e2e/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/e2e/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/e2e/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/e2e/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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..b98fefb68a95 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/Runner/Configuration.storekit @@ -0,0 +1,100 @@ +{ + "identifier" : "6073E9A3", + "nonRenewingSubscriptions" : [ + + ], + "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" : "4.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" : "P1W", + "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" : 1 + } +} 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/FIAPPaymentQueueDeleteTests.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.m new file mode 100644 index 000000000000..810e1fafe11a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/FIAPPaymentQueueDeleteTests.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 +#import +#import "FIAObjectTranslator.h" +#import "FIAPaymentQueueHandler.h" +#import "Stubs.h" + +@import in_app_purchase_ios; + +API_AVAILABLE(ios(13.0)) +@interface FIAPPaymentQueueDelegateTests : XCTestCase + +@property(strong, nonatomic) FlutterMethodChannel *channel; +@property(strong, nonatomic) SKPaymentTransaction *transaction; +@property(strong, nonatomic) SKStorefront *storefront; + +@end + +@implementation FIAPPaymentQueueDelegateTests + +- (void)setUp { + self.channel = OCMClassMock(FlutterMethodChannel.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], + }; + self.transaction = [[SKPaymentTransactionStub alloc] initWithMap:transactionMap]; + + NSDictionary *storefrontMap = @{ + @"countryCode" : @"USA", + @"identifier" : @"unique_identifier", + }; + self.storefront = [[SKStorefrontStub alloc] initWithMap:storefrontMap]; +} + +- (void)tearDown { + self.channel = nil; +} + +- (void)testShouldContinueTransaction { + if (@available(iOS 13.0, *)) { + FIAPPaymentQueueDelegate *delegate = + [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:self.channel]; + + OCMStub([self.channel + invokeMethod:@"shouldContinueTransaction" + arguments:[FIAObjectTranslator getMapFromSKStorefront:self.storefront + andSKPaymentTransaction:self.transaction] + result:([OCMArg invokeBlockWithArgs:[NSNumber numberWithBool:NO], nil])]); + + BOOL shouldContinue = [delegate paymentQueue:OCMClassMock(SKPaymentQueue.class) + shouldContinueTransaction:self.transaction + inStorefront:self.storefront]; + + XCTAssertFalse(shouldContinue); + } +} + +- (void)testShouldContinueTransaction_should_default_to_yes { + if (@available(iOS 13.0, *)) { + FIAPPaymentQueueDelegate *delegate = + [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:self.channel]; + + OCMStub([self.channel invokeMethod:@"shouldContinueTransaction" + arguments:[FIAObjectTranslator getMapFromSKStorefront:self.storefront + andSKPaymentTransaction:self.transaction] + result:[OCMArg any]]); + + BOOL shouldContinue = [delegate paymentQueue:OCMClassMock(SKPaymentQueue.class) + shouldContinueTransaction:self.transaction + inStorefront:self.storefront]; + + XCTAssertTrue(shouldContinue); + } +} + +- (void)testShouldShowPriceConsentIfNeeded { + if (@available(iOS 13.4, *)) { + FIAPPaymentQueueDelegate *delegate = + [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:self.channel]; + + OCMStub([self.channel + invokeMethod:@"shouldShowPriceConsent" + arguments:nil + result:([OCMArg invokeBlockWithArgs:[NSNumber numberWithBool:NO], nil])]); + + BOOL shouldShow = + [delegate paymentQueueShouldShowPriceConsent:OCMClassMock(SKPaymentQueue.class)]; + + XCTAssertFalse(shouldShow); + } +} + +- (void)testShouldShowPriceConsentIfNeeded_should_default_to_yes { + if (@available(iOS 13.4, *)) { + FIAPPaymentQueueDelegate *delegate = + [[FIAPPaymentQueueDelegate alloc] initWithMethodChannel:self.channel]; + + OCMStub([self.channel invokeMethod:@"shouldShowPriceConsent" + arguments:nil + result:[OCMArg any]]); + + BOOL shouldShow = + [delegate paymentQueueShouldShowPriceConsent:OCMClassMock(SKPaymentQueue.class)]; + + XCTAssertTrue(shouldShow); + } +} + +@end 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..b51f622e939b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/InAppPurchasePluginTests.m @@ -0,0 +1,441 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import "FIAPaymentQueueHandler.h" +#import "Stubs.h" + +@import in_app_purchase_ios; + +@interface InAppPurchasePluginTest : XCTestCase + +@property(strong, nonatomic) FIAPReceiptManagerStub* receiptManagerStub; +@property(strong, nonatomic) InAppPurchasePlugin* plugin; + +@end + +@implementation InAppPurchasePluginTest + +- (void)setUp { + self.receiptManagerStub = [FIAPReceiptManagerStub new]; + self.plugin = [[InAppPurchasePluginStub alloc] initWithReceiptManager:self.receiptManagerStub]; +} + +- (void)tearDown { +} + +- (void)testInvalidMethodCall { + XCTestExpectation* expectation = + [self expectationWithDescription:@"expect result to be not implemented"]; + FlutterMethodCall* call = [FlutterMethodCall methodCallWithMethodName:@"invalid" arguments:NULL]; + __block id result; + [self.plugin handleMethodCall:call + result:^(id r) { + [expectation fulfill]; + result = r; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(result, FlutterMethodNotImplemented); +} + +- (void)testCanMakePayments { + XCTestExpectation* expectation = [self expectationWithDescription:@"expect result to be YES"]; + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue canMakePayments:]" + arguments:NULL]; + __block id result; + [self.plugin handleMethodCall:call + result:^(id r) { + [expectation fulfill]; + result = r; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(result, @YES); +} + +- (void)testGetProductResponse { + XCTestExpectation* expectation = + [self expectationWithDescription:@"expect response contains 1 item"]; + FlutterMethodCall* call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin startProductRequest:result:]" + arguments:@[ @"123" ]]; + __block id result; + [self.plugin handleMethodCall:call + result:^(id r) { + [expectation fulfill]; + result = r; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssert([result isKindOfClass:[NSDictionary class]]); + NSArray* resultArray = [result objectForKey:@"products"]; + XCTAssertEqual(resultArray.count, 1); + XCTAssertTrue([resultArray.firstObject[@"productIdentifier"] isEqualToString:@"123"]); +} + +- (void)testAddPaymentFailure { + XCTestExpectation* expectation = + [self expectationWithDescription:@"result should return failed state"]; + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:@{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandbox" : @YES, + }]; + SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; + queue.testState = SKPaymentTransactionStateFailed; + __block SKPaymentTransaction* transactionForUpdateBlock; + self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray* _Nonnull transactions) { + SKPaymentTransaction* transaction = transactions[0]; + if (transaction.transactionState == SKPaymentTransactionStateFailed) { + transactionForUpdateBlock = transaction; + [expectation fulfill]; + } + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { + return YES; + } + updatedDownloads:nil]; + [queue addTransactionObserver:self.plugin.paymentQueueHandler]; + + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStateFailed); +} + +- (void)testAddPaymentSuccessWithMockQueue { + XCTestExpectation* expectation = + [self expectationWithDescription:@"result should return success state"]; + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:@{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandbox" : @YES, + }]; + SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; + queue.testState = SKPaymentTransactionStatePurchased; + __block SKPaymentTransaction* transactionForUpdateBlock; + self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray* _Nonnull transactions) { + SKPaymentTransaction* transaction = transactions[0]; + if (transaction.transactionState == SKPaymentTransactionStatePurchased) { + transactionForUpdateBlock = transaction; + [expectation fulfill]; + } + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { + return YES; + } + updatedDownloads:nil]; + [queue addTransactionObserver:self.plugin.paymentQueueHandler]; + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased); +} + +- (void)testAddPaymentWithNullSandboxArgument { + XCTestExpectation* expectation = + [self expectationWithDescription:@"result should return success state"]; + XCTestExpectation* simulatesAskToBuyInSandboxExpectation = + [self expectationWithDescription:@"payment isn't simulatesAskToBuyInSandbox"]; + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" + arguments:@{ + @"productIdentifier" : @"123", + @"quantity" : @(1), + @"simulatesAskToBuyInSandbox" : [NSNull null], + }]; + SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; + queue.testState = SKPaymentTransactionStatePurchased; + __block SKPaymentTransaction* transactionForUpdateBlock; + self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray* _Nonnull transactions) { + SKPaymentTransaction* transaction = transactions[0]; + if (transaction.transactionState == SKPaymentTransactionStatePurchased) { + transactionForUpdateBlock = transaction; + [expectation fulfill]; + } + if (!transaction.payment.simulatesAskToBuyInSandbox) { + [simulatesAskToBuyInSandboxExpectation fulfill]; + } + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { + return YES; + } + updatedDownloads:nil]; + [queue addTransactionObserver:self.plugin.paymentQueueHandler]; + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + [self waitForExpectations:@[ expectation, simulatesAskToBuyInSandboxExpectation ] timeout:5]; + XCTAssertEqual(transactionForUpdateBlock.transactionState, SKPaymentTransactionStatePurchased); +} + +- (void)testRestoreTransactions { + XCTestExpectation* expectation = + [self expectationWithDescription:@"result successfully restore transactions"]; + FlutterMethodCall* call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin restoreTransactions:result:]" + arguments:nil]; + SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; + queue.testState = SKPaymentTransactionStatePurchased; + __block BOOL callbackInvoked = NO; + self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:^(NSArray* _Nonnull transactions) { + } + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:^() { + callbackInvoked = YES; + [expectation fulfill]; + } + shouldAddStorePayment:nil + updatedDownloads:nil]; + [queue addTransactionObserver:self.plugin.paymentQueueHandler]; + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertTrue(callbackInvoked); +} + +- (void)testRetrieveReceiptDataSuccess { + XCTestExpectation* expectation = [self expectationWithDescription:@"receipt data retrieved"]; + FlutterMethodCall* call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" + arguments:nil]; + __block NSDictionary* result; + [self.plugin handleMethodCall:call + result:^(id r) { + result = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertNotNil(result); + XCTAssert([result isKindOfClass:[NSString class]]); +} + +- (void)testRetrieveReceiptDataError { + XCTestExpectation* expectation = [self expectationWithDescription:@"receipt data retrieved"]; + FlutterMethodCall* call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin retrieveReceiptData:result:]" + arguments:nil]; + __block NSDictionary* result; + self.receiptManagerStub.returnError = YES; + [self.plugin handleMethodCall:call + result:^(id r) { + result = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertNotNil(result); + XCTAssert([result isKindOfClass:[FlutterError class]]); + NSDictionary* details = ((FlutterError*)result).details; + XCTAssertNotNil(details[@"error"]); + NSNumber* errorCode = (NSNumber*)details[@"error"][@"code"]; + XCTAssertEqual(errorCode, [NSNumber numberWithInteger:99]); +} + +- (void)testRefreshReceiptRequest { + XCTestExpectation* expectation = [self expectationWithDescription:@"expect success"]; + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin refreshReceipt:result:]" + arguments:nil]; + __block BOOL result = NO; + [self.plugin handleMethodCall:call + result:^(id r) { + result = YES; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertTrue(result); +} + +- (void)testPresentCodeRedemptionSheet { + XCTestExpectation* expectation = + [self expectationWithDescription:@"expect successfully present Code Redemption Sheet"]; + FlutterMethodCall* call = [FlutterMethodCall + methodCallWithMethodName:@"-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]" + arguments:nil]; + __block BOOL callbackInvoked = NO; + [self.plugin handleMethodCall:call + result:^(id r) { + callbackInvoked = YES; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertTrue(callbackInvoked); +} + +- (void)testGetPendingTransactions { + XCTestExpectation* expectation = [self expectationWithDescription:@"expect success"]; + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue transactions]" arguments:nil]; + SKPaymentQueue* mockQueue = OCMClassMock(SKPaymentQueue.class); + NSDictionary* transactionMap = @{ + @"transactionIdentifier" : [NSNull null], + @"transactionState" : @(SKPaymentTransactionStatePurchasing), + @"payment" : [NSNull null], + @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" + code:123 + userInfo:@{}]], + @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), + @"originalTransaction" : [NSNull null], + }; + OCMStub(mockQueue.transactions).andReturn(@[ [[SKPaymentTransactionStub alloc] + initWithMap:transactionMap] ]); + + __block NSArray* resultArray; + self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:mockQueue + transactionsUpdated:nil + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:nil + updatedDownloads:nil]; + [self.plugin handleMethodCall:call + result:^(id r) { + resultArray = r; + [expectation fulfill]; + }]; + [self waitForExpectations:@[ expectation ] timeout:5]; + XCTAssertEqualObjects(resultArray, @[ transactionMap ]); +} + +- (void)testStartAndStopObservingPaymentQueue { + FlutterMethodCall* startCall = [FlutterMethodCall + methodCallWithMethodName:@"-[SKPaymentQueue startObservingTransactionQueue]" + arguments:nil]; + FlutterMethodCall* stopCall = + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue stopObservingTransactionQueue]" + arguments:nil]; + + SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; + + self.plugin.paymentQueueHandler = + [[FIAPaymentQueueHandler alloc] initWithQueue:queue + transactionsUpdated:nil + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, + SKProduct* _Nonnull product) { + return YES; + } + updatedDownloads:nil]; + + // Check that there is no observer to start with. + XCTAssertNil(queue.observer); + + // Start observing + [self.plugin handleMethodCall:startCall + result:^(id r){ + }]; + + // Observer should be set + XCTAssertNotNil(queue.observer); + + // Stop observing + [self.plugin handleMethodCall:stopCall + result:^(id r){ + }]; + + // No observer should be set + XCTAssertNil(queue.observer); +} + +- (void)testRegisterPaymentQueueDelegate { + if (@available(iOS 13, *)) { + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue registerDelegate]" + arguments:nil]; + + self.plugin.paymentQueueHandler = + [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueueStub new] + transactionsUpdated:nil + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:nil + updatedDownloads:nil]; + + // Verify the delegate is nil before we register one. + XCTAssertNil(self.plugin.paymentQueueHandler.delegate); + + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + + // Verify the delegate is not nil after we registered one. + XCTAssertNotNil(self.plugin.paymentQueueHandler.delegate); + } +} + +- (void)testRemovePaymentQueueDelegate { + if (@available(iOS 13, *)) { + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue removeDelegate]" + arguments:nil]; + + self.plugin.paymentQueueHandler = + [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueueStub new] + transactionsUpdated:nil + transactionRemoved:nil + restoreTransactionFailed:nil + restoreCompletedTransactionsFinished:nil + shouldAddStorePayment:nil + updatedDownloads:nil]; + self.plugin.paymentQueueHandler.delegate = OCMProtocolMock(@protocol(SKPaymentQueueDelegate)); + + // Verify the delegate is not nil before removing it. + XCTAssertNotNil(self.plugin.paymentQueueHandler.delegate); + + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + + // Verify the delegate is nill after removing it. + XCTAssertNil(self.plugin.paymentQueueHandler.delegate); + } +} + +- (void)testShowPriceConsentIfNeeded { + FlutterMethodCall* call = + [FlutterMethodCall methodCallWithMethodName:@"-[SKPaymentQueue showPriceConsentIfNeeded]" + arguments:nil]; + + FIAPaymentQueueHandler* mockQueueHandler = OCMClassMock(FIAPaymentQueueHandler.class); + self.plugin.paymentQueueHandler = mockQueueHandler; + + [self.plugin handleMethodCall:call + result:^(id r){ + }]; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wpartial-availability" + if (@available(iOS 13.4, *)) { + OCMVerify(times(1), [mockQueueHandler showPriceConsentIfNeeded]); + } else { + OCMVerify(never(), [mockQueueHandler showPriceConsentIfNeeded]); + } +#pragma clang diagnostic pop +} + +@end diff --git a/packages/in_app_purchase/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 77% 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..085a06337386 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)) @@ -36,6 +36,7 @@ API_AVAILABLE(ios(11.2), macos(10.13.2)) @interface SKPaymentQueueStub : SKPaymentQueue @property(assign, nonatomic) SKPaymentTransactionState testState; +@property(strong, nonatomic, nullable) id observer; @end @interface SKPaymentTransactionStub : SKPaymentTransaction @@ -53,10 +54,18 @@ API_AVAILABLE(ios(11.2), macos(10.13.2)) @end @interface FIAPReceiptManagerStub : FIAPReceiptManager +// Indicates whether getReceiptData of this stub is going to return an error. +// Setting this to true will let getReceiptData give a basic NSError and return nil. +@property(assign, nonatomic) BOOL returnError; @end @interface SKReceiptRefreshRequestStub : SKReceiptRefreshRequest - (instancetype)initWithFailureError:(NSError *)error; @end +API_AVAILABLE(ios(13.0), macos(10.15)) +@interface SKStorefrontStub : SKStorefront +- (instancetype)initWithMap:(NSDictionary *)map; +@end + NS_ASSUME_NONNULL_END 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 81% 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 2c3460f17f4b..364505d6754a 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. @@ -151,8 +151,6 @@ - (SKReceiptRefreshRequestStub *)getRefreshReceiptRequest:(NSDictionary *)proper @interface SKPaymentQueueStub () -@property(strong, nonatomic) id observer; - @end @implementation SKPaymentQueueStub @@ -161,6 +159,10 @@ - (void)addTransactionObserver:(id)observer { self.observer = observer; } +- (void)removeTransactionObserver:(id)observer { + self.observer = nil; +} + - (void)addPayment:(SKPayment *)payment { SKPaymentTransactionStub *transaction = [[SKPaymentTransactionStub alloc] initWithState:self.testState payment:payment]; @@ -215,7 +217,11 @@ - (instancetype)initWithMap:(NSDictionary *)map { - (instancetype)initWithState:(SKPaymentTransactionState)state { self = [super init]; if (self) { - [self setValue:@"fakeID" forKey:@"transactionIdentifier"]; + // Only purchased and restored transactions have transactionIdentifier: + // https://developer.apple.com/documentation/storekit/skpaymenttransaction/1411288-transactionidentifier?language=objc + if (state == SKPaymentTransactionStatePurchased || state == SKPaymentTransactionStateRestored) { + [self setValue:@"fakeID" forKey:@"transactionIdentifier"]; + } [self setValue:@(state) forKey:@"transactionState"]; } return self; @@ -224,7 +230,11 @@ - (instancetype)initWithState:(SKPaymentTransactionState)state { - (instancetype)initWithState:(SKPaymentTransactionState)state payment:(SKPayment *)payment { self = [super init]; if (self) { - [self setValue:@"fakeID" forKey:@"transactionIdentifier"]; + // Only purchased and restored transactions have transactionIdentifier: + // https://developer.apple.com/documentation/storekit/skpaymenttransaction/1411288-transactionidentifier?language=objc + if (state == SKPaymentTransactionStatePurchased || state == SKPaymentTransactionStateRestored) { + [self setValue:@"fakeID" forKey:@"transactionIdentifier"]; + } [self setValue:@(state) forKey:@"transactionState"]; _payment = payment; } @@ -249,7 +259,19 @@ - (instancetype)initWithMap:(NSDictionary *)map { @implementation FIAPReceiptManagerStub : FIAPReceiptManager -- (NSData *)getReceiptData:(NSURL *)url { +- (NSData *)getReceiptData:(NSURL *)url error:(NSError **)error { + if (self.returnError) { + *error = [NSError errorWithDomain:@"test" + code:1 + userInfo:@{ + @"name" : @"test", + @"houseNr" : @5, + @"error" : [[NSError alloc] initWithDomain:@"internalTestDomain" + code:99 + userInfo:nil] + }]; + return nil; + } NSString *originalString = [NSString stringWithFormat:@"test"]; return [[NSData alloc] initWithBase64EncodedString:originalString options:kNilOptions]; } @@ -280,3 +302,17 @@ - (void)start { } @end + +@implementation SKStorefrontStub + +- (instancetype)initWithMap:(NSDictionary *)map { + self = [super init]; + if (self) { + // Set stub values + [self setValue:map[@"countryCode"] forKey:@"countryCode"]; + [self setValue:map[@"identifier"] forKey:@"identifier"]; + } + return self; +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/TranslatorTests.m b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/TranslatorTests.m new file mode 100644 index 000000000000..89a7b2c84380 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/ios/RunnerTests/TranslatorTests.m @@ -0,0 +1,178 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import "Stubs.h" + +@import in_app_purchase_ios; + +@interface TranslatorTest : XCTestCase + +@property(strong, nonatomic) NSDictionary *periodMap; +@property(strong, nonatomic) NSDictionary *discountMap; +@property(strong, nonatomic) NSMutableDictionary *productMap; +@property(strong, nonatomic) NSDictionary *productResponseMap; +@property(strong, nonatomic) NSDictionary *paymentMap; +@property(strong, nonatomic) NSDictionary *transactionMap; +@property(strong, nonatomic) NSDictionary *errorMap; +@property(strong, nonatomic) NSDictionary *localeMap; +@property(strong, nonatomic) NSDictionary *storefrontMap; +@property(strong, nonatomic) NSDictionary *storefrontAndPaymentTransactionMap; + +@end + +@implementation TranslatorTest + +- (void)setUp { + self.periodMap = @{@"numberOfUnits" : @(0), @"unit" : @(0)}; + self.discountMap = @{ + @"price" : @"1", + @"priceLocale" : [FIAObjectTranslator getMapFromNSLocale:NSLocale.systemLocale], + @"numberOfPeriods" : @1, + @"subscriptionPeriod" : self.periodMap, + @"paymentMode" : @1 + }; + + self.productMap = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"price" : @"1", + @"priceLocale" : [FIAObjectTranslator getMapFromNSLocale:NSLocale.systemLocale], + @"productIdentifier" : @"123", + @"localizedTitle" : @"title", + @"localizedDescription" : @"des", + }]; + if (@available(iOS 11.2, *)) { + self.productMap[@"subscriptionPeriod"] = self.periodMap; + self.productMap[@"introductoryPrice"] = self.discountMap; + } + + if (@available(iOS 12.0, *)) { + self.productMap[@"subscriptionGroupIdentifier"] = @"com.group"; + } + + self.productResponseMap = + @{@"products" : @[ self.productMap ], @"invalidProductIdentifiers" : @[]}; + self.paymentMap = @{ + @"productIdentifier" : @"123", + @"requestData" : @"abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh", + @"quantity" : @(2), + @"applicationUsername" : @"app user name", + @"simulatesAskToBuyInSandbox" : @(NO) + }; + NSDictionary *originalTransactionMap = @{ + @"transactionIdentifier" : @"567", + @"transactionState" : @(SKPaymentTransactionStatePurchasing), + @"payment" : [NSNull null], + @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" + code:123 + userInfo:@{}]], + @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), + @"originalTransaction" : [NSNull null], + }; + self.transactionMap = @{ + @"transactionIdentifier" : @"567", + @"transactionState" : @(SKPaymentTransactionStatePurchasing), + @"payment" : [NSNull null], + @"error" : [FIAObjectTranslator getMapFromNSError:[NSError errorWithDomain:@"test_stub" + code:123 + userInfo:@{}]], + @"transactionTimeStamp" : @([NSDate date].timeIntervalSince1970), + @"originalTransaction" : originalTransactionMap, + }; + self.errorMap = @{ + @"code" : @(123), + @"domain" : @"test_domain", + @"userInfo" : @{ + @"key" : @"value", + } + }; + self.storefrontMap = @{ + @"countryCode" : @"USA", + @"identifier" : @"unique_identifier", + }; + + self.storefrontAndPaymentTransactionMap = @{ + @"storefront" : self.storefrontMap, + @"transaction" : self.transactionMap, + }; +} + +- (void)testSKProductSubscriptionPeriodStubToMap { + if (@available(iOS 11.2, *)) { + SKProductSubscriptionPeriodStub *period = + [[SKProductSubscriptionPeriodStub alloc] initWithMap:self.periodMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKProductSubscriptionPeriod:period]; + XCTAssertEqualObjects(map, self.periodMap); + } +} + +- (void)testSKProductDiscountStubToMap { + if (@available(iOS 11.2, *)) { + SKProductDiscountStub *discount = [[SKProductDiscountStub alloc] initWithMap:self.discountMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKProductDiscount:discount]; + XCTAssertEqualObjects(map, self.discountMap); + } +} + +- (void)testProductToMap { + SKProductStub *product = [[SKProductStub alloc] initWithMap:self.productMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKProduct:product]; + XCTAssertEqualObjects(map, self.productMap); +} + +- (void)testProductResponseToMap { + SKProductsResponseStub *response = + [[SKProductsResponseStub alloc] initWithMap:self.productResponseMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKProductsResponse:response]; + XCTAssertEqualObjects(map, self.productResponseMap); +} + +- (void)testPaymentToMap { + SKMutablePayment *payment = [FIAObjectTranslator getSKMutablePaymentFromMap:self.paymentMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKPayment:payment]; + XCTAssertEqualObjects(map, self.paymentMap); +} + +- (void)testPaymentTransactionToMap { + // payment is not KVC, cannot test payment field. + SKPaymentTransactionStub *paymentTransaction = + [[SKPaymentTransactionStub alloc] initWithMap:self.transactionMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKPaymentTransaction:paymentTransaction]; + XCTAssertEqualObjects(map, self.transactionMap); +} + +- (void)testError { + NSErrorStub *error = [[NSErrorStub alloc] initWithMap:self.errorMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromNSError:error]; + XCTAssertEqualObjects(map, self.errorMap); +} + +- (void)testLocaleToMap { + if (@available(iOS 10.0, *)) { + NSLocale *system = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]; + NSDictionary *map = [FIAObjectTranslator getMapFromNSLocale:system]; + XCTAssertEqualObjects(map[@"currencySymbol"], system.currencySymbol); + XCTAssertEqualObjects(map[@"countryCode"], system.countryCode); + } +} + +- (void)testSKStorefrontToMap { + if (@available(iOS 13.0, *)) { + SKStorefront *storefront = [[SKStorefrontStub alloc] initWithMap:self.storefrontMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKStorefront:storefront]; + XCTAssertEqualObjects(map, self.storefrontMap); + } +} + +- (void)testSKStorefrontAndSKPaymentTransactionToMap { + if (@available(iOS 13.0, *)) { + SKStorefront *storefront = [[SKStorefrontStub alloc] initWithMap:self.storefrontMap]; + SKPaymentTransaction *transaction = + [[SKPaymentTransactionStub alloc] initWithMap:self.transactionMap]; + NSDictionary *map = [FIAObjectTranslator getMapFromSKStorefront:storefront + andSKPaymentTransaction:transaction]; + XCTAssertEqualObjects(map, self.storefrontAndPaymentTransactionMap); + } +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/example/lib/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/example_payment_queue_delegate.dart b/packages/in_app_purchase/in_app_purchase_ios/example/lib/example_payment_queue_delegate.dart new file mode 100644 index 000000000000..dfebdf9cdf98 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/lib/example_payment_queue_delegate.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:in_app_purchase_ios/store_kit_wrappers.dart'; + +/// Example implementation of the +/// [`SKPaymentQueueDelegate`](https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate?language=objc). +/// +/// The payment queue delegate can be implementated to provide information +/// needed to complete transactions. +class ExamplePaymentQueueDelegate implements SKPaymentQueueDelegateWrapper { + @override + bool shouldContinueTransaction( + SKPaymentTransactionWrapper transaction, SKStorefrontWrapper storefront) { + return true; + } + + @override + bool shouldShowPriceConsent() { + return false; + } +} 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..15ab64b6ea80 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/example/lib/main.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 'dart:async'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; +import 'package:in_app_purchase_ios_example/example_payment_queue_delegate.dart'; +import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; +import 'consumable_store.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + + // When using the Android plugin directly it is mandatory to register + // the plugin as default instance as part of initializing the app. + InAppPurchaseIosPlatform.registerPlatform(); + + runApp(_MyApp()); +} + +const bool _kAutoConsume = true; + +const String _kConsumableId = 'consumable'; +const String _kUpgradeId = 'upgrade'; +const String _kSilverSubscriptionId = 'subscription_silver'; +const String _kGoldSubscriptionId = 'subscription_gold'; +const List _kProductIds = [ + _kConsumableId, + _kUpgradeId, + _kSilverSubscriptionId, + _kGoldSubscriptionId, +]; + +class _MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State<_MyApp> { + final InAppPurchaseIosPlatform _iapIosPlatform = + InAppPurchasePlatform.instance as InAppPurchaseIosPlatform; + final InAppPurchaseIosPlatformAddition _iapIosPlatformAddition = + InAppPurchasePlatformAddition.instance + as InAppPurchaseIosPlatformAddition; + late StreamSubscription> _subscription; + List _notFoundIds = []; + List _products = []; + List _purchases = []; + List _consumables = []; + bool _isAvailable = false; + bool _purchasePending = false; + bool _loading = true; + String? _queryProductError; + + @override + void initState() { + final Stream> purchaseUpdated = + _iapIosPlatform.purchaseStream; + _subscription = purchaseUpdated.listen((purchaseDetailsList) { + _listenToPurchaseUpdated(purchaseDetailsList); + }, onDone: () { + _subscription.cancel(); + }, onError: (error) { + // handle error here. + }); + + // Register the example payment queue delegate + _iapIosPlatformAddition.setDelegate(ExamplePaymentQueueDelegate()); + + initStoreInfo(); + super.initState(); + } + + Future initStoreInfo() async { + final bool isAvailable = await _iapIosPlatform.isAvailable(); + if (!isAvailable) { + setState(() { + _isAvailable = isAvailable; + _products = []; + _purchases = []; + _notFoundIds = []; + _consumables = []; + _purchasePending = false; + _loading = false; + }); + return; + } + + ProductDetailsResponse productDetailResponse = + await _iapIosPlatform.queryProductDetails(_kProductIds.toSet()); + if (productDetailResponse.error != null) { + setState(() { + _queryProductError = productDetailResponse.error!.message; + _isAvailable = isAvailable; + _products = productDetailResponse.productDetails; + _purchases = []; + _notFoundIds = productDetailResponse.notFoundIDs; + _consumables = []; + _purchasePending = false; + _loading = false; + }); + return; + } + + if (productDetailResponse.productDetails.isEmpty) { + setState(() { + _queryProductError = null; + _isAvailable = isAvailable; + _products = productDetailResponse.productDetails; + _purchases = []; + _notFoundIds = productDetailResponse.notFoundIDs; + _consumables = []; + _purchasePending = false; + _loading = false; + }); + return; + } + + List consumables = await ConsumableStore.load(); + setState(() { + _isAvailable = isAvailable; + _products = productDetailResponse.productDetails; + _notFoundIds = productDetailResponse.notFoundIDs; + _consumables = consumables; + _purchasePending = false; + _loading = false; + }); + } + + @override + void dispose() { + _subscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + List stack = []; + if (_queryProductError == null) { + stack.add( + ListView( + children: [ + _buildConnectionCheckTile(), + _buildProductList(), + _buildConsumableBox(), + _buildRestoreButton(), + ], + ), + ); + } else { + stack.add(Center( + child: Text(_queryProductError!), + )); + } + if (_purchasePending) { + stack.add( + Stack( + children: [ + Opacity( + opacity: 0.3, + child: const ModalBarrier(dismissible: false, color: Colors.grey), + ), + Center( + child: CircularProgressIndicator(), + ), + ], + ), + ); + } + + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('IAP Example'), + ), + body: Stack( + children: stack, + ), + ), + ); + } + + Card _buildConnectionCheckTile() { + if (_loading) { + return Card(child: ListTile(title: const Text('Trying to connect...'))); + } + final Widget storeHeader = ListTile( + leading: Icon(_isAvailable ? Icons.check : Icons.block, + color: _isAvailable ? Colors.green : ThemeData.light().errorColor), + title: Text( + 'The store is ' + (_isAvailable ? 'available' : 'unavailable') + '.'), + ); + final List children = [storeHeader]; + + if (!_isAvailable) { + children.addAll([ + Divider(), + ListTile( + title: Text('Not connected', + style: TextStyle(color: ThemeData.light().errorColor)), + subtitle: const Text( + 'Unable to connect to the payments processor. Has this app been configured correctly? See the example README for instructions.'), + ), + ]); + } + return Card(child: Column(children: children)); + } + + Card _buildProductList() { + if (_loading) { + return Card( + child: (ListTile( + leading: CircularProgressIndicator(), + title: Text('Fetching products...')))); + } + if (!_isAvailable) { + return Card(); + } + final ListTile productHeader = ListTile(title: Text('Products for Sale')); + List productList = []; + if (_notFoundIds.isNotEmpty) { + productList.add(ListTile( + title: Text('[${_notFoundIds.join(", ")}] not found', + style: TextStyle(color: ThemeData.light().errorColor)), + subtitle: Text( + 'This app needs special configuration to run. Please see example/README.md for instructions.'))); + } + + // This loading previous purchases code is just a demo. Please do not use this as it is. + // In your app you should always verify the purchase data using the `verificationData` inside the [PurchaseDetails] object before trusting it. + // We recommend that you use your own server to verify the purchase data. + Map purchases = + Map.fromEntries(_purchases.map((PurchaseDetails purchase) { + if (purchase.pendingCompletePurchase) { + _iapIosPlatform.completePurchase(purchase); + } + return MapEntry(purchase.productID, purchase); + })); + productList.addAll(_products.map( + (ProductDetails productDetails) { + PurchaseDetails? previousPurchase = purchases[productDetails.id]; + return ListTile( + title: Text( + productDetails.title, + ), + subtitle: Text( + productDetails.description, + ), + trailing: previousPurchase != null + ? IconButton( + onPressed: () { + _iapIosPlatformAddition.showPriceConsentIfNeeded(); + }, + icon: Icon(Icons.upgrade)) + : TextButton( + child: Text(productDetails.price), + style: TextButton.styleFrom( + backgroundColor: Colors.green[800], + primary: Colors.white, + ), + onPressed: () { + PurchaseParam purchaseParam = PurchaseParam( + productDetails: productDetails, + applicationUserName: null, + ); + if (productDetails.id == _kConsumableId) { + _iapIosPlatform.buyConsumable( + purchaseParam: purchaseParam, + autoConsume: _kAutoConsume || Platform.isIOS); + } else { + _iapIosPlatform.buyNonConsumable( + purchaseParam: purchaseParam); + } + }, + )); + }, + )); + + return Card( + child: + Column(children: [productHeader, Divider()] + productList)); + } + + Card _buildConsumableBox() { + if (_loading) { + return Card( + child: (ListTile( + leading: CircularProgressIndicator(), + title: Text('Fetching consumables...')))); + } + if (!_isAvailable || _notFoundIds.contains(_kConsumableId)) { + return Card(); + } + final ListTile consumableHeader = + ListTile(title: Text('Purchased consumables')); + final List tokens = _consumables.map((String id) { + return GridTile( + child: IconButton( + icon: Icon( + Icons.stars, + size: 42.0, + color: Colors.orange, + ), + splashColor: Colors.yellowAccent, + onPressed: () => consume(id), + ), + ); + }).toList(); + return Card( + child: Column(children: [ + consumableHeader, + Divider(), + GridView.count( + crossAxisCount: 5, + children: tokens, + shrinkWrap: true, + padding: EdgeInsets.all(16.0), + ) + ])); + } + + Widget _buildRestoreButton() { + if (_loading) { + return Container(); + } + + return Padding( + padding: const EdgeInsets.all(4.0), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + child: Text('Restore purchases'), + style: TextButton.styleFrom( + backgroundColor: Theme.of(context).primaryColor, + primary: Colors.white, + ), + onPressed: () => _iapIosPlatform.restorePurchases(), + ), + ], + ), + ); + } + + Future consume(String id) async { + await ConsumableStore.consume(id); + final List consumables = await ConsumableStore.load(); + setState(() { + _consumables = consumables; + }); + } + + void showPendingUI() { + setState(() { + _purchasePending = true; + }); + } + + void deliverProduct(PurchaseDetails purchaseDetails) async { + // IMPORTANT!! Always verify purchase details before delivering the product. + if (purchaseDetails.productID == _kConsumableId) { + await ConsumableStore.save(purchaseDetails.purchaseID!); + List consumables = await ConsumableStore.load(); + setState(() { + _purchasePending = false; + _consumables = consumables; + }); + } else { + setState(() { + _purchases.add(purchaseDetails); + _purchasePending = false; + }); + } + } + + void handleError(IAPError error) { + setState(() { + _purchasePending = false; + }); + } + + Future _verifyPurchase(PurchaseDetails purchaseDetails) { + // IMPORTANT!! Always verify a purchase before delivering the product. + // For the purpose of an example, we directly return true. + return Future.value(true); + } + + void _handleInvalidPurchase(PurchaseDetails purchaseDetails) { + // handle invalid purchase here if _verifyPurchase` failed. + } + + void _listenToPurchaseUpdated(List purchaseDetailsList) { + purchaseDetailsList.forEach((PurchaseDetails purchaseDetails) async { + if (purchaseDetails.status == PurchaseStatus.pending) { + showPendingUI(); + } else { + if (purchaseDetails.status == PurchaseStatus.error) { + handleError(purchaseDetails.error!); + } else if (purchaseDetails.status == PurchaseStatus.purchased || + purchaseDetails.status == PurchaseStatus.restored) { + 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..0474d70e8b71 --- /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.14.0 <3.0.0" + flutter: ">=2.5.0" + +dependencies: + flutter: + sdk: flutter + shared_preferences: ^2.0.0 + in_app_purchase_ios: + # When depending on this package from a real application you should use: + # in_app_purchase: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + in_app_purchase_platform_interface: ^1.0.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + pedantic: ^1.10.0 + +flutter: + uses-material-design: true diff --git a/packages/in_app_purchase/in_app_purchase_ios/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/camera/ios/Assets/.gitkeep b/packages/in_app_purchase/in_app_purchase_ios/ios/Assets/.gitkeep similarity index 100% rename from packages/camera/ios/Assets/.gitkeep rename to packages/in_app_purchase/in_app_purchase_ios/ios/Assets/.gitkeep diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h new file mode 100644 index 000000000000..95a5edc245dc --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAObjectTranslator.h @@ -0,0 +1,53 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FIAObjectTranslator : NSObject + +// Converts an instance of SKProduct into a dictionary. ++ (NSDictionary *)getMapFromSKProduct:(SKProduct *)product; + +// Converts an instance of SKProductSubscriptionPeriod into a dictionary. ++ (NSDictionary *)getMapFromSKProductSubscriptionPeriod:(SKProductSubscriptionPeriod *)period + API_AVAILABLE(ios(11.2)); + +// Converts an instance of SKProductDiscount into a dictionary. ++ (NSDictionary *)getMapFromSKProductDiscount:(SKProductDiscount *)discount + API_AVAILABLE(ios(11.2)); + +// Converts an instance of SKProductsResponse into a dictionary. ++ (NSDictionary *)getMapFromSKProductsResponse:(SKProductsResponse *)productResponse; + +// Converts an instance of SKPayment into a dictionary. ++ (NSDictionary *)getMapFromSKPayment:(SKPayment *)payment; + +// Converts an instance of NSLocale into a dictionary. ++ (NSDictionary *)getMapFromNSLocale:(NSLocale *)locale; + +// Creates an instance of the SKMutablePayment class based on the supplied dictionary. ++ (SKMutablePayment *)getSKMutablePaymentFromMap:(NSDictionary *)map; + +// Converts an instance of SKPaymentTransaction into a dictionary. ++ (NSDictionary *)getMapFromSKPaymentTransaction:(SKPaymentTransaction *)transaction; + +// Converts an instance of NSError into a dictionary. ++ (NSDictionary *)getMapFromNSError:(NSError *)error; + +// Converts an instance of SKStorefront into a dictionary. ++ (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront + API_AVAILABLE(ios(13), macos(10.15), watchos(6.2)); + +// Converts the supplied instances of SKStorefront and SKPaymentTransaction into a dictionary. ++ (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront + andSKPaymentTransaction:(SKPaymentTransaction *)transaction + API_AVAILABLE(ios(13), macos(10.15), watchos(6.2)); + +@end +; + +NS_ASSUME_NONNULL_END 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 85% 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..0125604b3b3c 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. @@ -96,9 +96,7 @@ + (NSDictionary *)getMapFromSKPayment:(SKPayment *)payment { @"quantity" : @(payment.quantity), @"applicationUsername" : payment.applicationUsername ?: [NSNull null] }]; - if (@available(iOS 8.3, *)) { - [map setObject:@(payment.simulatesAskToBuyInSandbox) forKey:@"simulatesAskToBuyInSandbox"]; - } + [map setObject:@(payment.simulatesAskToBuyInSandbox) forKey:@"simulatesAskToBuyInSandbox"]; return map; } @@ -111,6 +109,7 @@ + (NSDictionary *)getMapFromNSLocale:(NSLocale *)locale { forKey:@"currencySymbol"]; [map setObject:[locale objectForKey:NSLocaleCurrencyCode] ?: [NSNull null] forKey:@"currencyCode"]; + [map setObject:[locale objectForKey:NSLocaleCountryCode] ?: [NSNull null] forKey:@"countryCode"]; return map; } @@ -124,9 +123,7 @@ + (SKMutablePayment *)getSKMutablePaymentFromMap:(NSDictionary *)map { payment.requestData = [utf8String dataUsingEncoding:NSUTF8StringEncoding]; payment.quantity = [map[@"quantity"] integerValue]; payment.applicationUsername = map[@"applicationUsername"]; - if (@available(iOS 8.3, *)) { - payment.simulatesAskToBuyInSandbox = [map[@"simulatesAskToBuyInSandbox"] boolValue]; - } + payment.simulatesAskToBuyInSandbox = [map[@"simulatesAskToBuyInSandbox"] boolValue]; return payment; } @@ -169,4 +166,31 @@ + (NSDictionary *)getMapFromNSError:(NSError *)error { return @{@"code" : @(error.code), @"domain" : error.domain ?: @"", @"userInfo" : userInfo}; } ++ (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront { + if (!storefront) { + return nil; + } + + NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"countryCode" : storefront.countryCode, + @"identifier" : storefront.identifier + }]; + + return map; +} + ++ (NSDictionary *)getMapFromSKStorefront:(SKStorefront *)storefront + andSKPaymentTransaction:(SKPaymentTransaction *)transaction { + if (!storefront || !transaction) { + return nil; + } + + NSMutableDictionary *map = [[NSMutableDictionary alloc] initWithDictionary:@{ + @"storefront" : [FIAObjectTranslator getMapFromSKStorefront:storefront], + @"transaction" : [FIAObjectTranslator getMapFromSKPaymentTransaction:transaction] + }]; + + return map; +} + @end diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.h new file mode 100644 index 000000000000..a6c91fa9e6b6 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.h @@ -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 +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +API_AVAILABLE(ios(13)) +@interface FIAPPaymentQueueDelegate : NSObject +- (id)initWithMethodChannel:(FlutterMethodChannel *)methodChannel; +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.m new file mode 100644 index 000000000000..1056086030a5 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPPaymentQueueDelegate.m @@ -0,0 +1,78 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FIAPPaymentQueueDelegate.h" +#import "FIAObjectTranslator.h" + +@interface FIAPPaymentQueueDelegate () + +@property(strong, nonatomic, readonly) FlutterMethodChannel *callbackChannel; + +@end + +@implementation FIAPPaymentQueueDelegate + +- (id)initWithMethodChannel:(FlutterMethodChannel *)methodChannel { + self = [super init]; + if (self) { + _callbackChannel = methodChannel; + } + + return self; +} + +- (BOOL)paymentQueue:(SKPaymentQueue *)paymentQueue + shouldContinueTransaction:(SKPaymentTransaction *)transaction + inStorefront:(SKStorefront *)newStorefront { + // Default return value for this method is true (see + // https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate/3521328-paymentqueueshouldshowpriceconse?language=objc) + __block BOOL shouldContinue = YES; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + [self.callbackChannel invokeMethod:@"shouldContinueTransaction" + arguments:[FIAObjectTranslator getMapFromSKStorefront:newStorefront + andSKPaymentTransaction:transaction] + result:^(id _Nullable result) { + // When result is a valid instance of NSNumber use it to determine + // if the transaction should continue. Otherwise use the default + // value. + if (result && [result isKindOfClass:[NSNumber class]]) { + shouldContinue = [(NSNumber *)result boolValue]; + } + + dispatch_semaphore_signal(semaphore); + }]; + + // The client should respond within 1 second otherwise continue + // with default value. + dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC)); + + return shouldContinue; +} + +- (BOOL)paymentQueueShouldShowPriceConsent:(SKPaymentQueue *)paymentQueue { + // Default return value for this method is true (see + // https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate/3521328-paymentqueueshouldshowpriceconse?language=objc) + __block BOOL shouldShowPriceConsent = YES; + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + [self.callbackChannel invokeMethod:@"shouldShowPriceConsent" + arguments:nil + result:^(id _Nullable result) { + // When result is a valid instance of NSNumber use it to determine + // if the transaction should continue. Otherwise use the default + // value. + if (result && [result isKindOfClass:[NSNumber class]]) { + shouldShowPriceConsent = [(NSNumber *)result boolValue]; + } + + dispatch_semaphore_signal(semaphore); + }]; + + // The client should respond within 1 second otherwise continue + // with default value. + dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC)); + + return shouldShowPriceConsent; +} + +@end 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/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m new file mode 100644 index 000000000000..b359b415d873 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPReceiptManager.m @@ -0,0 +1,38 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FIAPReceiptManager.h" +#import +#import "FIAObjectTranslator.h" + +@interface FIAPReceiptManager () +// Gets the receipt file data from the location of the url. Can be nil if +// there is an error. This interface is defined so it can be stubbed for testing. +- (NSData *)getReceiptData:(NSURL *)url error:(NSError **)error; + +@end + +@implementation FIAPReceiptManager + +- (NSString *)retrieveReceiptWithError:(FlutterError **)flutterError { + NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; + NSError *receiptError; + NSData *receipt = [self getReceiptData:receiptURL error:&receiptError]; + if (!receipt || receiptError) { + if (flutterError) { + NSDictionary *errorMap = [FIAObjectTranslator getMapFromNSError:receiptError]; + *flutterError = [FlutterError errorWithCode:errorMap[@"code"] + message:errorMap[@"domain"] + details:errorMap[@"userInfo"]]; + } + return nil; + } + return [receipt base64EncodedStringWithOptions:kNilOptions]; +} + +- (NSData *)getReceiptData:(NSURL *)url error:(NSError **)error { + return [NSData dataWithContentsOfURL:url options:NSDataReadingMappedIfSafe error:error]; +} + +@end 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/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h new file mode 100644 index 000000000000..8019831d6355 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.h @@ -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 +#import + +@class SKPaymentTransaction; + +NS_ASSUME_NONNULL_BEGIN + +typedef void (^TransactionsUpdated)(NSArray *transactions); +typedef void (^TransactionsRemoved)(NSArray *transactions); +typedef void (^RestoreTransactionFailed)(NSError *error); +typedef void (^RestoreCompletedTransactionsFinished)(void); +typedef BOOL (^ShouldAddStorePayment)(SKPayment *payment, SKProduct *product); +typedef void (^UpdatedDownloads)(NSArray *downloads); + +@interface FIAPaymentQueueHandler : NSObject + +@property(NS_NONATOMIC_IOSONLY, weak, nullable) id delegate API_AVAILABLE( + ios(13.0), macos(10.15), watchos(6.2)); + +- (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue + transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated + transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved + restoreTransactionFailed:(nullable RestoreTransactionFailed)restoreTransactionFailed + restoreCompletedTransactionsFinished: + (nullable RestoreCompletedTransactionsFinished)restoreCompletedTransactionsFinished + shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment + updatedDownloads:(nullable UpdatedDownloads)updatedDownloads; +// Can throw exceptions if the transaction type is purchasing, should always used in a @try block. +- (void)finishTransaction:(nonnull SKPaymentTransaction *)transaction; +- (void)restoreTransactions:(nullable NSString *)applicationName; +- (void)presentCodeRedemptionSheet; +- (NSArray *)getUnfinishedTransactions; + +// This method needs to be called before any other methods. +- (void)startObservingPaymentQueue; +// Call this method when the Flutter app is no longer listening +- (void)stopObservingPaymentQueue; + +// Appends a payment to the SKPaymentQueue. +// +// @param payment Payment object to be added to the payment queue. +// @return whether "addPayment" was successful. +- (BOOL)addPayment:(SKPayment *)payment; + +// Displays the price consent sheet. +// +// The price consent sheet is only displayed when the following +// it true: +// - You have increased the price of the subscription in App Store Connect. +// - The subscriber has not yet responded to a price consent query. +// Otherwise the method has no effect. +- (void)showPriceConsentIfNeeded API_AVAILABLE(ios(13.4)); + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.m similarity index 76% 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 8bdb7f25f111..21667954cf8d 100644 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.m +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/FIAPaymentQueueHandler.m @@ -1,8 +1,9 @@ -// 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 "FIAPaymentQueueHandler.h" +#import "FIAPPaymentQueueDelegate.h" @interface FIAPaymentQueueHandler () @@ -15,9 +16,6 @@ @interface FIAPaymentQueueHandler () @property(nullable, copy, nonatomic) ShouldAddStorePayment shouldAddStorePayment; @property(nullable, copy, nonatomic) UpdatedDownloads updatedDownloads; -@property(strong, nonatomic) - NSMutableDictionary *transactionsSetter; - @end @implementation FIAPaymentQueueHandler @@ -39,7 +37,10 @@ - (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue _paymentQueueRestoreCompletedTransactionsFinished = restoreCompletedTransactionsFinished; _shouldAddStorePayment = shouldAddStorePayment; _updatedDownloads = updatedDownloads; - _transactionsSetter = [NSMutableDictionary dictionary]; + + if (@available(iOS 13.0, macOS 10.15, *)) { + queue.delegate = self.delegate; + } } return self; } @@ -48,9 +49,15 @@ - (void)startObservingPaymentQueue { [_queue addTransactionObserver:self]; } +- (void)stopObservingPaymentQueue { + [_queue removeTransactionObserver:self]; +} + - (BOOL)addPayment:(SKPayment *)payment { - if (self.transactionsSetter[payment.productIdentifier]) { - return NO; + for (SKPaymentTransaction *transaction in self.queue.transactions) { + if ([transaction.payment.productIdentifier isEqualToString:payment.productIdentifier]) { + return NO; + } } [self.queue addPayment:payment]; return YES; @@ -68,22 +75,24 @@ - (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"); + } +} + +- (void)showPriceConsentIfNeeded { + [self.queue showPriceConsentIfNeeded]; +} + #pragma mark - observing // Sent when the transaction array has changed (additions or state changes). Client should check // state of transactions and finish as appropriate. - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { - for (SKPaymentTransaction *transaction in transactions) { - if (transaction.transactionIdentifier) { - // Use product identifier instead of transaction identifier for few reasons: - // 1. Only transactions with purchased state and failed state will have a transaction id, it - // will become impossible for clients to finish deferred transactions when needed. - // 2. Using product identifiers can help prevent clients from purchasing the same - // subscription more than once by accident. - self.transactionsSetter[transaction.payment.productIdentifier] = transaction; - } - } // notify dart through callbacks. self.transactionsUpdated(transactions); } @@ -91,11 +100,6 @@ - (void)paymentQueue:(SKPaymentQueue *)queue // Sent when transactions are removed from the queue (via finishTransaction:). - (void)paymentQueue:(SKPaymentQueue *)queue removedTransactions:(NSArray *)transactions { - for (SKPaymentTransaction *transaction in transactions) { - if (transaction.transactionIdentifier) { - [self.transactionsSetter removeObjectForKey:transaction.payment.productIdentifier]; - } - } self.transactionsRemoved(transactions); } @@ -128,10 +132,4 @@ - (BOOL)paymentQueue:(SKPaymentQueue *)queue return self.queue.transactions; } -#pragma mark - getter - -- (NSDictionary *)transactions { - return [self.transactionsSetter copy]; -} - @end 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/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m new file mode 100644 index 000000000000..7e2d2ca80675 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/ios/Classes/InAppPurchasePlugin.m @@ -0,0 +1,417 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "InAppPurchasePlugin.h" +#import +#import "FIAObjectTranslator.h" +#import "FIAPPaymentQueueDelegate.h" +#import "FIAPReceiptManager.h" +#import "FIAPRequestHandler.h" +#import "FIAPaymentQueueHandler.h" + +@interface InAppPurchasePlugin () + +// Holding strong references to FIAPRequestHandlers. Remove the handlers from the set after +// the request is finished. +@property(strong, nonatomic, readonly) NSMutableSet *requestHandlers; + +// After querying the product, the available products will be saved in the map to be used +// for purchase. +@property(strong, nonatomic, readonly) NSMutableDictionary *productsCache; + +// Callback channel to dart used for when a function from the transaction observer is triggered. +@property(strong, nonatomic, readonly) FlutterMethodChannel *transactionObserverCallbackChannel; + +// Callback channel to dart used for when a function from the payment queue delegate is triggered. +@property(strong, nonatomic, readonly) FlutterMethodChannel *paymentQueueDelegateCallbackChannel; + +@property(strong, nonatomic, readonly) NSObject *registry; +@property(strong, nonatomic, readonly) NSObject *messenger; +@property(strong, nonatomic, readonly) NSObject *registrar; + +@property(strong, nonatomic, readonly) FIAPReceiptManager *receiptManager; +@property(strong, nonatomic, readonly) + FIAPPaymentQueueDelegate *paymentQueueDelegate API_AVAILABLE(ios(13)); + +@end + +@implementation InAppPurchasePlugin + ++ (void)registerWithRegistrar:(NSObject *)registrar { + FlutterMethodChannel *channel = + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase" + binaryMessenger:[registrar messenger]]; + InAppPurchasePlugin *instance = [[InAppPurchasePlugin alloc] initWithRegistrar:registrar]; + [registrar addMethodCallDelegate:instance channel:channel]; +} + +- (instancetype)initWithReceiptManager:(FIAPReceiptManager *)receiptManager { + self = [super init]; + _receiptManager = receiptManager; + _requestHandlers = [NSMutableSet new]; + _productsCache = [NSMutableDictionary new]; + return self; +} + +- (instancetype)initWithRegistrar:(NSObject *)registrar { + self = [self initWithReceiptManager:[FIAPReceiptManager new]]; + _registrar = registrar; + _registry = [registrar textures]; + _messenger = [registrar messenger]; + + __weak typeof(self) weakSelf = self; + _paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueue defaultQueue] + transactionsUpdated:^(NSArray *_Nonnull transactions) { + [weakSelf handleTransactionsUpdated:transactions]; + } + transactionRemoved:^(NSArray *_Nonnull transactions) { + [weakSelf handleTransactionsRemoved:transactions]; + } + restoreTransactionFailed:^(NSError *_Nonnull error) { + [weakSelf handleTransactionRestoreFailed:error]; + } + restoreCompletedTransactionsFinished:^{ + [weakSelf restoreCompletedTransactionsFinished]; + } + shouldAddStorePayment:^BOOL(SKPayment *payment, SKProduct *product) { + return [weakSelf shouldAddStorePayment:payment product:product]; + } + updatedDownloads:^void(NSArray *_Nonnull downloads) { + [weakSelf updatedDownloads:downloads]; + }]; + + _transactionObserverCallbackChannel = + [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase" + binaryMessenger:[registrar messenger]]; + return self; +} + +- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { + if ([@"-[SKPaymentQueue canMakePayments:]" isEqualToString:call.method]) { + [self canMakePayments:result]; + } else if ([@"-[SKPaymentQueue transactions]" isEqualToString:call.method]) { + [self getPendingTransactions:result]; + } else if ([@"-[InAppPurchasePlugin startProductRequest:result:]" isEqualToString:call.method]) { + [self handleProductRequestMethodCall:call result:result]; + } else if ([@"-[InAppPurchasePlugin addPayment:result:]" isEqualToString:call.method]) { + [self addPayment:call result:result]; + } else if ([@"-[InAppPurchasePlugin finishTransaction:result:]" isEqualToString:call.method]) { + [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]) { + [self refreshReceipt:call result:result]; + } else if ([@"-[SKPaymentQueue startObservingTransactionQueue]" isEqualToString:call.method]) { + [self startObservingPaymentQueue:result]; + } else if ([@"-[SKPaymentQueue stopObservingTransactionQueue]" isEqualToString:call.method]) { + [self stopObservingPaymentQueue:result]; + } else if ([@"-[SKPaymentQueue registerDelegate]" isEqualToString:call.method]) { + [self registerPaymentQueueDelegate:result]; + } else if ([@"-[SKPaymentQueue removeDelegate]" isEqualToString:call.method]) { + [self removePaymentQueueDelegate:result]; + } else if ([@"-[SKPaymentQueue showPriceConsentIfNeeded]" isEqualToString:call.method]) { + [self showPriceConsentIfNeeded:result]; + } else { + result(FlutterMethodNotImplemented); + } +} + +- (void)canMakePayments:(FlutterResult)result { + result(@([SKPaymentQueue canMakePayments])); +} + +- (void)getPendingTransactions:(FlutterResult)result { + NSArray *transactions = + [self.paymentQueueHandler getUnfinishedTransactions]; + NSMutableArray *transactionMaps = [[NSMutableArray alloc] init]; + for (SKPaymentTransaction *transaction in transactions) { + [transactionMaps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]]; + } + result(transactionMaps); +} + +- (void)handleProductRequestMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { + if (![call.arguments isKindOfClass:[NSArray class]]) { + result([FlutterError errorWithCode:@"storekit_invalid_argument" + message:@"Argument type of startRequest is not array" + details:call.arguments]); + return; + } + NSArray *productIdentifiers = (NSArray *)call.arguments; + SKProductsRequest *request = + [self getProductRequestWithIdentifiers:[NSSet setWithArray:productIdentifiers]]; + FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; + [self.requestHandlers addObject:handler]; + __weak typeof(self) weakSelf = self; + [handler startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + result([FlutterError errorWithCode:@"storekit_getproductrequest_platform_error" + message:error.localizedDescription + details:error.description]); + return; + } + if (!response) { + result([FlutterError errorWithCode:@"storekit_platform_no_response" + message:@"Failed to get SKProductResponse in startRequest " + @"call. Error occured on iOS platform" + details:call.arguments]); + return; + } + for (SKProduct *product in response.products) { + [self.productsCache setObject:product forKey:product.productIdentifier]; + } + result([FIAObjectTranslator getMapFromSKProductsResponse:response]); + [weakSelf.requestHandlers removeObject:handler]; + }]; +} + +- (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { + if (![call.arguments isKindOfClass:[NSDictionary class]]) { + result([FlutterError errorWithCode:@"storekit_invalid_argument" + message:@"Argument type of addPayment is not a Dictionary" + details:call.arguments]); + return; + } + NSDictionary *paymentMap = (NSDictionary *)call.arguments; + NSString *productID = [paymentMap objectForKey:@"productIdentifier"]; + // When a product is already fetched, we create a payment object with + // the product to process the payment. + SKProduct *product = [self getProduct:productID]; + if (!product) { + result([FlutterError + errorWithCode:@"storekit_invalid_payment_object" + message: + @"You have requested a payment for an invalid product. Either the " + @"`productIdentifier` of the payment is not valid or the product has not been " + @"fetched before adding the payment to the payment queue." + details:call.arguments]); + return; + } + SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product]; + payment.applicationUsername = [paymentMap objectForKey:@"applicationUsername"]; + NSNumber *quantity = [paymentMap objectForKey:@"quantity"]; + payment.quantity = (quantity != nil) ? quantity.integerValue : 1; + NSNumber *simulatesAskToBuyInSandbox = [paymentMap objectForKey:@"simulatesAskToBuyInSandbox"]; + payment.simulatesAskToBuyInSandbox = (id)simulatesAskToBuyInSandbox == (id)[NSNull null] + ? NO + : [simulatesAskToBuyInSandbox boolValue]; + + if (![self.paymentQueueHandler addPayment:payment]) { + result([FlutterError + errorWithCode:@"storekit_duplicate_product_object" + message:@"There is a pending transaction for the same product identifier. Please " + @"either wait for it to be finished or finish it manually using " + @"`completePurchase` to avoid edge cases." + + details:call.arguments]); + return; + } + result(nil); +} + +- (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result { + if (![call.arguments isKindOfClass:[NSDictionary class]]) { + result([FlutterError errorWithCode:@"storekit_invalid_argument" + message:@"Argument type of finishTransaction is not a Dictionary" + details:call.arguments]); + return; + } + 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 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) { + result([FlutterError errorWithCode:@"storekit_finish_transaction_exception" + message:e.name + details:e.description]); + return; + } + } + } + + result(nil); +} + +- (void)restoreTransactions:(FlutterMethodCall *)call result:(FlutterResult)result { + if (call.arguments && ![call.arguments isKindOfClass:[NSString class]]) { + result([FlutterError + errorWithCode:@"storekit_invalid_argument" + message:@"Argument is not nil and the type of finishTransaction is not a string." + details:call.arguments]); + return; + } + [self.paymentQueueHandler restoreTransactions:call.arguments]; + 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]; + if (error) { + result(error); + return; + } + result(receiptData); +} + +- (void)refreshReceipt:(FlutterMethodCall *)call result:(FlutterResult)result { + NSDictionary *arguments = call.arguments; + SKReceiptRefreshRequest *request; + if (arguments) { + if (![arguments isKindOfClass:[NSDictionary class]]) { + result([FlutterError errorWithCode:@"storekit_invalid_argument" + message:@"Argument type of startRequest is not array" + details:call.arguments]); + return; + } + NSMutableDictionary *properties = [NSMutableDictionary new]; + properties[SKReceiptPropertyIsExpired] = arguments[@"isExpired"]; + properties[SKReceiptPropertyIsRevoked] = arguments[@"isRevoked"]; + properties[SKReceiptPropertyIsVolumePurchase] = arguments[@"isVolumePurchase"]; + request = [self getRefreshReceiptRequest:properties]; + } else { + request = [self getRefreshReceiptRequest:nil]; + } + FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; + [self.requestHandlers addObject:handler]; + __weak typeof(self) weakSelf = self; + [handler startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable response, + NSError *_Nullable error) { + if (error) { + result([FlutterError errorWithCode:@"storekit_refreshreceiptrequest_platform_error" + message:error.localizedDescription + details:error.description]); + return; + } + result(nil); + [weakSelf.requestHandlers removeObject:handler]; + }]; +} + +- (void)startObservingPaymentQueue:(FlutterResult)result { + [_paymentQueueHandler startObservingPaymentQueue]; + result(nil); +} + +- (void)stopObservingPaymentQueue:(FlutterResult)result { + [_paymentQueueHandler stopObservingPaymentQueue]; + result(nil); +} + +- (void)registerPaymentQueueDelegate:(FlutterResult)result { + if (@available(iOS 13.0, *)) { + _paymentQueueDelegateCallbackChannel = [FlutterMethodChannel + methodChannelWithName:@"plugins.flutter.io/in_app_purchase_payment_queue_delegate" + binaryMessenger:_messenger]; + + _paymentQueueDelegate = [[FIAPPaymentQueueDelegate alloc] + initWithMethodChannel:_paymentQueueDelegateCallbackChannel]; + _paymentQueueHandler.delegate = _paymentQueueDelegate; + } + result(nil); +} + +- (void)removePaymentQueueDelegate:(FlutterResult)result { + if (@available(iOS 13.0, *)) { + _paymentQueueHandler.delegate = nil; + } + _paymentQueueDelegate = nil; + _paymentQueueDelegateCallbackChannel = nil; + result(nil); +} + +- (void)showPriceConsentIfNeeded:(FlutterResult)result { + if (@available(iOS 13.4, *)) { + [_paymentQueueHandler showPriceConsentIfNeeded]; + } + result(nil); +} + +#pragma mark - transaction observer: + +- (void)handleTransactionsUpdated:(NSArray *)transactions { + NSMutableArray *maps = [NSMutableArray new]; + for (SKPaymentTransaction *transaction in transactions) { + [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]]; + } + [self.transactionObserverCallbackChannel invokeMethod:@"updatedTransactions" arguments:maps]; +} + +- (void)handleTransactionsRemoved:(NSArray *)transactions { + NSMutableArray *maps = [NSMutableArray new]; + for (SKPaymentTransaction *transaction in transactions) { + [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]]; + } + [self.transactionObserverCallbackChannel invokeMethod:@"removedTransactions" arguments:maps]; +} + +- (void)handleTransactionRestoreFailed:(NSError *)error { + [self.transactionObserverCallbackChannel + invokeMethod:@"restoreCompletedTransactionsFailed" + arguments:[FIAObjectTranslator getMapFromNSError:error]]; +} + +- (void)restoreCompletedTransactionsFinished { + [self.transactionObserverCallbackChannel + invokeMethod:@"paymentQueueRestoreCompletedTransactionsFinished" + arguments:nil]; +} + +- (void)updatedDownloads:(NSArray *)downloads { + NSLog(@"Received an updatedDownloads callback, but downloads are not supported."); +} + +- (BOOL)shouldAddStorePayment:(SKPayment *)payment product:(SKProduct *)product { + // We always return NO here. And we send the message to dart to process the payment; and we will + // have a interception method that deciding if the payment should be processed (implemented by the + // programmer). + [self.productsCache setObject:product forKey:product.productIdentifier]; + [self.transactionObserverCallbackChannel + invokeMethod:@"shouldAddStorePayment" + arguments:@{ + @"payment" : [FIAObjectTranslator getMapFromSKPayment:payment], + @"product" : [FIAObjectTranslator getMapFromSKProduct:product] + }]; + return NO; +} + +#pragma mark - dependency injection (for unit testing) + +- (SKProductsRequest *)getProductRequestWithIdentifiers:(NSSet *)identifiers { + return [[SKProductsRequest alloc] initWithProductIdentifiers:identifiers]; +} + +- (SKProduct *)getProduct:(NSString *)productID { + return [self.productsCache objectForKey:productID]; +} + +- (SKReceiptRefreshRequest *)getRefreshReceiptRequest:(NSDictionary *)properties { + return [[SKReceiptRefreshRequest alloc] initWithReceiptProperties:properties]; +} + +@end diff --git a/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase_ios.podspec b/packages/in_app_purchase/in_app_purchase_ios/ios/in_app_purchase_ios.podspec new file mode 100644 index 000000000000..3d15b5c0d02c --- /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, '9.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } +end diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/in_app_purchase_ios.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/in_app_purchase_ios.dart 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..d045dab448e8 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/channel.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/services.dart'; + +/// Method channel for the plugin's platform<-->Dart calls. +const MethodChannel channel = + MethodChannel('plugins.flutter.io/in_app_purchase'); + +/// Method channel used to deliver the payment queue delegate system calls to +/// Dart. +const MethodChannel paymentQueueDelegateChannel = + MethodChannel('plugins.flutter.io/in_app_purchase_payment_queue_delegate'); 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..74bb898a3382 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform.dart @@ -0,0 +1,213 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:in_app_purchase_ios/src/in_app_purchase_ios_platform_addition.dart'; +import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; + +import '../in_app_purchase_ios.dart'; +import '../store_kit_wrappers.dart'; + +/// [IAPError.code] code for failed purchases. +const String kPurchaseErrorCode = 'purchase_error'; + +/// Indicates store front is Apple AppStore. +const String kIAPSource = 'app_store'; + +/// An [InAppPurchasePlatform] that wraps StoreKit. +/// +/// This translates various `StoreKit` calls and responses into the +/// generic plugin API. +class InAppPurchaseIosPlatform extends InAppPurchasePlatform { + static late SKPaymentQueueWrapper _skPaymentQueueWrapper; + static late _TransactionObserver _observer; + + /// Creates an [InAppPurchaseIosPlatform] object. + /// + /// This constructor should only be used for testing, for any other purpose + /// get the connection from the [instance] getter. + @visibleForTesting + InAppPurchaseIosPlatform(); + + Stream> get purchaseStream => + _observer.purchaseUpdatedController.stream; + + /// Callback handler for transaction status changes. + @visibleForTesting + static SKTransactionObserverWrapper get observer => _observer; + + /// Registers this class as the default instance of [InAppPurchasePlatform]. + static void registerPlatform() { + // Register the [InAppPurchaseIosPlatformAddition] containing iOS + // platform-specific functionality. + InAppPurchasePlatformAddition.instance = InAppPurchaseIosPlatformAddition(); + + // Register the platform-specific implementation of the idiomatic + // InAppPurchase API. + InAppPurchasePlatform.instance = InAppPurchaseIosPlatform(); + + _skPaymentQueueWrapper = SKPaymentQueueWrapper(); + + // Create a purchaseUpdatedController and notify the native side when to + // start of stop sending updates. + StreamController> updateController = + StreamController.broadcast( + onListen: () => _skPaymentQueueWrapper.startObservingTransactionQueue(), + onCancel: () => _skPaymentQueueWrapper.stopObservingTransactionQueue(), + ); + _observer = _TransactionObserver(updateController); + _skPaymentQueueWrapper.setTransactionObserver(observer); + } + + @override + Future isAvailable() => SKPaymentQueueWrapper.canMakePayments(); + + @override + Future buyNonConsumable({required PurchaseParam purchaseParam}) async { + await _skPaymentQueueWrapper.addPayment(SKPaymentWrapper( + productIdentifier: purchaseParam.productDetails.id, + quantity: 1, + applicationUsername: purchaseParam.applicationUserName, + simulatesAskToBuyInSandbox: (purchaseParam is AppStorePurchaseParam) + ? purchaseParam.simulatesAskToBuyInSandbox + : false, + requestData: null)); + + return true; // There's no error feedback from iOS here to return. + } + + @override + Future buyConsumable( + {required PurchaseParam purchaseParam, bool autoConsume = true}) { + assert(autoConsume == true, 'On iOS, we should always auto consume'); + return buyNonConsumable(purchaseParam: purchaseParam); + } + + @override + Future completePurchase(PurchaseDetails purchase) { + assert( + purchase is AppStorePurchaseDetails, + 'On iOS, the `purchase` should always be of type `AppStorePurchaseDetails`.', + ); + + return _skPaymentQueueWrapper.finishTransaction( + (purchase as AppStorePurchaseDetails).skPaymentTransaction, + ); + } + + @override + Future restorePurchases({String? applicationUserName}) async { + return _observer + .restoreTransactions( + queue: _skPaymentQueueWrapper, + applicationUserName: applicationUserName) + .whenComplete(() => _observer.cleanUpRestoredTransactions()); + } + + /// Query the product detail list. + /// + /// This method only returns [ProductDetailsResponse]. + /// To get detailed Store Kit product list, use [SkProductResponseWrapper.startProductRequest] + /// to get the [SKProductResponseWrapper]. + @override + Future queryProductDetails( + Set identifiers) async { + final SKRequestMaker requestMaker = SKRequestMaker(); + SkProductResponseWrapper response; + PlatformException? exception; + try { + response = await requestMaker.startProductRequest(identifiers.toList()); + } on PlatformException catch (e) { + exception = e; + response = SkProductResponseWrapper( + products: [], invalidProductIdentifiers: identifiers.toList()); + } + List productDetails = []; + if (response.products != null) { + productDetails = response.products + .map((SKProductWrapper productWrapper) => + AppStoreProductDetails.fromSKProduct(productWrapper)) + .toList(); + } + List invalidIdentifiers = response.invalidProductIdentifiers; + if (productDetails.isEmpty) { + invalidIdentifiers = identifiers.toList(); + } + ProductDetailsResponse productDetailsResponse = ProductDetailsResponse( + productDetails: productDetails, + notFoundIDs: invalidIdentifiers, + error: exception == null + ? null + : IAPError( + source: kIAPSource, + code: exception.code, + message: exception.message ?? '', + details: exception.details), + ); + return productDetailsResponse; + } +} + +class _TransactionObserver implements SKTransactionObserverWrapper { + final StreamController> purchaseUpdatedController; + + Completer? _restoreCompleter; + late String _receiptData; + + _TransactionObserver(this.purchaseUpdatedController); + + Future restoreTransactions({ + required SKPaymentQueueWrapper queue, + String? applicationUserName, + }) { + _restoreCompleter = Completer(); + queue.restoreTransactions(applicationUserName: applicationUserName); + return _restoreCompleter!.future; + } + + void cleanUpRestoredTransactions() { + _restoreCompleter = null; + } + + void updatedTransactions( + {required List transactions}) async { + String receiptData = await getReceiptData(); + List purchases = transactions + .map((SKPaymentTransactionWrapper transaction) => + AppStorePurchaseDetails.fromSKTransaction(transaction, receiptData)) + .toList(); + + purchaseUpdatedController.add(purchases); + } + + void removedTransactions( + {required List transactions}) {} + + /// Triggered when there is an error while restoring transactions. + void restoreCompletedTransactionsFailed({required SKError error}) { + _restoreCompleter!.completeError(error); + } + + void paymentQueueRestoreCompletedTransactionsFinished() { + _restoreCompleter!.complete(); + } + + bool shouldAddStorePayment( + {required SKPaymentWrapper payment, required SKProductWrapper product}) { + // In this unified API, we always return true to keep it consistent with the behavior on Google Play. + return true; + } + + Future getReceiptData() async { + try { + _receiptData = await SKReceiptManager.retrieveReceiptData(); + } catch (e) { + _receiptData = ''; + } + return _receiptData; + } +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform_addition.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform_addition.dart new file mode 100644 index 000000000000..359e51713521 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/in_app_purchase_ios_platform_addition.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:in_app_purchase_ios/in_app_purchase_ios.dart'; +import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; + +import '../store_kit_wrappers.dart'; + +/// Contains InApp Purchase features that are only available on iOS. +class InAppPurchaseIosPlatformAddition extends InAppPurchasePlatformAddition { + /// Present Code Redemption Sheet. + /// + /// Available on devices running iOS 14 and iPadOS 14 and later. + Future presentCodeRedemptionSheet() { + return SKPaymentQueueWrapper().presentCodeRedemptionSheet(); + } + + /// Retry loading purchase data after an initial failure. + /// + /// If no results, a `null` value is returned. + Future refreshPurchaseVerificationData() async { + await SKRequestMaker().startRefreshReceiptRequest(); + try { + String receipt = await SKReceiptManager.retrieveReceiptData(); + return PurchaseVerificationData( + localVerificationData: receipt, + serverVerificationData: receipt, + source: kIAPSource); + } catch (e) { + print( + 'Something is wrong while fetching the receipt, this normally happens when the app is ' + 'running on a simulator: $e'); + return null; + } + } + + /// Sets an implementation of the [SKPaymentQueueDelegateWrapper]. + /// + /// The [SKPaymentQueueDelegateWrapper] can be used to inform iOS how to + /// finish transactions when the storefront changes or if the price consent + /// sheet should be displayed when the price of a subscription has changed. If + /// no delegate is registered iOS will fallback to it's default configuration. + /// See the documentation on StoreKite's [`-[SKPaymentQueue delegate:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/3182429-delegate?language=objc). + /// + /// When set to `null` the payment queue delegate will be removed and the + /// default behaviour will apply (see [documentation](https://developer.apple.com/documentation/storekit/skpaymentqueue/3182429-delegate?language=objc)). + Future setDelegate(SKPaymentQueueDelegateWrapper? delegate) => + SKPaymentQueueWrapper().setDelegate(delegate); + + /// Shows the price consent sheet if the user has not yet responded to a + /// subscription price change. + /// + /// Use this function when you have registered a [SKPaymentQueueDelegateWrapper] + /// (using the [setDelegate] method) and returned `false` when the + /// `SKPaymentQueueDelegateWrapper.shouldShowPriceConsent()` method was called. + /// + /// See documentation of StoreKit's [`-[SKPaymentQueue showPriceConsentIfNeeded]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/3521327-showpriceconsentifneeded?language=objc). + Future showPriceConsentIfNeeded() => + SKPaymentQueueWrapper().showPriceConsentIfNeeded(); +} diff --git a/packages/in_app_purchase/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..70178260febf --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.dart @@ -0,0 +1,110 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package: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 +// See https://github.com/google/json_serializable.dart/issues/778 +@JsonSerializable() +class _SerializedEnums { + late SKPaymentTransactionStateWrapper response; + late SKSubscriptionPeriodUnit unit; + late SKProductDiscountPaymentMode discountPaymentMode; +} diff --git a/packages/in_app_purchase/in_app_purchase_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..ce0f56ba4d34 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/enum_converters.g.dart @@ -0,0 +1,71 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'enum_converters.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_SerializedEnums _$SerializedEnumsFromJson(Map json) => _SerializedEnums() + ..response = + _$enumDecode(_$SKPaymentTransactionStateWrapperEnumMap, json['response']) + ..unit = _$enumDecode(_$SKSubscriptionPeriodUnitEnumMap, json['unit']) + ..discountPaymentMode = _$enumDecode( + _$SKProductDiscountPaymentModeEnumMap, json['discountPaymentMode']); + +Map _$SerializedEnumsToJson(_SerializedEnums instance) => + { + 'response': _$SKPaymentTransactionStateWrapperEnumMap[instance.response], + 'unit': _$SKSubscriptionPeriodUnitEnumMap[instance.unit], + 'discountPaymentMode': + _$SKProductDiscountPaymentModeEnumMap[instance.discountPaymentMode], + }; + +K _$enumDecode( + Map enumValues, + Object? source, { + K? unknownValue, +}) { + if (source == null) { + throw ArgumentError( + 'A value must be provided. Supported values: ' + '${enumValues.values.join(', ')}', + ); + } + + return enumValues.entries.singleWhere( + (e) => e.value == source, + orElse: () { + if (unknownValue == null) { + throw ArgumentError( + '`$source` is not one of the supported values: ' + '${enumValues.values.join(', ')}', + ); + } + return MapEntry(unknownValue, enumValues.values.first); + }, + ).key; +} + +const _$SKPaymentTransactionStateWrapperEnumMap = { + SKPaymentTransactionStateWrapper.purchasing: 0, + SKPaymentTransactionStateWrapper.purchased: 1, + SKPaymentTransactionStateWrapper.failed: 2, + SKPaymentTransactionStateWrapper.restored: 3, + SKPaymentTransactionStateWrapper.deferred: 4, + SKPaymentTransactionStateWrapper.unspecified: -1, +}; + +const _$SKSubscriptionPeriodUnitEnumMap = { + SKSubscriptionPeriodUnit.day: 0, + SKSubscriptionPeriodUnit.week: 1, + SKSubscriptionPeriodUnit.month: 2, + SKSubscriptionPeriodUnit.year: 3, +}; + +const _$SKProductDiscountPaymentModeEnumMap = { + SKProductDiscountPaymentMode.payAsYouGo: 0, + SKProductDiscountPaymentMode.payUpFront: 1, + SKProductDiscountPaymentMode.freeTrail: 2, + SKProductDiscountPaymentMode.unspecified: -1, +}; diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart new file mode 100644 index 000000000000..2759a296389b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_delegate_wrapper.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:in_app_purchase_ios/store_kit_wrappers.dart'; + +/// A wrapper around +/// [`SKPaymentQueueDelegate`](https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate?language=objc). +/// +/// The payment queue delegate can be implementated to provide information +/// needed to complete transactions. +/// +/// The [SKPaymentQueueDelegateWrapper] is only available on iOS 13 and higher. +/// Using the delegate on older iOS version will be ignored. +abstract class SKPaymentQueueDelegateWrapper { + /// Called by the system to check whether the transaction should continue if + /// the device's App Store storefront has changed during a transaction. + /// + /// - Return `true` if the transaction should continue within the updated + /// storefront (default behaviour). + /// - Return `false` if the transaction should be cancelled. In this case the + /// transaction will fail with the error [SKErrorStoreProductNotAvailable](https://developer.apple.com/documentation/storekit/skerrorcode/skerrorstoreproductnotavailable?language=objc). + /// + /// See the documentation in StoreKit's [`[-SKPaymentQueueDelegate shouldContinueTransaction]`](https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate/3242935-paymentqueue?language=objc). + bool shouldContinueTransaction( + SKPaymentTransactionWrapper transaction, + SKStorefrontWrapper storefront, + ) => + true; + + /// Called by the system to check whether to immediately show the price + /// consent form. + /// + /// The default return value is `true`. This will inform the system to display + /// the price consent sheet when the subscription price has been changed in + /// App Store Connect and the subscriber has not yet taken action. See the + /// documentation in StoreKit's [`[-SKPaymentQueueDelegate shouldShowPriceConsent:]`](https://developer.apple.com/documentation/storekit/skpaymentqueuedelegate/3521328-paymentqueueshouldshowpriceconse?language=objc). + bool shouldShowPriceConsent() => true; +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart new file mode 100644 index 000000000000..079e75078037 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart @@ -0,0 +1,476 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:ui' show hashValues; + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:meta/meta.dart'; + +import '../channel.dart'; +import '../in_app_purchase_ios_platform.dart'; +import 'sk_payment_queue_delegate_wrapper.dart'; +import 'sk_payment_transaction_wrappers.dart'; +import 'sk_product_wrapper.dart'; + +part 'sk_payment_queue_wrapper.g.dart'; + +/// A wrapper around +/// [`SKPaymentQueue`](https://developer.apple.com/documentation/storekit/skpaymentqueue?language=objc). +/// +/// The payment queue contains payment related operations. It communicates with +/// the App Store and presents a user interface for the user to process and +/// authorize payments. +/// +/// Full information on using `SKPaymentQueue` and processing purchases is +/// available at the [In-App Purchase Programming +/// Guide](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Introduction.html#//apple_ref/doc/uid/TP40008267). +class SKPaymentQueueWrapper { + /// Returns the default payment queue. + /// + /// We do not support instantiating a custom payment queue, hence the + /// singleton. However, you can override the observer. + factory SKPaymentQueueWrapper() { + return _singleton; + } + + SKPaymentQueueWrapper._(); + + static final SKPaymentQueueWrapper _singleton = SKPaymentQueueWrapper._(); + + SKPaymentQueueDelegateWrapper? _paymentQueueDelegate; + SKTransactionObserverWrapper? _observer; + + /// Calls [`-[SKPaymentQueue transactions]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506026-transactions?language=objc) + Future> transactions() async { + return _getTransactionList((await channel + .invokeListMethod('-[SKPaymentQueue transactions]'))!); + } + + /// Calls [`-[SKPaymentQueue canMakePayments:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506139-canmakepayments?language=objc). + static Future canMakePayments() async => + (await channel + .invokeMethod('-[SKPaymentQueue canMakePayments:]')) ?? + false; + + /// Sets an observer to listen to all incoming transaction events. + /// + /// This should be called and set as soon as the app launches in order to + /// avoid missing any purchase updates from the App Store. See the + /// documentation on StoreKit's [`-[SKPaymentQueue + /// addTransactionObserver:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506042-addtransactionobserver?language=objc). + void setTransactionObserver(SKTransactionObserverWrapper observer) { + _observer = observer; + channel.setMethodCallHandler(handleObserverCallbacks); + } + + /// Instructs the iOS implementation to register a transaction observer and + /// start listening to it. + /// + /// Call this method when the first listener is subscribed to the + /// [InAppPurchaseIosPlatform.purchaseStream]. + Future startObservingTransactionQueue() => channel + .invokeMethod('-[SKPaymentQueue startObservingTransactionQueue]'); + + /// Instructs the iOS implementation to remove the transaction observer and + /// stop listening to it. + /// + /// Call this when there are no longer any listeners subscribed to the + /// [InAppPurchaseIosPlatform.purchaseStream]. + Future stopObservingTransactionQueue() => channel + .invokeMethod('-[SKPaymentQueue stopObservingTransactionQueue]'); + + /// Sets an implementation of the [SKPaymentQueueDelegateWrapper]. + /// + /// The [SKPaymentQueueDelegateWrapper] can be used to inform iOS how to + /// finish transactions when the storefront changes or if the price consent + /// sheet should be displayed when the price of a subscription has changed. If + /// no delegate is registered iOS will fallback to it's default configuration. + /// See the documentation on StoreKite's [`-[SKPaymentQueue delegate:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/3182429-delegate?language=objc). + /// + /// When set to `null` the payment queue delegate will be removed and the + /// default behaviour will apply (see [documentation](https://developer.apple.com/documentation/storekit/skpaymentqueue/3182429-delegate?language=objc)). + Future setDelegate(SKPaymentQueueDelegateWrapper? delegate) async { + if (delegate == null) { + await channel.invokeMethod('-[SKPaymentQueue removeDelegate]'); + paymentQueueDelegateChannel.setMethodCallHandler(null); + } else { + await channel.invokeMethod('-[SKPaymentQueue registerDelegate]'); + paymentQueueDelegateChannel + .setMethodCallHandler(handlePaymentQueueDelegateCallbacks); + } + + _paymentQueueDelegate = delegate; + } + + /// Posts a payment to the queue. + /// + /// This sends a purchase request to the App Store for confirmation. + /// Transaction updates will be delivered to the set + /// [SkTransactionObserverWrapper]. + /// + /// A couple preconditions need to be met before calling this method. + /// + /// - At least one [SKTransactionObserverWrapper] should have been added to + /// the payment queue using [addTransactionObserver]. + /// - The [payment.productIdentifier] needs to have been previously fetched + /// using [SKRequestMaker.startProductRequest] so that a valid `SKProduct` + /// has been cached in the platform side already. Because of this + /// [payment.productIdentifier] cannot be hardcoded. + /// + /// This method calls StoreKit's [`-[SKPaymentQueue addPayment:]`] + /// (https://developer.apple.com/documentation/storekit/skpaymentqueue/1506036-addpayment?preferredLanguage=occ). + /// + /// Also see [sandbox + /// testing](https://developer.apple.com/apple-pay/sandbox-testing/). + Future addPayment(SKPaymentWrapper payment) async { + assert(_observer != null, + '[in_app_purchase]: Trying to add a payment without an observer. One must be set using `SkPaymentQueueWrapper.setTransactionObserver` before the app launches.'); + final Map requestMap = payment.toMap(); + await channel.invokeMethod( + '-[InAppPurchasePlugin addPayment:result:]', + requestMap, + ); + } + + /// Finishes a transaction and removes it from the queue. + /// + /// This method should be called after the given [transaction] has been + /// succesfully processed and its content has been delivered to the user. + /// Transaction status updates are propagated to [SkTransactionObserver]. + /// + /// This will throw a Platform exception if [transaction.transactionState] is + /// [SKPaymentTransactionStateWrapper.purchasing]. + /// + /// This method calls StoreKit's [`-[SKPaymentQueue + /// finishTransaction:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506003-finishtransaction?language=objc). + Future finishTransaction( + SKPaymentTransactionWrapper transaction) async { + Map requestMap = transaction.toFinishMap(); + await channel.invokeMethod( + '-[InAppPurchasePlugin finishTransaction:result:]', + requestMap, + ); + } + + /// Restore previously purchased transactions. + /// + /// Use this to load previously purchased content on a new device. + /// + /// This call triggers purchase updates on the set + /// [SKTransactionObserverWrapper] for previously made transactions. This will + /// invoke [SKTransactionObserverWrapper.restoreCompletedTransactions], + /// [SKTransactionObserverWrapper.paymentQueueRestoreCompletedTransactionsFinished], + /// and [SKTransactionObserverWrapper.updatedTransaction]. These restored + /// transactions need to be marked complete with [finishTransaction] once the + /// content is delivered, like any other transaction. + /// + /// The `applicationUserName` should match the original + /// [SKPaymentWrapper.applicationUsername] used in [addPayment]. + /// If no `applicationUserName` was used, `applicationUserName` should be null. + /// + /// This method either triggers [`-[SKPayment + /// restoreCompletedTransactions]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506123-restorecompletedtransactions?language=objc) + /// or [`-[SKPayment restoreCompletedTransactionsWithApplicationUsername:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1505992-restorecompletedtransactionswith?language=objc) + /// depending on whether the `applicationUserName` is set. + Future restoreTransactions({String? applicationUserName}) async { + await channel.invokeMethod( + '-[InAppPurchasePlugin restoreTransactions:result:]', + applicationUserName); + } + + /// Present Code Redemption Sheet + /// + /// Use this to allow Users to enter and redeem Codes + /// + /// This method triggers [`-[SKPayment + /// presentCodeRedemptionSheet]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/3566726-presentcoderedemptionsheet?language=objc) + Future presentCodeRedemptionSheet() async { + await channel.invokeMethod( + '-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]'); + } + + /// Shows the price consent sheet if the user has not yet responded to a + /// subscription price change. + /// + /// Use this function when you have registered a [SKPaymentQueueDelegateWrapper] + /// (using the [setDelegate] method) and returned `false` when the + /// `SKPaymentQueueDelegateWrapper.shouldShowPriceConsent()` method was called. + /// + /// See documentation of StoreKit's [`-[SKPaymentQueue showPriceConsentIfNeeded]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/3521327-showpriceconsentifneeded?language=objc). + Future showPriceConsentIfNeeded() async { + await channel + .invokeMethod('-[SKPaymentQueue showPriceConsentIfNeeded]'); + } + + /// Triage a method channel call from the platform and triggers the correct observer method. + /// + /// This method is public for testing purposes only and should not be used + /// outside this class. + @visibleForTesting + Future handleObserverCallbacks(MethodCall call) async { + assert(_observer != null, + '[in_app_purchase]: (Fatal)The observer has not been set but we received a purchase transaction notification. Please ensure the observer has been set using `setTransactionObserver`. Make sure the observer is added right at the App Launch.'); + final SKTransactionObserverWrapper observer = _observer!; + switch (call.method) { + case 'updatedTransactions': + { + final List transactions = + _getTransactionList(call.arguments); + return Future(() { + observer.updatedTransactions(transactions: transactions); + }); + } + case 'removedTransactions': + { + final List transactions = + _getTransactionList(call.arguments); + return Future(() { + observer.removedTransactions(transactions: transactions); + }); + } + case 'restoreCompletedTransactionsFailed': + { + SKError error = + SKError.fromJson(Map.from(call.arguments)); + return Future(() { + observer.restoreCompletedTransactionsFailed(error: error); + }); + } + case 'paymentQueueRestoreCompletedTransactionsFinished': + { + return Future(() { + observer.paymentQueueRestoreCompletedTransactionsFinished(); + }); + } + case 'shouldAddStorePayment': + { + SKPaymentWrapper payment = + SKPaymentWrapper.fromJson(call.arguments['payment']); + SKProductWrapper product = + SKProductWrapper.fromJson(call.arguments['product']); + return Future(() { + if (observer.shouldAddStorePayment( + payment: payment, product: product) == + true) { + SKPaymentQueueWrapper().addPayment(payment); + } + }); + } + default: + break; + } + throw PlatformException( + code: 'no_such_callback', + message: 'Did not recognize the observer callback ${call.method}.'); + } + + // Get transaction wrapper object list from arguments. + List _getTransactionList( + List transactionsData) { + return transactionsData.map((dynamic map) { + return SKPaymentTransactionWrapper.fromJson( + Map.castFrom(map)); + }).toList(); + } + + /// Triage a method channel call from the platform and triggers the correct + /// payment queue delegate method. + /// + /// This method is public for testing purposes only and should not be used + /// outside this class. + @visibleForTesting + Future handlePaymentQueueDelegateCallbacks(MethodCall call) async { + assert(_paymentQueueDelegate != null, + '[in_app_purchase]: (Fatal)The payment queue delegate has not been set but we received a payment queue notification. Please ensure the payment queue has been set using `setDelegate`.'); + + final SKPaymentQueueDelegateWrapper delegate = _paymentQueueDelegate!; + switch (call.method) { + case 'shouldContinueTransaction': + final SKPaymentTransactionWrapper transaction = + SKPaymentTransactionWrapper.fromJson(call.arguments['transaction']); + final SKStorefrontWrapper storefront = + SKStorefrontWrapper.fromJson(call.arguments['storefront']); + return delegate.shouldContinueTransaction(transaction, storefront); + case 'shouldShowPriceConsent': + return delegate.shouldShowPriceConsent(); + default: + break; + } + throw PlatformException( + code: 'no_such_callback', + message: + 'Did not recognize the payment queue delegate callback ${call.method}.'); + } +} + +/// Dart wrapper around StoreKit's +/// [NSError](https://developer.apple.com/documentation/foundation/nserror?language=objc). +@immutable +@JsonSerializable() +class SKError { + /// Creates a new [SKError] object with the provided information. + const SKError( + {required this.code, required this.domain, required this.userInfo}); + + /// Constructs an instance of this from a key-value map of data. + /// + /// The map needs to have named string keys with values matching the names and + /// types of all of the members on this class. The `map` parameter must not be + /// null. + factory SKError.fromJson(Map map) { + return _$SKErrorFromJson(map); + } + + /// Error [code](https://developer.apple.com/documentation/foundation/1448136-nserror_codes) + /// as defined in the Cocoa Framework. + @JsonKey(defaultValue: 0) + final int code; + + /// Error + /// [domain](https://developer.apple.com/documentation/foundation/nscocoaerrordomain?language=objc) + /// as defined in the Cocoa Framework. + @JsonKey(defaultValue: '') + final String domain; + + /// A map that contains more detailed information about the error. + /// + /// Any key of the map must be a valid [NSErrorUserInfoKey](https://developer.apple.com/documentation/foundation/nserroruserinfokey?language=objc). + @JsonKey(defaultValue: {}) + final Map userInfo; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + final SKError typedOther = other as SKError; + return typedOther.code == code && + typedOther.domain == domain && + DeepCollectionEquality.unordered() + .equals(typedOther.userInfo, userInfo); + } + + @override + int get hashCode => hashValues( + code, + domain, + userInfo, + ); +} + +/// Dart wrapper around StoreKit's +/// [SKPayment](https://developer.apple.com/documentation/storekit/skpayment?language=objc). +/// +/// Used as the parameter to initiate a payment. In general, a developer should +/// not need to create the payment object explicitly; instead, use +/// [SKPaymentQueueWrapper.addPayment] directly with a product identifier to +/// initiate a payment. +@immutable +@JsonSerializable() +class SKPaymentWrapper { + /// Creates a new [SKPaymentWrapper] with the provided information. + const SKPaymentWrapper( + {required this.productIdentifier, + this.applicationUsername, + this.requestData, + this.quantity = 1, + this.simulatesAskToBuyInSandbox = false}); + + /// Constructs an instance of this from a key value map of data. + /// + /// The map needs to have named string keys with values matching the names and + /// types of all of the members on this class. The `map` parameter must not be + /// null. + factory SKPaymentWrapper.fromJson(Map map) { + assert(map != null); + return _$SKPaymentWrapperFromJson(map); + } + + /// Creates a Map object describes the payment object. + Map toMap() { + return { + 'productIdentifier': productIdentifier, + 'applicationUsername': applicationUsername, + 'requestData': requestData, + 'quantity': quantity, + 'simulatesAskToBuyInSandbox': simulatesAskToBuyInSandbox + }; + } + + /// The id for the product that the payment is for. + @JsonKey(defaultValue: '') + final String productIdentifier; + + /// An opaque id for the user's account. + /// + /// Used to help the store detect irregular activity. See + /// [applicationUsername](https://developer.apple.com/documentation/storekit/skpayment/1506116-applicationusername?language=objc) + /// for more details. For example, you can use a one-way hash of the user’s + /// account name on your server. Don’t use the Apple ID for your developer + /// account, the user’s Apple ID, or the user’s plaintext account name on + /// your server. + final String? applicationUsername; + + /// Reserved for future use. + /// + /// The value must be null before sending the payment. If the value is not + /// null, the payment will be rejected. + /// + // The iOS Platform provided this property but it is reserved for future use. + // We also provide this property to match the iOS platform. Converted to + // String from NSData from ios platform using UTF8Encoding. The / default is + // null. + final String? requestData; + + /// The amount of the product this payment is for. + /// + /// The default is 1. The minimum is 1. The maximum is 10. + /// + /// If the object is invalid, the value could be 0. + @JsonKey(defaultValue: 0) + final int quantity; + + /// Produces an "ask to buy" flow in the sandbox. + /// + /// Setting it to `true` will cause a transaction to be in the state [SKPaymentTransactionStateWrapper.deferred], + /// which produce an "ask to buy" prompt that interrupts the the payment flow. + /// + /// Default is `false`. + /// + /// See https://developer.apple.com/in-app-purchase/ for a guide on Sandbox + /// testing. + @JsonKey(defaultValue: false) + final bool simulatesAskToBuyInSandbox; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + final SKPaymentWrapper typedOther = other as SKPaymentWrapper; + return typedOther.productIdentifier == productIdentifier && + typedOther.applicationUsername == applicationUsername && + typedOther.quantity == quantity && + typedOther.simulatesAskToBuyInSandbox == simulatesAskToBuyInSandbox && + typedOther.requestData == requestData; + } + + @override + int get hashCode => hashValues(productIdentifier, applicationUsername, + quantity, simulatesAskToBuyInSandbox, requestData); + + @override + String toString() => _$SKPaymentWrapperToJson(this).toString(); +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.g.dart new file mode 100644 index 000000000000..4d2b5e4b3d4b --- /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,40 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sk_payment_queue_wrapper.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SKError _$SKErrorFromJson(Map json) => SKError( + code: json['code'] as int? ?? 0, + domain: json['domain'] as String? ?? '', + userInfo: (json['userInfo'] as Map?)?.map( + (k, e) => MapEntry(k as String, e), + ) ?? + {}, + ); + +Map _$SKErrorToJson(SKError instance) => { + 'code': instance.code, + 'domain': instance.domain, + 'userInfo': instance.userInfo, + }; + +SKPaymentWrapper _$SKPaymentWrapperFromJson(Map json) => SKPaymentWrapper( + productIdentifier: json['productIdentifier'] as String? ?? '', + applicationUsername: json['applicationUsername'] as String?, + requestData: json['requestData'] as String?, + quantity: json['quantity'] as int? ?? 0, + simulatesAskToBuyInSandbox: + json['simulatesAskToBuyInSandbox'] as bool? ?? false, + ); + +Map _$SKPaymentWrapperToJson(SKPaymentWrapper instance) => + { + 'productIdentifier': instance.productIdentifier, + 'applicationUsername': instance.applicationUsername, + 'requestData': instance.requestData, + 'quantity': instance.quantity, + 'simulatesAskToBuyInSandbox': instance.simulatesAskToBuyInSandbox, + }; 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..fd10d9ad977b --- /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,36 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sk_payment_transaction_wrappers.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SKPaymentTransactionWrapper _$SKPaymentTransactionWrapperFromJson(Map json) => + SKPaymentTransactionWrapper( + payment: SKPaymentWrapper.fromJson( + Map.from(json['payment'] as Map)), + transactionState: const SKTransactionStatusConverter() + .fromJson(json['transactionState'] as int?), + originalTransaction: json['originalTransaction'] == null + ? null + : SKPaymentTransactionWrapper.fromJson( + Map.from(json['originalTransaction'] as Map)), + transactionTimeStamp: (json['transactionTimeStamp'] as num?)?.toDouble(), + transactionIdentifier: json['transactionIdentifier'] as String?, + error: json['error'] == null + ? null + : SKError.fromJson(Map.from(json['error'] as Map)), + ); + +Map _$SKPaymentTransactionWrapperToJson( + SKPaymentTransactionWrapper instance) => + { + 'transactionState': const SKTransactionStatusConverter() + .toJson(instance.transactionState), + 'payment': instance.payment, + 'originalTransaction': instance.originalTransaction, + 'transactionTimeStamp': instance.transactionTimeStamp, + 'transactionIdentifier': instance.transactionIdentifier, + 'error': instance.error, + }; diff --git a/packages/in_app_purchase/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 76% 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..1b681f24f8db 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,24 +334,36 @@ class SKProductWrapper { // https://github.com/flutter/flutter/issues/26610 @JsonSerializable() class SKPriceLocaleWrapper { - SKPriceLocaleWrapper( - {@required this.currencySymbol, @required this.currencyCode}); + /// Creates a new price locale for `currencySymbol` and `currencyCode`. + SKPriceLocaleWrapper({ + required this.currencySymbol, + required this.currencyCode, + required this.countryCode, + }); /// 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: '', countryCode: ''); + } 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; + ///The country code for the locale, e.g. US for US locale. + @JsonKey(defaultValue: '') + final String countryCode; + @override bool operator ==(Object other) { if (identical(other, this)) { @@ -330,7 +372,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..485bf1932efa --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_product_wrapper.g.dart @@ -0,0 +1,120 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sk_product_wrapper.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SkProductResponseWrapper _$SkProductResponseWrapperFromJson(Map json) => + SkProductResponseWrapper( + products: (json['products'] as List?) + ?.map((e) => SKProductWrapper.fromJson( + Map.from(e as Map))) + .toList() ?? + [], + invalidProductIdentifiers: + (json['invalidProductIdentifiers'] as List?) + ?.map((e) => e as String) + .toList() ?? + [], + ); + +Map _$SkProductResponseWrapperToJson( + SkProductResponseWrapper instance) => + { + 'products': instance.products, + 'invalidProductIdentifiers': instance.invalidProductIdentifiers, + }; + +SKProductSubscriptionPeriodWrapper _$SKProductSubscriptionPeriodWrapperFromJson( + Map json) => + 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) => + 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) => 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) => + SKPriceLocaleWrapper( + currencySymbol: json['currencySymbol'] as String? ?? '', + currencyCode: json['currencyCode'] as String? ?? '', + countryCode: json['countryCode'] as String? ?? '', + ); + +Map _$SKPriceLocaleWrapperToJson( + SKPriceLocaleWrapper instance) => + { + 'currencySymbol': instance.currencySymbol, + 'currencyCode': instance.currencyCode, + 'countryCode': instance.countryCode, + }; diff --git a/packages/in_app_purchase/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/store_kit_wrappers/sk_storefront_wrapper.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart new file mode 100644 index 000000000000..934fdea355e3 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.dart @@ -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 'dart:ui' show hashValues; + +import 'package:json_annotation/json_annotation.dart'; + +part 'sk_storefront_wrapper.g.dart'; + +/// Contains the location and unique identifier of an Apple App Store storefront. +/// +/// Dart wrapper around StoreKit's +/// [SKStorefront](https://developer.apple.com/documentation/storekit/skstorefront?language=objc). +@JsonSerializable() +class SKStorefrontWrapper { + /// Creates a new [SKStorefrontWrapper] with the provided information. + SKStorefrontWrapper({ + required this.countryCode, + required this.identifier, + }); + + /// Constructs an instance of the [SKStorefrontWrapper] 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 SKStorefrontWrapper.fromJson(Map map) { + return _$SKStorefrontWrapperFromJson(map); + } + + /// The three-letter code representing the country or region associated with + /// the App Store storefront. + final String countryCode; + + /// A value defined by Apple that uniquely identifies an App Store storefront. + final String identifier; + + @override + bool operator ==(Object other) { + if (identical(other, this)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + final SKStorefrontWrapper typedOther = other as SKStorefrontWrapper; + return typedOther.countryCode == countryCode && + typedOther.identifier == identifier; + } + + @override + int get hashCode => hashValues( + this.countryCode, + this.identifier, + ); + + @override + String toString() => _$SKStorefrontWrapperToJson(this).toString(); + + /// Converts the instance to a key value map which can be used to serialize + /// to JSON format. + Map toMap() => _$SKStorefrontWrapperToJson(this); +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.g.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.g.dart new file mode 100644 index 000000000000..b2d5d3a06d1d --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/store_kit_wrappers/sk_storefront_wrapper.g.dart @@ -0,0 +1,20 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sk_storefront_wrapper.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SKStorefrontWrapper _$SKStorefrontWrapperFromJson(Map json) => + SKStorefrontWrapper( + countryCode: json['countryCode'] as String, + identifier: json['identifier'] as String, + ); + +Map _$SKStorefrontWrapperToJson( + SKStorefrontWrapper instance) => + { + 'countryCode': instance.countryCode, + 'identifier': instance.identifier, + }; diff --git a/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_product_details.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_product_details.dart new file mode 100644 index 000000000000..ff1153e27e47 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/src/types/app_store_product_details.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: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, + required String currencySymbol, + }) : super( + id: id, + title: title, + description: description, + price: price, + rawPrice: rawPrice, + currencyCode: currencyCode, + currencySymbol: currencySymbol, + ); + + /// Points back to the [SKProductWrapper] object that was used to generate + /// this [AppStoreProductDetails] object. + final SKProductWrapper skProduct; + + /// Generate a [AppStoreProductDetails] object based on an iOS [SKProductWrapper] object. + factory AppStoreProductDetails.fromSKProduct(SKProductWrapper product) { + return AppStoreProductDetails( + id: product.productIdentifier, + title: product.localizedTitle, + description: product.localizedDescription, + price: product.priceLocale.currencySymbol + product.price, + rawPrice: double.parse(product.price), + currencyCode: product.priceLocale.currencyCode, + currencySymbol: product.priceLocale.currencySymbol.isNotEmpty + ? product.priceLocale.currencySymbol + : 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/in_app_purchase_ios/lib/store_kit_wrappers.dart b/packages/in_app_purchase/in_app_purchase_ios/lib/store_kit_wrappers.dart new file mode 100644 index 000000000000..09eb1acb8420 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/lib/store_kit_wrappers.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/store_kit_wrappers/sk_payment_queue_delegate_wrapper.dart'; +export 'src/store_kit_wrappers/sk_payment_queue_wrapper.dart'; +export 'src/store_kit_wrappers/sk_payment_transaction_wrappers.dart'; +export 'src/store_kit_wrappers/sk_product_wrapper.dart'; +export 'src/store_kit_wrappers/sk_receipt_manager.dart'; +export 'src/store_kit_wrappers/sk_request_maker.dart'; +export 'src/store_kit_wrappers/sk_storefront_wrapper.dart'; 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..fdd769e90674 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/pubspec.yaml @@ -0,0 +1,31 @@ +name: in_app_purchase_ios +description: An implementation for the iOS platform of the Flutter `in_app_purchase` plugin. This uses the iOS StoreKit Framework. +repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_ios +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22 +version: 0.1.3+5 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" + +flutter: + plugin: + implements: in_app_purchase + platforms: + ios: + pluginClass: InAppPurchasePlugin + +dependencies: + collection: ^1.15.0 + flutter: + sdk: flutter + in_app_purchase_platform_interface: ^1.1.0 + json_annotation: ^4.0.1 + meta: ^1.3.0 + +dev_dependencies: + build_runner: ^2.0.0 + flutter_test: + sdk: flutter + json_serializable: ^5.0.2 + test: ^1.16.0 diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart b/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart new file mode 100644 index 000000000000..e7dbd1a49ae2 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/test/fakes/fake_ios_platform.dart @@ -0,0 +1,192 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; +import 'package:in_app_purchase_ios/src/channel.dart'; +import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; + +import '../store_kit_wrappers/sk_test_stub_objects.dart'; + +class FakeIOSPlatform { + FakeIOSPlatform() { + channel.setMockMethodCallHandler(onMethodCall); + } + + // pre-configured store informations + String? receiptData; + late Set validProductIDs; + late Map validProducts; + late List transactions; + late List finishedTransactions; + late bool testRestoredTransactionsNull; + late bool testTransactionFail; + PlatformException? queryProductException; + PlatformException? restoreException; + SKError? testRestoredError; + bool queueIsActive = false; + + void reset() { + transactions = []; + receiptData = 'dummy base64data'; + validProductIDs = ['123', '456'].toSet(); + validProducts = Map(); + for (String validID in validProductIDs) { + Map productWrapperMap = + buildProductMap(dummyProductWrapper); + productWrapperMap['productIdentifier'] = validID; + if (validID == '456') { + productWrapperMap['priceLocale'] = buildLocaleMap(noSymbolLocale); + } + validProducts[validID] = SKProductWrapper.fromJson(productWrapperMap); + } + + SKPaymentTransactionWrapper tran1 = SKPaymentTransactionWrapper( + transactionIdentifier: '123', + payment: dummyPayment, + originalTransaction: dummyTransaction, + transactionTimeStamp: 123123123.022, + transactionState: SKPaymentTransactionStateWrapper.restored, + error: null, + ); + SKPaymentTransactionWrapper tran2 = SKPaymentTransactionWrapper( + transactionIdentifier: '1234', + payment: dummyPayment, + originalTransaction: dummyTransaction, + transactionTimeStamp: 123123123.022, + transactionState: SKPaymentTransactionStateWrapper.restored, + error: null, + ); + + transactions.addAll([tran1, tran2]); + finishedTransactions = []; + testRestoredTransactionsNull = false; + testTransactionFail = false; + queryProductException = null; + restoreException = null; + testRestoredError = null; + queueIsActive = false; + } + + SKPaymentTransactionWrapper createPendingTransaction(String id) { + return SKPaymentTransactionWrapper( + transactionIdentifier: '', + payment: SKPaymentWrapper(productIdentifier: id), + transactionState: SKPaymentTransactionStateWrapper.purchasing, + transactionTimeStamp: 123123.121, + error: null, + originalTransaction: null); + } + + SKPaymentTransactionWrapper createPurchasedTransaction( + String productId, String transactionId) { + return SKPaymentTransactionWrapper( + payment: SKPaymentWrapper(productIdentifier: productId), + transactionState: SKPaymentTransactionStateWrapper.purchased, + transactionTimeStamp: 123123.121, + transactionIdentifier: transactionId, + error: null, + originalTransaction: null); + } + + SKPaymentTransactionWrapper createFailedTransaction(String productId) { + return SKPaymentTransactionWrapper( + transactionIdentifier: '', + payment: SKPaymentWrapper(productIdentifier: productId), + transactionState: SKPaymentTransactionStateWrapper.failed, + transactionTimeStamp: 123123.121, + error: SKError( + code: 0, + domain: 'ios_domain', + userInfo: {'message': 'an error message'}), + originalTransaction: null); + } + + Future onMethodCall(MethodCall call) { + switch (call.method) { + case '-[SKPaymentQueue canMakePayments:]': + return Future.value(true); + case '-[InAppPurchasePlugin startProductRequest:result:]': + if (queryProductException != null) { + throw queryProductException!; + } + List productIDS = + List.castFrom(call.arguments); + List invalidFound = []; + List products = []; + for (String productID in productIDS) { + if (!validProductIDs.contains(productID)) { + invalidFound.add(productID); + } else { + products.add(validProducts[productID]!); + } + } + SkProductResponseWrapper response = SkProductResponseWrapper( + products: products, invalidProductIdentifiers: invalidFound); + return Future>.value( + buildProductResponseMap(response)); + case '-[InAppPurchasePlugin restoreTransactions:result:]': + if (restoreException != null) { + throw restoreException!; + } + if (testRestoredError != null) { + InAppPurchaseIosPlatform.observer + .restoreCompletedTransactionsFailed(error: testRestoredError!); + return Future.sync(() {}); + } + if (!testRestoredTransactionsNull) { + InAppPurchaseIosPlatform.observer + .updatedTransactions(transactions: transactions); + } + InAppPurchaseIosPlatform.observer + .paymentQueueRestoreCompletedTransactionsFinished(); + + return Future.sync(() {}); + case '-[InAppPurchasePlugin retrieveReceiptData:result:]': + if (receiptData != null) { + return Future.value(receiptData); + } else { + throw PlatformException(code: 'no_receipt_data'); + } + case '-[InAppPurchasePlugin refreshReceipt:result:]': + receiptData = 'refreshed receipt data'; + return Future.sync(() {}); + case '-[InAppPurchasePlugin addPayment:result:]': + String id = call.arguments['productIdentifier']; + SKPaymentTransactionWrapper transaction = createPendingTransaction(id); + InAppPurchaseIosPlatform.observer + .updatedTransactions(transactions: [transaction]); + sleep(const Duration(milliseconds: 30)); + if (testTransactionFail) { + SKPaymentTransactionWrapper transaction_failed = + createFailedTransaction(id); + InAppPurchaseIosPlatform.observer + .updatedTransactions(transactions: [transaction_failed]); + } else { + SKPaymentTransactionWrapper transaction_finished = + createPurchasedTransaction( + id, transaction.transactionIdentifier ?? ''); + InAppPurchaseIosPlatform.observer + .updatedTransactions(transactions: [transaction_finished]); + } + break; + case '-[InAppPurchasePlugin finishTransaction:result:]': + finishedTransactions.add(createPurchasedTransaction( + call.arguments["productIdentifier"], + call.arguments["transactionIdentifier"])); + break; + case '-[SKPaymentQueue startObservingTransactionQueue]': + queueIsActive = true; + break; + case '-[SKPaymentQueue stopObservingTransactionQueue]': + queueIsActive = false; + break; + } + return Future.sync(() {}); + } +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_addtion_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_addtion_test.dart 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..865468f532bf --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/test/in_app_purchase_ios_platform_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 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:in_app_purchase_ios/in_app_purchase_ios.dart'; +import 'package:in_app_purchase_ios/src/store_kit_wrappers/enum_converters.dart'; +import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; +import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart'; + +import 'fakes/fake_ios_platform.dart'; +import 'store_kit_wrappers/sk_test_stub_objects.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final FakeIOSPlatform fakeIOSPlatform = FakeIOSPlatform(); + late InAppPurchaseIosPlatform iapIosPlatform; + + setUpAll(() { + SystemChannels.platform + .setMockMethodCallHandler(fakeIOSPlatform.onMethodCall); + }); + + setUp(() { + InAppPurchaseIosPlatform.registerPlatform(); + iapIosPlatform = InAppPurchasePlatform.instance as InAppPurchaseIosPlatform; + fakeIOSPlatform.reset(); + }); + + tearDown(() => fakeIOSPlatform.reset()); + + group('isAvailable', () { + test('true', () async { + expect(await iapIosPlatform.isAvailable(), isTrue); + }); + }); + + group('query product list', () { + test('should get product list and correct invalid identifiers', () async { + final InAppPurchaseIosPlatform connection = InAppPurchaseIosPlatform(); + final ProductDetailsResponse response = await connection + .queryProductDetails(['123', '456', '789'].toSet()); + List products = response.productDetails; + expect(products.first.id, '123'); + expect(products[1].id, '456'); + expect(response.notFoundIDs, ['789']); + expect(response.error, isNull); + expect(response.productDetails.first.currencySymbol, r'$'); + expect(response.productDetails[1].currencySymbol, 'EUR'); + }); + + test( + 'if query products throws error, should get error object in the response', + () async { + fakeIOSPlatform.queryProductException = PlatformException( + code: 'error_code', + message: 'error_message', + details: {'info': 'error_info'}); + final InAppPurchaseIosPlatform connection = InAppPurchaseIosPlatform(); + final ProductDetailsResponse response = await connection + .queryProductDetails(['123', '456', '789'].toSet()); + expect(response.productDetails, []); + expect(response.notFoundIDs, ['123', '456', '789']); + expect(response.error, isNotNull); + expect(response.error!.source, kIAPSource); + expect(response.error!.code, 'error_code'); + expect(response.error!.message, 'error_message'); + expect(response.error!.details, {'info': 'error_info'}); + }); + }); + + group('restore purchases', () { + test('should emit restored transactions on purchase stream', () async { + Completer completer = Completer(); + Stream> stream = iapIosPlatform.purchaseStream; + + late StreamSubscription subscription; + subscription = stream.listen((purchaseDetailsList) { + if (purchaseDetailsList.first.status == PurchaseStatus.restored) { + completer.complete(purchaseDetailsList); + subscription.cancel(); + } + }); + + await iapIosPlatform.restorePurchases(); + List details = await completer.future; + + expect(details.length, 2); + for (int i = 0; i < fakeIOSPlatform.transactions.length; i++) { + SKPaymentTransactionWrapper expected = fakeIOSPlatform.transactions[i]; + PurchaseDetails actual = details[i]; + + expect(actual.purchaseID, expected.transactionIdentifier); + expect(actual.verificationData, isNotNull); + expect(actual.status, PurchaseStatus.restored); + expect(actual.verificationData.localVerificationData, + fakeIOSPlatform.receiptData); + expect(actual.verificationData.serverVerificationData, + fakeIOSPlatform.receiptData); + expect(actual.pendingCompletePurchase, true); + } + }); + + test('should not block transaction updates', () async { + fakeIOSPlatform.transactions + .insert(0, fakeIOSPlatform.createPurchasedTransaction('foo', 'bar')); + Completer completer = Completer(); + Stream> stream = iapIosPlatform.purchaseStream; + + late StreamSubscription subscription; + subscription = stream.listen((purchaseDetailsList) { + if (purchaseDetailsList.first.status == PurchaseStatus.purchased) { + completer.complete(purchaseDetailsList); + subscription.cancel(); + } + }); + await iapIosPlatform.restorePurchases(); + List details = await completer.future; + expect(details.length, 3); + for (int i = 0; i < fakeIOSPlatform.transactions.length; i++) { + SKPaymentTransactionWrapper expected = fakeIOSPlatform.transactions[i]; + PurchaseDetails actual = details[i]; + + expect(actual.purchaseID, expected.transactionIdentifier); + expect(actual.verificationData, isNotNull); + expect( + actual.status, + SKTransactionStatusConverter() + .toPurchaseStatus(expected.transactionState), + ); + expect(actual.verificationData.localVerificationData, + fakeIOSPlatform.receiptData); + expect(actual.verificationData.serverVerificationData, + fakeIOSPlatform.receiptData); + expect(actual.pendingCompletePurchase, true); + } + }); + + test('receipt error should populate null to verificationData.data', + () async { + fakeIOSPlatform.receiptData = null; + Completer completer = Completer(); + Stream> stream = iapIosPlatform.purchaseStream; + + late StreamSubscription subscription; + subscription = stream.listen((purchaseDetailsList) { + if (purchaseDetailsList.first.status == PurchaseStatus.restored) { + completer.complete(purchaseDetailsList); + subscription.cancel(); + } + }); + + await iapIosPlatform.restorePurchases(); + List details = await completer.future; + + for (PurchaseDetails purchase in details) { + expect(purchase.verificationData.localVerificationData, isEmpty); + expect(purchase.verificationData.serverVerificationData, isEmpty); + } + }); + + test('test restore error', () { + fakeIOSPlatform.testRestoredError = SKError( + code: 123, + domain: 'error_test', + userInfo: {'message': 'errorMessage'}); + + expect( + () => iapIosPlatform.restorePurchases(), + throwsA( + isA() + .having((error) => error.code, 'code', 123) + .having((error) => error.domain, 'domain', 'error_test') + .having((error) => error.userInfo, 'userInfo', + {'message': 'errorMessage'}), + )); + }); + }); + + group('make payment', () { + test( + 'buying non consumable, should get purchase objects in the purchase update callback', + () async { + List details = []; + Completer completer = Completer(); + Stream> stream = iapIosPlatform.purchaseStream; + + late StreamSubscription subscription; + subscription = stream.listen((purchaseDetailsList) { + details.addAll(purchaseDetailsList); + if (purchaseDetailsList.first.status == PurchaseStatus.purchased) { + completer.complete(details); + subscription.cancel(); + } + }); + final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( + productDetails: + AppStoreProductDetails.fromSKProduct(dummyProductWrapper), + applicationUserName: 'appName'); + await iapIosPlatform.buyNonConsumable(purchaseParam: purchaseParam); + + List result = await completer.future; + expect(result.length, 2); + expect(result.first.productID, dummyProductWrapper.productIdentifier); + }); + + test( + 'buying consumable, should get purchase objects in the purchase update callback', + () async { + List details = []; + Completer completer = Completer(); + Stream> stream = iapIosPlatform.purchaseStream; + + late StreamSubscription subscription; + subscription = stream.listen((purchaseDetailsList) { + details.addAll(purchaseDetailsList); + if (purchaseDetailsList.first.status == PurchaseStatus.purchased) { + completer.complete(details); + subscription.cancel(); + } + }); + final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( + productDetails: + AppStoreProductDetails.fromSKProduct(dummyProductWrapper), + applicationUserName: 'appName'); + await iapIosPlatform.buyConsumable(purchaseParam: purchaseParam); + + List result = await completer.future; + expect(result.length, 2); + expect(result.first.productID, dummyProductWrapper.productIdentifier); + }); + + test('buying consumable, should throw when autoConsume is false', () async { + final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( + productDetails: + AppStoreProductDetails.fromSKProduct(dummyProductWrapper), + applicationUserName: 'appName'); + expect( + () => iapIosPlatform.buyConsumable( + purchaseParam: purchaseParam, autoConsume: false), + throwsA(isInstanceOf())); + }); + + test('should get failed purchase status', () async { + fakeIOSPlatform.testTransactionFail = true; + List details = []; + Completer completer = Completer(); + late IAPError error; + + Stream> stream = iapIosPlatform.purchaseStream; + late StreamSubscription subscription; + subscription = stream.listen((purchaseDetailsList) { + details.addAll(purchaseDetailsList); + purchaseDetailsList.forEach((purchaseDetails) { + if (purchaseDetails.status == PurchaseStatus.error) { + error = purchaseDetails.error!; + completer.complete(error); + subscription.cancel(); + } + }); + }); + final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( + productDetails: + AppStoreProductDetails.fromSKProduct(dummyProductWrapper), + applicationUserName: 'appName'); + await iapIosPlatform.buyNonConsumable(purchaseParam: purchaseParam); + + IAPError completerError = await completer.future; + expect(completerError.code, 'purchase_error'); + expect(completerError.source, kIAPSource); + expect(completerError.message, 'ios_domain'); + expect(completerError.details, {'message': 'an error message'}); + }); + }); + + group('complete purchase', () { + test('should complete purchase', () async { + List details = []; + Completer completer = Completer(); + Stream> stream = iapIosPlatform.purchaseStream; + late StreamSubscription subscription; + subscription = stream.listen((purchaseDetailsList) { + details.addAll(purchaseDetailsList); + purchaseDetailsList.forEach((purchaseDetails) { + if (purchaseDetails.pendingCompletePurchase) { + iapIosPlatform.completePurchase(purchaseDetails); + completer.complete(details); + subscription.cancel(); + } + }); + }); + final AppStorePurchaseParam purchaseParam = AppStorePurchaseParam( + productDetails: + AppStoreProductDetails.fromSKProduct(dummyProductWrapper), + applicationUserName: 'appName'); + await iapIosPlatform.buyNonConsumable(purchaseParam: purchaseParam); + List result = await completer.future; + expect(result.length, 2); + expect(result.first.productID, dummyProductWrapper.productIdentifier); + expect(fakeIOSPlatform.finishedTransactions.length, 1); + }); + }); + + group('purchase stream', () { + test('Should only have active queue when purchaseStream has listeners', () { + Stream> stream = iapIosPlatform.purchaseStream; + expect(fakeIOSPlatform.queueIsActive, false); + StreamSubscription subscription1 = stream.listen((event) {}); + expect(fakeIOSPlatform.queueIsActive, true); + StreamSubscription subscription2 = stream.listen((event) {}); + expect(fakeIOSPlatform.queueIsActive, true); + subscription1.cancel(); + expect(fakeIOSPlatform.queueIsActive, true); + subscription2.cancel(); + expect(fakeIOSPlatform.queueIsActive, false); + }); + }); +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart new file mode 100644 index 000000000000..c7f7d800f45f --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_methodchannel_apis_test.dart @@ -0,0 +1,298 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:in_app_purchase_ios/src/channel.dart'; +import 'package:in_app_purchase_ios/store_kit_wrappers.dart'; +import 'sk_test_stub_objects.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final FakeIOSPlatform fakeIOSPlatform = FakeIOSPlatform(); + + setUpAll(() { + SystemChannels.platform + .setMockMethodCallHandler(fakeIOSPlatform.onMethodCall); + }); + + setUp(() {}); + + tearDown(() { + fakeIOSPlatform.testReturnNull = false; + fakeIOSPlatform.queueIsActive = null; + fakeIOSPlatform.getReceiptFailTest = false; + }); + + group('sk_request_maker', () { + test('get products method channel', () async { + SkProductResponseWrapper productResponseWrapper = + await SKRequestMaker().startProductRequest(['xxx']); + expect( + productResponseWrapper.products, + isNotEmpty, + ); + expect( + productResponseWrapper.products.first.priceLocale.currencySymbol, + '\$', + ); + + expect( + productResponseWrapper.products.first.priceLocale.currencySymbol, + isNot('A'), + ); + expect( + productResponseWrapper.products.first.priceLocale.currencyCode, + 'USD', + ); + expect( + productResponseWrapper.products.first.priceLocale.countryCode, + 'US', + ); + expect( + productResponseWrapper.invalidProductIdentifiers, + isNotEmpty, + ); + + expect( + fakeIOSPlatform.startProductRequestParam, + ['xxx'], + ); + }); + + test('get products method channel should throw exception', () async { + fakeIOSPlatform.getProductRequestFailTest = true; + expect( + SKRequestMaker().startProductRequest(['xxx']), + throwsException, + ); + fakeIOSPlatform.getProductRequestFailTest = false; + }); + + test('refreshed receipt', () async { + int receiptCountBefore = fakeIOSPlatform.refreshReceipt; + await SKRequestMaker().startRefreshReceiptRequest( + receiptProperties: {"isExpired": true}); + expect(fakeIOSPlatform.refreshReceipt, receiptCountBefore + 1); + expect(fakeIOSPlatform.refreshReceiptParam, + {"isExpired": true}); + }); + + test('should get null receipt if any exceptions are raised', () async { + fakeIOSPlatform.getReceiptFailTest = true; + expect(() async => SKReceiptManager.retrieveReceiptData(), + throwsA(TypeMatcher())); + }); + }); + + group('sk_receipt_manager', () { + test('should get receipt (faking it by returning a `receipt data` string)', + () async { + String receiptData = await SKReceiptManager.retrieveReceiptData(); + expect(receiptData, 'receipt data'); + }); + }); + + group('sk_payment_queue', () { + test('canMakePayment should return true', () async { + expect(await SKPaymentQueueWrapper.canMakePayments(), true); + }); + + test('canMakePayment returns false if method channel returns null', + () async { + fakeIOSPlatform.testReturnNull = true; + expect(await SKPaymentQueueWrapper.canMakePayments(), false); + }); + + test('transactions should return a valid list of transactions', () async { + expect(await SKPaymentQueueWrapper().transactions(), isNotEmpty); + }); + + test( + 'throws if observer is not set for payment queue before adding payment', + () async { + expect(SKPaymentQueueWrapper().addPayment(dummyPayment), + throwsAssertionError); + }); + + test('should add payment to the payment queue', () async { + SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); + TestPaymentTransactionObserver observer = + TestPaymentTransactionObserver(); + queue.setTransactionObserver(observer); + await queue.addPayment(dummyPayment); + expect(fakeIOSPlatform.payments.first, equals(dummyPayment)); + }); + + test('should finish transaction', () async { + SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); + TestPaymentTransactionObserver observer = + TestPaymentTransactionObserver(); + queue.setTransactionObserver(observer); + await queue.finishTransaction(dummyTransaction); + expect(fakeIOSPlatform.transactionsFinished.first, + equals(dummyTransaction.toFinishMap())); + }); + + test('should restore transaction', () async { + SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); + TestPaymentTransactionObserver observer = + TestPaymentTransactionObserver(); + queue.setTransactionObserver(observer); + await queue.restoreTransactions(applicationUserName: 'aUserID'); + expect(fakeIOSPlatform.applicationNameHasTransactionRestored, 'aUserID'); + }); + + test('startObservingTransactionQueue should call methodChannel', () async { + expect(fakeIOSPlatform.queueIsActive, isNot(true)); + await SKPaymentQueueWrapper().startObservingTransactionQueue(); + expect(fakeIOSPlatform.queueIsActive, true); + }); + + test('stopObservingTransactionQueue should call methodChannel', () async { + expect(fakeIOSPlatform.queueIsActive, isNot(false)); + await SKPaymentQueueWrapper().stopObservingTransactionQueue(); + expect(fakeIOSPlatform.queueIsActive, false); + }); + + test('setDelegate should call methodChannel', () async { + expect(fakeIOSPlatform.isPaymentQueueDelegateRegistered, false); + await SKPaymentQueueWrapper().setDelegate(TestPaymentQueueDelegate()); + expect(fakeIOSPlatform.isPaymentQueueDelegateRegistered, true); + await SKPaymentQueueWrapper().setDelegate(null); + expect(fakeIOSPlatform.isPaymentQueueDelegateRegistered, false); + }); + + test('showPriceConsentIfNeeded should call methodChannel', () async { + expect(fakeIOSPlatform.showPriceConsentIfNeeded, false); + await SKPaymentQueueWrapper().showPriceConsentIfNeeded(); + expect(fakeIOSPlatform.showPriceConsentIfNeeded, true); + }); + }); + + group('Code Redemption Sheet', () { + test('presentCodeRedemptionSheet should not throw', () async { + expect(fakeIOSPlatform.presentCodeRedemption, false); + await SKPaymentQueueWrapper().presentCodeRedemptionSheet(); + expect(fakeIOSPlatform.presentCodeRedemption, true); + fakeIOSPlatform.presentCodeRedemption = false; + }); + }); +} + +class FakeIOSPlatform { + FakeIOSPlatform() { + channel.setMockMethodCallHandler(onMethodCall); + } + // get product request + List startProductRequestParam = []; + bool getProductRequestFailTest = false; + bool testReturnNull = false; + + // get receipt request + bool getReceiptFailTest = false; + + // refresh receipt request + int refreshReceipt = 0; + late Map refreshReceiptParam; + + // payment queue + List payments = []; + List> transactionsFinished = []; + String applicationNameHasTransactionRestored = ''; + + // present Code Redemption + bool presentCodeRedemption = false; + + // show price consent sheet + bool showPriceConsentIfNeeded = false; + + // indicate if the payment queue delegate is registered + bool isPaymentQueueDelegateRegistered = false; + + // Listen to purchase updates + bool? queueIsActive; + + Future onMethodCall(MethodCall call) { + switch (call.method) { + // request makers + case '-[InAppPurchasePlugin startProductRequest:result:]': + startProductRequestParam = call.arguments; + if (getProductRequestFailTest) { + return Future.value(null); + } + return Future>.value( + buildProductResponseMap(dummyProductResponseWrapper)); + case '-[InAppPurchasePlugin refreshReceipt:result:]': + refreshReceipt++; + refreshReceiptParam = + Map.castFrom(call.arguments); + return Future.sync(() {}); + // receipt manager + case '-[InAppPurchasePlugin retrieveReceiptData:result:]': + if (getReceiptFailTest) { + throw ("some arbitrary error"); + } + return Future.value('receipt data'); + // payment queue + case '-[SKPaymentQueue canMakePayments:]': + if (testReturnNull) { + return Future.value(null); + } + return Future.value(true); + case '-[SKPaymentQueue transactions]': + return Future>.value( + [buildTransactionMap(dummyTransaction)]); + case '-[InAppPurchasePlugin addPayment:result:]': + payments.add(SKPaymentWrapper.fromJson( + Map.from(call.arguments))); + return Future.sync(() {}); + case '-[InAppPurchasePlugin finishTransaction:result:]': + transactionsFinished.add(Map.from(call.arguments)); + return Future.sync(() {}); + case '-[InAppPurchasePlugin restoreTransactions:result:]': + applicationNameHasTransactionRestored = call.arguments; + return Future.sync(() {}); + case '-[InAppPurchasePlugin presentCodeRedemptionSheet:result:]': + presentCodeRedemption = true; + return Future.sync(() {}); + case '-[SKPaymentQueue startObservingTransactionQueue]': + queueIsActive = true; + return Future.sync(() {}); + case '-[SKPaymentQueue stopObservingTransactionQueue]': + queueIsActive = false; + return Future.sync(() {}); + case '-[SKPaymentQueue registerDelegate]': + isPaymentQueueDelegateRegistered = true; + return Future.sync(() {}); + case '-[SKPaymentQueue removeDelegate]': + isPaymentQueueDelegateRegistered = false; + return Future.sync(() {}); + case '-[SKPaymentQueue showPriceConsentIfNeeded]': + showPriceConsentIfNeeded = true; + return Future.sync(() {}); + } + return Future.error('method not mocked'); + } +} + +class TestPaymentQueueDelegate extends SKPaymentQueueDelegateWrapper {} + +class TestPaymentTransactionObserver extends SKTransactionObserverWrapper { + void updatedTransactions( + {required List transactions}) {} + + void removedTransactions( + {required List transactions}) {} + + void restoreCompletedTransactionsFailed({required SKError error}) {} + + void paymentQueueRestoreCompletedTransactionsFinished() {} + + bool shouldAddStorePayment( + {required SKPaymentWrapper payment, required SKProductWrapper product}) { + return true; + } +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart new file mode 100644 index 000000000000..ca2b3364d680 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_payment_queue_delegate_api_test.dart @@ -0,0 +1,168 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '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'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final FakeIOSPlatform fakeIOSPlatform = FakeIOSPlatform(); + + setUpAll(() { + SystemChannels.platform + .setMockMethodCallHandler(fakeIOSPlatform.onMethodCall); + }); + + test( + 'handlePaymentQueueDelegateCallbacks should call SKPaymentQueueDelegateWrapper.shouldContinueTransaction', + () async { + SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); + TestPaymentQueueDelegate testDelegate = TestPaymentQueueDelegate(); + await queue.setDelegate(testDelegate); + + final Map arguments = { + 'storefront': { + 'countryCode': 'USA', + 'identifier': 'unique_identifier', + }, + 'transaction': { + 'payment': { + 'productIdentifier': 'product_identifier', + } + }, + }; + + final result = await queue.handlePaymentQueueDelegateCallbacks( + MethodCall('shouldContinueTransaction', arguments), + ); + + expect(result, false); + expect( + testDelegate.log, + { + equals('shouldContinueTransaction'), + }, + ); + }); + + test( + 'handlePaymentQueueDelegateCallbacks should call SKPaymentQueueDelegateWrapper.shouldShowPriceConsent', + () async { + SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); + TestPaymentQueueDelegate testDelegate = TestPaymentQueueDelegate(); + await queue.setDelegate(testDelegate); + + final result = await queue.handlePaymentQueueDelegateCallbacks( + MethodCall('shouldShowPriceConsent'), + ); + + expect(result, false); + expect( + testDelegate.log, + { + equals('shouldShowPriceConsent'), + }, + ); + }); + + test( + 'handleObserverCallbacks should call SKTransactionObserverWrapper.restoreCompletedTransactionsFailed', + () async { + SKPaymentQueueWrapper queue = SKPaymentQueueWrapper(); + TestTransactionObserverWrapper testObserver = + TestTransactionObserverWrapper(); + queue.setTransactionObserver(testObserver); + + final arguments = { + 'code': 100, + 'domain': 'domain', + 'userInfo': {'error': 'underlying_error'}, + }; + + await queue.handleObserverCallbacks( + MethodCall('restoreCompletedTransactionsFailed', arguments), + ); + + expect( + testObserver.log, + { + equals('restoreCompletedTransactionsFailed'), + }, + ); + }); +} + +class TestTransactionObserverWrapper extends SKTransactionObserverWrapper { + final List log = []; + + @override + void updatedTransactions( + {required List transactions}) { + log.add('updatedTransactions'); + } + + @override + void removedTransactions( + {required List transactions}) { + log.add('removedTransactions'); + } + + @override + void restoreCompletedTransactionsFailed({required SKError error}) { + log.add('restoreCompletedTransactionsFailed'); + } + + @override + void paymentQueueRestoreCompletedTransactionsFinished() { + log.add('paymentQueueRestoreCompletedTransactionsFinished'); + } + + @override + bool shouldAddStorePayment( + {required SKPaymentWrapper payment, required SKProductWrapper product}) { + log.add('shouldAddStorePayment'); + return false; + } +} + +class TestPaymentQueueDelegate extends SKPaymentQueueDelegateWrapper { + final List log = []; + + @override + bool shouldContinueTransaction( + SKPaymentTransactionWrapper transaction, SKStorefrontWrapper storefront) { + log.add('shouldContinueTransaction'); + return false; + } + + @override + bool shouldShowPriceConsent() { + log.add('shouldShowPriceConsent'); + return false; + } +} + +class FakeIOSPlatform { + FakeIOSPlatform() { + channel.setMockMethodCallHandler(onMethodCall); + } + + // indicate if the payment queue delegate is registered + bool isPaymentQueueDelegateRegistered = false; + + Future onMethodCall(MethodCall call) { + switch (call.method) { + case '-[SKPaymentQueue registerDelegate]': + isPaymentQueueDelegateRegistered = true; + return Future.sync(() {}); + case '-[SKPaymentQueue removeDelegate]': + isPaymentQueueDelegateRegistered = false; + return Future.sync(() {}); + } + return Future.error('method not mocked'); + } +} diff --git a/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_product_test.dart b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_product_test.dart new file mode 100644 index 000000000000..6a33b75d9808 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_ios/test/store_kit_wrappers/sk_product_test.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 '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: '', + countryCode: '', + )); + 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: '', + countryCode: '', + )); + 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(dollarLocale)); + expect(wrapper, equals(dollarLocale)); + }); + }); + + 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 81% 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 1dc70748f1db..595a074f1cfe 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', @@ -22,6 +22,7 @@ final SKPaymentTransactionWrapper dummyOriginalTransaction = transactionIdentifier: '123123', error: dummyError, ); + final SKPaymentTransactionWrapper dummyTransaction = SKPaymentTransactionWrapper( transactionState: SKPaymentTransactionStateWrapper.purchased, @@ -32,8 +33,17 @@ final SKPaymentTransactionWrapper dummyTransaction = error: dummyError, ); -final SKPriceLocaleWrapper dummyLocale = - SKPriceLocaleWrapper(currencySymbol: '\$', currencyCode: 'USD'); +final SKPriceLocaleWrapper dollarLocale = SKPriceLocaleWrapper( + currencySymbol: '\$', + currencyCode: 'USD', + countryCode: 'US', +); + +final SKPriceLocaleWrapper noSymbolLocale = SKPriceLocaleWrapper( + currencySymbol: '', + currencyCode: 'EUR', + countryCode: 'UK', +); final SKProductSubscriptionPeriodWrapper dummySubscription = SKProductSubscriptionPeriodWrapper( @@ -43,7 +53,7 @@ final SKProductSubscriptionPeriodWrapper dummySubscription = final SKProductDiscountWrapper dummyDiscount = SKProductDiscountWrapper( price: '1.0', - priceLocale: dummyLocale, + priceLocale: dollarLocale, numberOfPeriods: 1, paymentMode: SKProductDiscountPaymentMode.payUpFront, subscriptionPeriod: dummySubscription, @@ -53,7 +63,7 @@ final SKProductWrapper dummyProductWrapper = SKProductWrapper( productIdentifier: 'id', localizedTitle: 'title', localizedDescription: 'description', - priceLocale: dummyLocale, + priceLocale: dollarLocale, subscriptionGroupIdentifier: 'com.group', price: '1.0', subscriptionPeriod: dummySubscription, @@ -69,12 +79,16 @@ final SkProductResponseWrapper dummyProductResponseWrapper = Map buildLocaleMap(SKPriceLocaleWrapper local) { return { 'currencySymbol': local.currencySymbol, - 'currencyCode': local.currencyCode + 'currencyCode': local.currencyCode, + 'countryCode': local.countryCode, }; } -Map buildSubscriptionPeriodMap( - SKProductSubscriptionPeriodWrapper sub) { +Map? buildSubscriptionPeriodMap( + SKProductSubscriptionPeriodWrapper? sub) { + if (sub == null) { + return null; + } return { 'numberOfUnits': sub.numberOfUnits, 'unit': SKSubscriptionPeriodUnit.values.indexOf(sub.unit), @@ -103,7 +117,7 @@ Map buildProductMap(SKProductWrapper product) { 'price': product.price, 'subscriptionPeriod': buildSubscriptionPeriodMap(product.subscriptionPeriod), - 'introductoryPrice': buildDiscountMap(product.introductoryPrice), + 'introductoryPrice': buildDiscountMap(product.introductoryPrice!), }; } @@ -128,17 +142,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/AUTHORS b/packages/in_app_purchase/in_app_purchase_platform_interface/AUTHORS new file mode 100644 index 000000000000..78f9e5ad9f6b --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/AUTHORS @@ -0,0 +1,67 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Maurits van Beusekom 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..cd4b86d7f39a --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/CHANGELOG.md @@ -0,0 +1,15 @@ +## 1.2.0 + +* Added `toString()` to `IAPError` + +## 1.1.0 + +* Added `currencySymbol` in ProductDetails. + +## 1.0.1 + +* Fixed `Restoring previous purchases` link. + +## 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..8e10997aaedc --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/errors.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 'in_app_purchase_error.dart'; +export 'in_app_purchase_exception.dart'; diff --git a/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/in_app_purchase_error.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/in_app_purchase_error.dart new file mode 100644 index 000000000000..166646d35b24 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/errors/in_app_purchase_error.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. + +/// 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; + + @override + String toString() { + return 'IAPError(code: $code, source: $source, message: $message, details: $details)'; + } +} 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/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..aa03a41b4776 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/product_details.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. + +/// 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, + this.currencySymbol = '', + }); + + /// 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; + + /// The currency symbol for the locale, e.g. $ for US locale. + /// + /// When the currency symbol cannot be determined, the ISO 4217 currency code is returned. + final String currencySymbol; +} 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..3a9d7c3c976e --- /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 '../errors/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..8c98beb591ef --- /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 '../errors/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..78695066702d --- /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#restoring-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..7cb666408249 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/lib/src/types/types.dart @@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export '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..64574e0cf306 --- /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.2.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_error_test.dart b/packages/in_app_purchase/in_app_purchase_platform_interface/test/src/errors/in_app_purchase_error_test.dart new file mode 100644 index 000000000000..ed63f495b4c2 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/test/src/errors/in_app_purchase_error_test.dart @@ -0,0 +1,29 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:in_app_purchase_platform_interface/src/errors/in_app_purchase_error.dart'; + +void main() { + test('toString: Should return a description of the error', () { + final IAPError exceptionNoDetails = IAPError( + code: 'error_code', + message: 'dummy_message', + source: 'dummy_source', + ); + + expect(exceptionNoDetails.toString(), + 'IAPError(code: error_code, source: dummy_source, message: dummy_message, details: null)'); + + final IAPError exceptionWithDetails = IAPError( + code: 'error_code', + message: 'dummy_message', + source: 'dummy_source', + details: 'dummy_details', + ); + + expect(exceptionWithDetails.toString(), + 'IAPError(code: error_code, source: dummy_source, message: dummy_message, details: dummy_details)'); + }); +} 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..ce49d9992131 --- /dev/null +++ b/packages/in_app_purchase/in_app_purchase_platform_interface/test/src/types/product_details_test.dart @@ -0,0 +1,30 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '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', + currencySymbol: r'$', + rawPrice: 13.37); + + expect(productDetails.id, 'id'); + expect(productDetails.title, 'title'); + expect(productDetails.description, 'description'); + expect(productDetails.rawPrice, 13.37); + expect(productDetails.currencyCode, 'USD'); + expect(productDetails.currencySymbol, r'$'); + }); + }); +} 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/Classes/FIAObjectTranslator.h b/packages/in_app_purchase/ios/Classes/FIAObjectTranslator.h deleted file mode 100644 index 5243a391ddaf..000000000000 --- a/packages/in_app_purchase/ios/Classes/FIAObjectTranslator.h +++ /dev/null @@ -1,35 +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 - -@interface FIAObjectTranslator : NSObject - -+ (NSDictionary *)getMapFromSKProduct:(SKProduct *)product; - -+ (NSDictionary *)getMapFromSKProductSubscriptionPeriod:(SKProductSubscriptionPeriod *)period - API_AVAILABLE(ios(11.2)); - -+ (NSDictionary *)getMapFromSKProductDiscount:(SKProductDiscount *)discount - API_AVAILABLE(ios(11.2)); - -+ (NSDictionary *)getMapFromSKProductsResponse:(SKProductsResponse *)productResponse; - -+ (NSDictionary *)getMapFromSKPayment:(SKPayment *)payment; - -+ (NSDictionary *)getMapFromNSLocale:(NSLocale *)locale; - -+ (SKMutablePayment *)getSKMutablePaymentFromMap:(NSDictionary *)map; - -+ (NSDictionary *)getMapFromSKPaymentTransaction:(SKPaymentTransaction *)transaction; - -+ (NSDictionary *)getMapFromNSError:(NSError *)error; - -@end -; - -NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.m b/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.m deleted file mode 100644 index 92872d91234e..000000000000 --- a/packages/in_app_purchase/ios/Classes/FIAPReceiptManager.m +++ /dev/null @@ -1,33 +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. - -// -// FIAPReceiptManager.m -// in_app_purchase -// -// Created by Chris Yang on 3/2/19. -// - -#import "FIAPReceiptManager.h" -#import - -@implementation FIAPReceiptManager - -- (NSString *)retrieveReceiptWithError:(FlutterError **)error { - NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL]; - NSData *receipt = [self getReceiptData:receiptURL]; - if (!receipt) { - *error = [FlutterError errorWithCode:@"storekit_no_receipt" - message:@"Cannot find receipt for the current main bundle." - details:nil]; - return nil; - } - return [receipt base64EncodedStringWithOptions:kNilOptions]; -} - -- (NSData *)getReceiptData:(NSURL *)url { - return [NSData dataWithContentsOfURL:url]; -} - -@end diff --git a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h b/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h deleted file mode 100644 index ed1788186909..000000000000 --- a/packages/in_app_purchase/ios/Classes/FIAPaymentQueueHandler.h +++ /dev/null @@ -1,48 +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 - -@class SKPaymentTransaction; - -NS_ASSUME_NONNULL_BEGIN - -typedef void (^TransactionsUpdated)(NSArray *transactions); -typedef void (^TransactionsRemoved)(NSArray *transactions); -typedef void (^RestoreTransactionFailed)(NSError *error); -typedef void (^RestoreCompletedTransactionsFinished)(void); -typedef BOOL (^ShouldAddStorePayment)(SKPayment *payment, SKProduct *product); -typedef void (^UpdatedDownloads)(NSArray *downloads); - -@interface FIAPaymentQueueHandler : NSObject - -// Unfinished transactions. -@property(nonatomic, readonly) NSDictionary *transactions; - -- (instancetype)initWithQueue:(nonnull SKPaymentQueue *)queue - transactionsUpdated:(nullable TransactionsUpdated)transactionsUpdated - transactionRemoved:(nullable TransactionsRemoved)transactionsRemoved - restoreTransactionFailed:(nullable RestoreTransactionFailed)restoreTransactionFailed - restoreCompletedTransactionsFinished: - (nullable RestoreCompletedTransactionsFinished)restoreCompletedTransactionsFinished - shouldAddStorePayment:(nullable ShouldAddStorePayment)shouldAddStorePayment - updatedDownloads:(nullable UpdatedDownloads)updatedDownloads; -// 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; -- (NSArray *)getUnfinishedTransactions; - -// This method needs to be called before any other methods. -- (void)startObservingPaymentQueue; - -// Appends a payment to the SKPaymentQueue. -// -// @param payment Payment object to be added to the payment queue. -// @return whether "addPayment" was successful. -- (BOOL)addPayment:(SKPayment *)payment; - -@end - -NS_ASSUME_NONNULL_END diff --git a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m b/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m deleted file mode 100644 index bfe29f981396..000000000000 --- a/packages/in_app_purchase/ios/Classes/InAppPurchasePlugin.m +++ /dev/null @@ -1,348 +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 "InAppPurchasePlugin.h" -#import -#import "FIAObjectTranslator.h" -#import "FIAPReceiptManager.h" -#import "FIAPRequestHandler.h" -#import "FIAPaymentQueueHandler.h" - -@interface InAppPurchasePlugin () - -// Holding strong references to FIAPRequestHandlers. Remove the handlers from the set after -// the request is finished. -@property(strong, nonatomic, readonly) NSMutableSet *requestHandlers; - -// After querying the product, the available products will be saved in the map to be used -// for purchase. -@property(strong, nonatomic, readonly) NSMutableDictionary *productsCache; - -// Call back channel to dart used for when a listener function is triggered. -@property(strong, nonatomic, readonly) FlutterMethodChannel *callbackChannel; -@property(strong, nonatomic, readonly) NSObject *registry; -@property(strong, nonatomic, readonly) NSObject *messenger; -@property(strong, nonatomic, readonly) NSObject *registrar; - -@property(strong, nonatomic, readonly) FIAPReceiptManager *receiptManager; - -@end - -@implementation InAppPurchasePlugin - -+ (void)registerWithRegistrar:(NSObject *)registrar { - FlutterMethodChannel *channel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase" - binaryMessenger:[registrar messenger]]; - InAppPurchasePlugin *instance = [[InAppPurchasePlugin alloc] initWithRegistrar:registrar]; - [registrar addMethodCallDelegate:instance channel:channel]; -} - -- (instancetype)initWithReceiptManager:(FIAPReceiptManager *)receiptManager { - self = [super init]; - _receiptManager = receiptManager; - _requestHandlers = [NSMutableSet new]; - _productsCache = [NSMutableDictionary new]; - return self; -} - -- (instancetype)initWithRegistrar:(NSObject *)registrar { - self = [self initWithReceiptManager:[FIAPReceiptManager new]]; - _registrar = registrar; - _registry = [registrar textures]; - _messenger = [registrar messenger]; - - __weak typeof(self) weakSelf = self; - _paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:[SKPaymentQueue defaultQueue] - transactionsUpdated:^(NSArray *_Nonnull transactions) { - [weakSelf handleTransactionsUpdated:transactions]; - } - transactionRemoved:^(NSArray *_Nonnull transactions) { - [weakSelf handleTransactionsRemoved:transactions]; - } - restoreTransactionFailed:^(NSError *_Nonnull error) { - [weakSelf handleTransactionRestoreFailed:error]; - } - restoreCompletedTransactionsFinished:^{ - [weakSelf restoreCompletedTransactionsFinished]; - } - shouldAddStorePayment:^BOOL(SKPayment *payment, SKProduct *product) { - return [weakSelf shouldAddStorePayment:payment product:product]; - } - updatedDownloads:^void(NSArray *_Nonnull downloads) { - [weakSelf updatedDownloads:downloads]; - }]; - [_paymentQueueHandler startObservingPaymentQueue]; - _callbackChannel = - [FlutterMethodChannel methodChannelWithName:@"plugins.flutter.io/in_app_purchase_callback" - binaryMessenger:[registrar messenger]]; - return self; -} - -- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if ([@"-[SKPaymentQueue canMakePayments:]" isEqualToString:call.method]) { - [self canMakePayments:result]; - } else if ([@"-[SKPaymentQueue transactions]" isEqualToString:call.method]) { - [self getPendingTransactions:result]; - } else if ([@"-[InAppPurchasePlugin startProductRequest:result:]" isEqualToString:call.method]) { - [self handleProductRequestMethodCall:call result:result]; - } else if ([@"-[InAppPurchasePlugin addPayment:result:]" isEqualToString:call.method]) { - [self addPayment:call result:result]; - } else if ([@"-[InAppPurchasePlugin finishTransaction:result:]" isEqualToString:call.method]) { - [self finishTransaction:call result:result]; - } else if ([@"-[InAppPurchasePlugin restoreTransactions:result:]" isEqualToString:call.method]) { - [self restoreTransactions:call result:result]; - } else if ([@"-[InAppPurchasePlugin retrieveReceiptData:result:]" isEqualToString:call.method]) { - [self retrieveReceiptData:call result:result]; - } else if ([@"-[InAppPurchasePlugin refreshReceipt:result:]" isEqualToString:call.method]) { - [self refreshReceipt:call result:result]; - } else { - result(FlutterMethodNotImplemented); - } -} - -- (void)canMakePayments:(FlutterResult)result { - result([NSNumber numberWithBool:[SKPaymentQueue canMakePayments]]); -} - -- (void)getPendingTransactions:(FlutterResult)result { - NSArray *transactions = - [self.paymentQueueHandler getUnfinishedTransactions]; - NSMutableArray *transactionMaps = [[NSMutableArray alloc] init]; - for (SKPaymentTransaction *transaction in transactions) { - [transactionMaps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]]; - } - result(transactionMaps); -} - -- (void)handleProductRequestMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result { - if (![call.arguments isKindOfClass:[NSArray class]]) { - result([FlutterError errorWithCode:@"storekit_invalid_argument" - message:@"Argument type of startRequest is not array" - details:call.arguments]); - return; - } - NSArray *productIdentifiers = (NSArray *)call.arguments; - SKProductsRequest *request = - [self getProductRequestWithIdentifiers:[NSSet setWithArray:productIdentifiers]]; - FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; - [self.requestHandlers addObject:handler]; - __weak typeof(self) weakSelf = self; - [handler startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable response, - NSError *_Nullable error) { - if (error) { - result([FlutterError errorWithCode:@"storekit_getproductrequest_platform_error" - message:error.localizedDescription - details:error.description]); - return; - } - if (!response) { - result([FlutterError errorWithCode:@"storekit_platform_no_response" - message:@"Failed to get SKProductResponse in startRequest " - @"call. Error occured on iOS platform" - details:call.arguments]); - return; - } - for (SKProduct *product in response.products) { - [self.productsCache setObject:product forKey:product.productIdentifier]; - } - result([FIAObjectTranslator getMapFromSKProductsResponse:response]); - [weakSelf.requestHandlers removeObject:handler]; - }]; -} - -- (void)addPayment:(FlutterMethodCall *)call result:(FlutterResult)result { - if (![call.arguments isKindOfClass:[NSDictionary class]]) { - result([FlutterError errorWithCode:@"storekit_invalid_argument" - message:@"Argument type of addPayment is not a Dictionary" - details:call.arguments]); - return; - } - NSDictionary *paymentMap = (NSDictionary *)call.arguments; - NSString *productID = [paymentMap objectForKey:@"productIdentifier"]; - // When a product is already fetched, we create a payment object with - // the product to process the payment. - SKProduct *product = [self getProduct:productID]; - if (!product) { - result([FlutterError - errorWithCode:@"storekit_invalid_payment_object" - message: - @"You have requested a payment for an invalid product. Either the " - @"`productIdentifier` of the payment is not valid or the product has not been " - @"fetched before adding the payment to the payment queue." - details:call.arguments]); - return; - } - SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product]; - payment.applicationUsername = [paymentMap objectForKey:@"applicationUsername"]; - NSNumber *quantity = [paymentMap objectForKey:@"quantity"]; - payment.quantity = (quantity != nil) ? quantity.integerValue : 1; - if (@available(iOS 8.3, *)) { - payment.simulatesAskToBuyInSandbox = - [[paymentMap objectForKey:@"simulatesAskToBuyInSandBox"] boolValue]; - } - - if (![self.paymentQueueHandler addPayment:payment]) { - result([FlutterError - errorWithCode:@"storekit_duplicate_product_object" - message:@"There is a pending transaction for the same product identifier. Please " - @"either wait for it to be finished or finish it manuelly using " - @"`completePurchase` to avoid edge cases." - - details:call.arguments]); - return; - } - result(nil); -} - -- (void)finishTransaction:(FlutterMethodCall *)call result:(FlutterResult)result { - if (![call.arguments isKindOfClass:[NSString class]]) { - result([FlutterError errorWithCode:@"storekit_invalid_argument" - message:@"Argument type of finishTransaction is not a string." - details:call.arguments]); - return; - } - NSString *identifier = call.arguments; - SKPaymentTransaction *transaction = - [self.paymentQueueHandler.transactions objectForKey:identifier]; - if (!transaction) { - result([FlutterError - errorWithCode:@"storekit_platform_invalid_transaction" - message:[NSString - stringWithFormat:@"The transaction with transactionIdentifer:%@ does not " - @"exist. Note that if the transactionState is " - @"purchasing, the transactionIdentifier will be " - @"nil(null).", - transaction.transactionIdentifier] - details:call.arguments]); - return; - } - @try { - // finish transaction will throw exception if the transaction type is purchasing. Notify dart - // about this exception. - [self.paymentQueueHandler finishTransaction:transaction]; - } @catch (NSException *e) { - result([FlutterError errorWithCode:@"storekit_finish_transaction_exception" - message:e.name - details:e.description]); - return; - } - result(nil); -} - -- (void)restoreTransactions:(FlutterMethodCall *)call result:(FlutterResult)result { - if (call.arguments && ![call.arguments isKindOfClass:[NSString class]]) { - result([FlutterError - errorWithCode:@"storekit_invalid_argument" - message:@"Argument is not nil and the type of finishTransaction is not a string." - details:call.arguments]); - return; - } - [self.paymentQueueHandler restoreTransactions:call.arguments]; -} - -- (void)retrieveReceiptData:(FlutterMethodCall *)call result:(FlutterResult)result { - FlutterError *error = nil; - NSString *receiptData = [self.receiptManager retrieveReceiptWithError:&error]; - if (error) { - result(error); - return; - } - result(receiptData); -} - -- (void)refreshReceipt:(FlutterMethodCall *)call result:(FlutterResult)result { - NSDictionary *arguments = call.arguments; - SKReceiptRefreshRequest *request; - if (arguments) { - if (![arguments isKindOfClass:[NSDictionary class]]) { - result([FlutterError errorWithCode:@"storekit_invalid_argument" - message:@"Argument type of startRequest is not array" - details:call.arguments]); - return; - } - NSMutableDictionary *properties = [NSMutableDictionary new]; - properties[SKReceiptPropertyIsExpired] = arguments[@"isExpired"]; - properties[SKReceiptPropertyIsRevoked] = arguments[@"isRevoked"]; - properties[SKReceiptPropertyIsVolumePurchase] = arguments[@"isVolumePurchase"]; - request = [self getRefreshReceiptRequest:properties]; - } else { - request = [self getRefreshReceiptRequest:nil]; - } - FIAPRequestHandler *handler = [[FIAPRequestHandler alloc] initWithRequest:request]; - [self.requestHandlers addObject:handler]; - __weak typeof(self) weakSelf = self; - [handler startProductRequestWithCompletionHandler:^(SKProductsResponse *_Nullable response, - NSError *_Nullable error) { - if (error) { - result([FlutterError errorWithCode:@"storekit_refreshreceiptrequest_platform_error" - message:error.localizedDescription - details:error.description]); - return; - } - result(nil); - [weakSelf.requestHandlers removeObject:handler]; - }]; -} - -#pragma mark - delegates - -- (void)handleTransactionsUpdated:(NSArray *)transactions { - NSMutableArray *maps = [NSMutableArray new]; - for (SKPaymentTransaction *transaction in transactions) { - [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]]; - } - [self.callbackChannel invokeMethod:@"updatedTransactions" arguments:maps]; -} - -- (void)handleTransactionsRemoved:(NSArray *)transactions { - NSMutableArray *maps = [NSMutableArray new]; - for (SKPaymentTransaction *transaction in transactions) { - [maps addObject:[FIAObjectTranslator getMapFromSKPaymentTransaction:transaction]]; - } - [self.callbackChannel invokeMethod:@"removedTransactions" arguments:maps]; -} - -- (void)handleTransactionRestoreFailed:(NSError *)error { - [self.callbackChannel invokeMethod:@"restoreCompletedTransactionsFailed" - arguments:[FIAObjectTranslator getMapFromNSError:error]]; -} - -- (void)restoreCompletedTransactionsFinished { - [self.callbackChannel invokeMethod:@"paymentQueueRestoreCompletedTransactionsFinished" - arguments:nil]; -} - -- (void)updatedDownloads:(NSArray *)downloads { - NSLog(@"Received an updatedDownloads callback, but downloads are not supported."); -} - -- (BOOL)shouldAddStorePayment:(SKPayment *)payment product:(SKProduct *)product { - // We always return NO here. And we send the message to dart to process the payment; and we will - // have a interception method that deciding if the payment should be processed (implemented by the - // programmer). - [self.productsCache setObject:product forKey:product.productIdentifier]; - [self.callbackChannel invokeMethod:@"shouldAddStorePayment" - arguments:@{ - @"payment" : [FIAObjectTranslator getMapFromSKPayment:payment], - @"product" : [FIAObjectTranslator getMapFromSKProduct:product] - }]; - return NO; -} - -#pragma mark - dependency injection (for unit testing) - -- (SKProductsRequest *)getProductRequestWithIdentifiers:(NSSet *)identifiers { - return [[SKProductsRequest alloc] initWithProductIdentifiers:identifiers]; -} - -- (SKProduct *)getProduct:(NSString *)productID { - return [self.productsCache objectForKey:productID]; -} - -- (SKReceiptRefreshRequest *)getRefreshReceiptRequest:(NSDictionary *)properties { - return [[SKReceiptRefreshRequest alloc] initWithReceiptProperties:properties]; -} - -@end 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 e6a18e0acf58..000000000000 --- a/packages/in_app_purchase/ios/Tests/InAppPurchasePluginTest.m +++ /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 -#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)testAddPaymentWithSameProductIDWillFail { - XCTestExpectation* expectation = - [self expectationWithDescription:@"result should return expected error"]; - FlutterMethodCall* call = - [FlutterMethodCall methodCallWithMethodName:@"-[InAppPurchasePlugin addPayment:result:]" - arguments:@{ - @"productIdentifier" : @"123", - @"quantity" : @(1), - @"simulatesAskToBuyInSandBox" : @YES, - }]; - SKPaymentQueueStub* queue = [SKPaymentQueueStub new]; - self.plugin.paymentQueueHandler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray* _Nonnull transactions) { - } - transactionRemoved:nil - restoreTransactionFailed:nil - restoreCompletedTransactionsFinished:nil - shouldAddStorePayment:^BOOL(SKPayment* _Nonnull payment, SKProduct* _Nonnull product) { - return YES; - } - updatedDownloads:nil]; - [queue addTransactionObserver:self.plugin.paymentQueueHandler]; - - FlutterResult addDuplicatePaymentBlock = ^(id r) { - XCTAssertNil(r); - [self.plugin - handleMethodCall:call - result:^(id result) { - XCTAssertNotNil(result); - XCTAssertTrue([result isKindOfClass:[FlutterError class]]); - FlutterError* error = (FlutterError*)result; - XCTAssertEqualObjects(error.code, @"storekit_duplicate_product_object"); - XCTAssertEqualObjects( - error.message, - @"There is a pending transaction for the same product identifier. Please " - @"either wait for it to be finished or finish it manuelly using " - @"`completePurchase` to avoid edge cases."); - [expectation fulfill]; - }]; - }; - [self.plugin handleMethodCall:call result:addDuplicatePaymentBlock]; - [self waitForExpectations:@[ expectation ] timeout:5]; -} - -- (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 2085ba328140..000000000000 --- a/packages/in_app_purchase/ios/Tests/PaymentQueueTest.m +++ /dev/null @@ -1,229 +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); -} - -- (void)testDuplicateTransactionsWillTriggerAnError { - SKPaymentQueueStub *queue = [[SKPaymentQueueStub alloc] init]; - queue.testState = SKPaymentTransactionStatePurchased; - FIAPaymentQueueHandler *handler = [[FIAPaymentQueueHandler alloc] initWithQueue:queue - transactionsUpdated:^(NSArray *_Nonnull transactions) { - } - 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]]; - XCTAssertTrue([handler addPayment:payment]); - XCTAssertFalse([handler addPayment:payment]); -} - -- (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); -} - -- (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); -} - -- (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); -} - -- (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); -} - -- (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(handler.transactions.count, 1); - XCTAssertEqual(transactions.count, 1); - SKPaymentTransaction *transaction = transactions[0]; - [handler finishTransaction:transaction]; - } - transactionRemoved:^(NSArray *_Nonnull transactions) { - XCTAssertEqual(handler.transactions.count, 0); - 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 f5ab95c5e513..000000000000 --- a/packages/in_app_purchase/lib/src/in_app_purchase/app_store_connection.dart +++ /dev/null @@ -1,258 +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)); - return; - } - - String receiptData = await getReceiptData(); - purchaseUpdatedController - .add(transactions.map((SKPaymentTransactionWrapper transaction) { - PurchaseDetails purchaseDetails = - 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; - 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 2079f69dce6c..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. - /// - /// Android Only: 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 e9dca786b4b6..000000000000 --- a/packages/in_app_purchase/lib/src/in_app_purchase/purchase_details.dart +++ /dev/null @@ -1,234 +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); - } - - /// 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); - } -} - -/// 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.dart b/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart deleted file mode 100644 index 49c438e40231..000000000000 --- a/packages/in_app_purchase/lib/src/store_kit_wrappers/sk_payment_queue_wrapper.dart +++ /dev/null @@ -1,350 +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 'dart:async'; -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:in_app_purchase/src/channel.dart'; -import 'package:json_annotation/json_annotation.dart'; -import 'package:flutter/services.dart'; -import 'sk_payment_transaction_wrappers.dart'; -import 'sk_product_wrapper.dart'; - -part 'sk_payment_queue_wrapper.g.dart'; - -/// A wrapper around -/// [`SKPaymentQueue`](https://developer.apple.com/documentation/storekit/skpaymentqueue?language=objc). -/// -/// The payment queue contains payment related operations. It communicates with -/// the App Store and presents a user interface for the user to process and -/// authorize payments. -/// -/// Full information on using `SKPaymentQueue` and processing purchases is -/// available at the [In-App Purchase Programming -/// Guide](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Introduction.html#//apple_ref/doc/uid/TP40008267). -class SKPaymentQueueWrapper { - SKTransactionObserverWrapper _observer; - - /// Returns the default payment queue. - /// - /// We do not support instantiating a custom payment queue, hence the - /// singleton. However, you can override the observer. - factory SKPaymentQueueWrapper() { - return _singleton; - } - - static final SKPaymentQueueWrapper _singleton = SKPaymentQueueWrapper._(); - - SKPaymentQueueWrapper._() { - callbackChannel.setMethodCallHandler(_handleObserverCallbacks); - } - - /// Calls [`-[SKPaymentQueue transactions]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506026-transactions?language=objc) - Future> transactions() async { - return _getTransactionList( - await channel.invokeListMethod('-[SKPaymentQueue transactions]')); - } - - /// Calls [`-[SKPaymentQueue canMakePayments:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506139-canmakepayments?language=objc). - static Future canMakePayments() async => - await channel.invokeMethod('-[SKPaymentQueue canMakePayments:]'); - - /// Sets an observer to listen to all incoming transaction events. - /// - /// This should be called and set as soon as the app launches in order to - /// avoid missing any purchase updates from the App Store. See the - /// documentation on StoreKit's [`-[SKPaymentQueue - /// addTransactionObserver:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506042-addtransactionobserver?language=objc). - void setTransactionObserver(SKTransactionObserverWrapper observer) { - _observer = observer; - } - - /// Posts a payment to the queue. - /// - /// This sends a purchase request to the App Store for confirmation. - /// Transaction updates will be delivered to the set - /// [SkTransactionObserverWrapper]. - /// - /// A couple preconditions need to be met before calling this method. - /// - /// - At least one [SKTransactionObserverWrapper] should have been added to - /// the payment queue using [addTransactionObserver]. - /// - The [payment.productIdentifier] needs to have been previously fetched - /// using [SKRequestMaker.startProductRequest] so that a valid `SKProduct` - /// has been cached in the platform side already. Because of this - /// [payment.productIdentifier] cannot be hardcoded. - /// - /// This method calls StoreKit's [`-[SKPaymentQueue addPayment:]`] - /// (https://developer.apple.com/documentation/storekit/skpaymentqueue/1506036-addpayment?preferredLanguage=occ). - /// - /// Also see [sandbox - /// testing](https://developer.apple.com/apple-pay/sandbox-testing/). - Future addPayment(SKPaymentWrapper payment) async { - assert(_observer != null, - '[in_app_purchase]: Trying to add a payment without an observer. One must be set using `SkPaymentQueueWrapper.setTransactionObserver` before the app launches.'); - Map requestMap = payment.toMap(); - await channel.invokeMethod( - '-[InAppPurchasePlugin addPayment:result:]', - requestMap, - ); - } - - /// Finishes a transaction and removes it from the queue. - /// - /// This method should be called after the given [transaction] has been - /// succesfully processed and its content has been delivered to the user. - /// Transaction status updates are propagated to [SkTransactionObserver]. - /// - /// This will throw a Platform exception if [transaction.transactionState] is - /// [SKPaymentTransactionStateWrapper.purchasing]. - /// - /// This method calls StoreKit's [`-[SKPaymentQueue - /// finishTransaction:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506003-finishtransaction?language=objc). - Future finishTransaction( - SKPaymentTransactionWrapper transaction) async { - await channel.invokeMethod( - '-[InAppPurchasePlugin finishTransaction:result:]', - transaction.payment.productIdentifier); - } - - /// Restore previously purchased transactions. - /// - /// Use this to load previously purchased content on a new device. - /// - /// This call triggers purchase updates on the set - /// [SKTransactionObserverWrapper] for previously made transactions. This will - /// invoke [SKTransactionObserverWrapper.restoreCompletedTransactions], - /// [SKTransactionObserverWrapper.paymentQueueRestoreCompletedTransactionsFinished], - /// and [SKTransactionObserverWrapper.updatedTransaction]. These restored - /// transactions need to be marked complete with [finishTransaction] once the - /// content is delivered, like any other transaction. - /// - /// The `applicationUserName` should match the original - /// [SKPaymentWrapper.applicationUsername] used in [addPayment]. - /// - /// This method either triggers [`-[SKPayment - /// restoreCompletedTransactions]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1506123-restorecompletedtransactions?language=objc) - /// or [`-[SKPayment restoreCompletedTransactionsWithApplicationUsername:]`](https://developer.apple.com/documentation/storekit/skpaymentqueue/1505992-restorecompletedtransactionswith?language=objc) - /// depending on whether the `applicationUserName` is set. - Future restoreTransactions({String applicationUserName}) async { - await channel.invokeMethod( - '-[InAppPurchasePlugin restoreTransactions:result:]', - applicationUserName); - } - - // Triage a method channel call from the platform and triggers the correct observer method. - Future _handleObserverCallbacks(MethodCall call) { - 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.'); - switch (call.method) { - case 'updatedTransactions': - { - final List transactions = - _getTransactionList(call.arguments); - return Future(() { - _observer.updatedTransactions(transactions: transactions); - }); - } - case 'removedTransactions': - { - final List transactions = - _getTransactionList(call.arguments); - return Future(() { - _observer.removedTransactions(transactions: transactions); - }); - } - case 'restoreCompletedTransactionsFailed': - { - SKError error = SKError.fromJson(call.arguments); - return Future(() { - _observer.restoreCompletedTransactionsFailed(error: error); - }); - } - case 'paymentQueueRestoreCompletedTransactionsFinished': - { - return Future(() { - _observer.paymentQueueRestoreCompletedTransactionsFinished(); - }); - } - case 'shouldAddStorePayment': - { - SKPaymentWrapper payment = - SKPaymentWrapper.fromJson(call.arguments['payment']); - SKProductWrapper product = - SKProductWrapper.fromJson(call.arguments['product']); - return Future(() { - if (_observer.shouldAddStorePayment( - payment: payment, product: product) == - true) { - SKPaymentQueueWrapper().addPayment(payment); - } - }); - } - default: - break; - } - return null; - } - - // Get transaction wrapper object list from arguments. - List _getTransactionList(dynamic arguments) { - final List transactions = arguments - .map( - (dynamic map) => SKPaymentTransactionWrapper.fromJson(map)) - .toList(); - return transactions; - } -} - -/// Dart wrapper around StoreKit's -/// [NSError](https://developer.apple.com/documentation/foundation/nserror?language=objc). -@JsonSerializable(nullable: true) -class SKError { - 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); - return _$SKErrorFromJson(map); - } - - /// Error [code](https://developer.apple.com/documentation/foundation/1448136-nserror_codes) - /// as defined in the Cocoa Framework. - final int code; - - /// Error - /// [domain](https://developer.apple.com/documentation/foundation/nscocoaerrordomain?language=objc) - /// as defined in the Cocoa Framework. - 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). - final Map userInfo; - - @override - bool operator ==(Object other) { - if (identical(other, this)) { - return true; - } - if (other.runtimeType != runtimeType) { - return false; - } - final SKError typedOther = other; - return typedOther.code == code && - typedOther.domain == domain && - DeepCollectionEquality.unordered() - .equals(typedOther.userInfo, userInfo); - } - - @override - int get hashCode => hashValues(this.code, this.domain, this.userInfo); -} - -/// Dart wrapper around StoreKit's -/// [SKPayment](https://developer.apple.com/documentation/storekit/skpayment?language=objc). -/// -/// Used as the parameter to initiate a payment. In general, a developer should -/// not need to create the payment object explicitly; instead, use -/// [SKPaymentQueueWrapper.addPayment] directly with a product identifier to -/// initiate a payment. -@JsonSerializable(nullable: true) -class SKPaymentWrapper { - SKPaymentWrapper( - {@required this.productIdentifier, - this.applicationUsername, - this.requestData, - this.quantity = 1, - this.simulatesAskToBuyInSandbox = false}); - - /// Constructs an instance of this from a key value map of data. - /// - /// The map needs to have named string keys with values matching the names and - /// types of all of the members on this class. The `map` parameter must not be - /// null. - factory SKPaymentWrapper.fromJson(Map map) { - assert(map != null); - return _$SKPaymentWrapperFromJson(map); - } - - /// Creates a Map object describes the payment object. - Map toMap() { - return { - 'productIdentifier': productIdentifier, - 'applicationUsername': applicationUsername, - 'requestData': requestData, - 'quantity': quantity, - 'simulatesAskToBuyInSandbox': simulatesAskToBuyInSandbox - }; - } - - /// The id for the product that the payment is for. - final String productIdentifier; - - /// An opaque id for the user's account. - /// - /// Used to help the store detect irregular activity. See - /// [applicationUsername](https://developer.apple.com/documentation/storekit/skpayment/1506116-applicationusername?language=objc) - /// for more details. For example, you can use a one-way hash of the user’s - /// account name on your server. Don’t use the Apple ID for your developer - /// account, the user’s Apple ID, or the user’s plaintext account name on - /// your server. - final String applicationUsername; - - /// Reserved for future use. - /// - /// The value must be null before sending the payment. If the value is not - /// null, the payment will be rejected. - /// - // The iOS Platform provided this property but it is reserved for future use. - // We also provide this property to match the iOS platform. Converted to - // String from NSData from ios platform using UTF8Encoding. The / default is - // null. - final String requestData; - - /// The amount of the product this payment is for. - /// - /// The default is 1. The minimum is 1. The maximum is 10. - final int quantity; - - /// Produces an "ask to buy" flow in the sandbox if set to true. Default is - /// false. - /// - /// See https://developer.apple.com/in-app-purchase/ for a guide on Sandbox - /// testing. - final bool simulatesAskToBuyInSandbox; - - @override - bool operator ==(Object other) { - if (identical(other, this)) { - return true; - } - if (other.runtimeType != runtimeType) { - return false; - } - final SKPaymentWrapper typedOther = other; - return typedOther.productIdentifier == productIdentifier && - typedOther.applicationUsername == applicationUsername && - typedOther.quantity == quantity && - typedOther.simulatesAskToBuyInSandbox == simulatesAskToBuyInSandbox && - typedOther.requestData == requestData; - } - - @override - int get hashCode => hashValues( - this.productIdentifier, - this.applicationUsername, - this.quantity, - this.simulatesAskToBuyInSandbox, - this.requestData); - - @override - String toString() => _$SKPaymentWrapperToJson(this).toString(); -} 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/lib/store_kit_wrappers.dart b/packages/in_app_purchase/lib/store_kit_wrappers.dart deleted file mode 100644 index 12af4ba0a18f..000000000000 --- a/packages/in_app_purchase/lib/store_kit_wrappers.dart +++ /dev/null @@ -1,9 +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/store_kit_wrappers/sk_payment_queue_wrapper.dart'; -export 'src/store_kit_wrappers/sk_payment_transaction_wrappers.dart'; -export 'src/store_kit_wrappers/sk_product_wrapper.dart'; -export 'src/store_kit_wrappers/sk_receipt_manager.dart'; -export 'src/store_kit_wrappers/sk_request_maker.dart'; diff --git a/packages/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/pubspec.yaml deleted file mode 100644 index b2ee79718846..000000000000 --- a/packages/in_app_purchase/pubspec.yaml +++ /dev/null @@ -1,39 +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.3+1 - -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 - e2e: ^0.2.0 - 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 9f963c4c99b7..000000000000 --- a/packages/in_app_purchase/test/in_app_purchase_connection/app_store_connection_test.dart +++ /dev/null @@ -1,439 +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('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, kPurchaseErrorCode); - 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( - payment: SKPaymentWrapper(productIdentifier: id), - transactionState: SKPaymentTransactionStateWrapper.purchasing, - transactionTimeStamp: 123123.121, - transactionIdentifier: id, - 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( - payment: SKPaymentWrapper(productIdentifier: id), - transactionState: SKPaymentTransactionStateWrapper.failed, - transactionTimeStamp: 123123.121, - transactionIdentifier: id, - 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/in_app_purchase_e2e.dart b/packages/in_app_purchase/test/in_app_purchase_e2e.dart deleted file mode 100644 index e167227b4c8f..000000000000 --- a/packages/in_app_purchase/test/in_app_purchase_e2e.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:e2e/e2e.dart'; - -void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('Can create InAppPurchaseConnection instance', - (WidgetTester tester) async { - final InAppPurchaseConnection connection = InAppPurchaseConnection.instance; - expect(connection, isNotNull); - }); -} 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 c8da68ab823a..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.payment.productIdentifier)); - }); - - 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/e2e/.gitignore b/packages/integration_test/.gitignore similarity index 100% rename from packages/e2e/.gitignore rename to packages/integration_test/.gitignore diff --git a/packages/e2e/.metadata b/packages/integration_test/.metadata similarity index 100% rename from packages/e2e/.metadata rename to packages/integration_test/.metadata diff --git a/packages/integration_test/README.md b/packages/integration_test/README.md new file mode 100644 index 000000000000..67f658b56327 --- /dev/null +++ b/packages/integration_test/README.md @@ -0,0 +1,265 @@ +# 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` +and native Android instrumentation testing. + +## Usage + +Add a dependency on the `integration_test` and `flutter_test` package in the +`dev_dependencies` section of `pubspec.yaml`. For plugins, do this in the +`pubspec.yaml` of the example app. + +Create a `integration_test/` directory for your package. In this directory, +create a `_test.dart`, using the following as a starting point to make +assertions. + +Note: You should only use `testWidgets` to declare your tests, or errors will not be reported correctly. + +```dart +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets("failing test example", (WidgetTester tester) async { + expect(2 + 2, equals(5)); + }); +} +``` + +### Driver Entrypoint + +An accompanying driver script will be needed that can be shared across all +integration tests. Create a file named `integration_test.dart` in the +`test_driver/` directory with the following contents: + +```dart +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); +``` + +You can also use different driver scripts to customize the behavior of the app +under test. For example, `FlutterDriver` can also be parameterized with +different [options](https://api.flutter.dev/flutter/flutter_driver/FlutterDriver/connect.html). +See the [extended driver](https://github.com/flutter/flutter/blob/master/packages/integration_test/example/test_driver/extended_integration_test.dart) for an example. + +### Package Structure + +Your package should have a structure that looks like this: + +``` +lib/ + ... +integration_test/ + foo_test.dart + bar_test.dart +test/ + # Other unit tests go here. +test_driver/ + integration_test.dart +``` + +[Example](https://github.com/flutter/plugins/tree/master/packages/integration_test/example) + +## Using Flutter Driver to Run Tests + +These tests can be launched with the `flutter drive` command. + +To run the `integration_test/foo_test.dart` test with the +`test_driver/integration_test.dart` driver, use the following command: + +```sh +flutter drive \ + --driver=test_driver/integration_test.dart \ + --target=integration_test/foo_test.dart +``` + +### Web + +Make sure you have [enabled web support](https://flutter.dev/docs/get-started/web#set-up) +then [download and run](https://flutter.dev/docs/cookbook/testing/integration/introduction#6b-web) +the web driver in another process. + +Use following command to execute the tests: + +```sh +flutter drive \ + --driver=test_driver/integration_test.dart \ + --target=integration_test/foo_test.dart \ + -d web-server +``` + +## Android Device Testing + +Create an instrumentation test file in your application's +**android/app/src/androidTest/java/com/example/myapp/** directory (replacing +com, example, and myapp with values from your app's package name). You can name +this test file `MainActivityTest.java` or another name of your choice. + +```java +package com.example.myapp; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@RunWith(FlutterTestRunner.class) +public class MainActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class, true, false); +} +``` + +Update your application's **myapp/android/app/build.gradle** to make sure it +uses androidx's version of `AndroidJUnitRunner` and has androidx libraries as a +dependency. + +```gradle +android { + ... + defaultConfig { + ... + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } +} + +dependencies { + testImplementation 'junit:junit:4.12' + + // https://developer.android.com/jetpack/androidx/releases/test/#1.2.0 + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' +} +``` + +To run `integration_test/foo_test.dart` on a local Android device (emulated or +physical): + +```sh +./gradlew app:connectedAndroidTest -Ptarget=`pwd`/../integration_test/foo_test.dart +``` + +## Firebase Test Lab + +If this is your first time testing with Firebase Test Lab, you'll need to follow +the guides in the [Firebase test lab +documentation](https://firebase.google.com/docs/test-lab/?gclid=EAIaIQobChMIs5qVwqW25QIV8iCtBh3DrwyUEAAYASAAEgLFU_D_BwE) +to set up a project. + +To run a test on Android devices using Firebase Test Lab, use gradle commands to build an +instrumentation test for Android, after creating `androidTest` as suggested in the last section. + +```bash +pushd android +# flutter build generates files in android/ for building the app +flutter build apk +./gradlew app:assembleAndroidTest +./gradlew app:assembleDebug -Ptarget=.dart +popd +``` + +Upload the build apks Firebase Test Lab, making sure to replace , +, , and with your values. + +```bash +gcloud auth activate-service-account --key-file= +gcloud --quiet config set project +gcloud firebase test android run --type instrumentation \ + --app build/app/outputs/apk/debug/app-debug.apk \ + --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk\ + --timeout 2m \ + --results-bucket= \ + --results-dir= +``` + +You can pass additional parameters on the command line, such as the +devices you want to test on. See +[gcloud firebase test android run](https://cloud.google.com/sdk/gcloud/reference/firebase/test/android/run). + +## iOS Device Testing + +Open `ios/Runner.xcworkspace` in Xcode. Create a test target if you +do not already have one via `File > New > Target...` and select `Unit Testing Bundle`. +Change the `Product Name` to `RunnerTests`. Make sure `Target to be Tested` is set to `Runner` and language is set to `Objective-C`. +Select `Finish`. +Make sure that the **iOS Deployment Target** of `RunnerTests` within the **Build Settings** section is the same as `Runner`. + +Add the new test target to `ios/Podfile` by embedding in the existing `Runner` target. + +```ruby +target 'Runner' do + # Do not change existing lines. + ... + + target 'RunnerTests' do + inherit! :search_paths + end +end +``` + +To build `integration_test/foo_test.dart` from the command line, run: +```sh +flutter build ios --config-only integration_test/foo_test.dart +``` + +In Xcode, add a test file called `RunnerTests.m` (or any name of your choice) to the new target and +replace the file: + +```objective-c +@import XCTest; +@import integration_test; + +INTEGRATION_TEST_IOS_RUNNER(RunnerTests) +``` + +Run `Product > Test` to run the integration tests on your selected device. + +To deploy it to Firebase Test Lab you can follow these steps: + +Execute this script at the root of your Flutter app: + +```sh +output="../build/ios_integ" +product="build/ios_integ/Build/Products" +dev_target="14.3" + +# Pass --simulator if building for the simulator. +flutter build ios integration_test/foo_test.dart --release + +pushd ios +xcodebuild -workspace Runner.xcworkspace -scheme Runner -config Flutter/Release.xcconfig -derivedDataPath $output -sdk iphoneos build-for-testing +popd + +pushd $product +zip -r "ios_tests.zip" "Release-iphoneos" "Runner_iphoneos$dev_target-arm64.xctestrun" +popd +``` + +You can verify locally that your tests are successful by running the following command: + +```sh +xcodebuild test-without-building -xctestrun "build/ios_integ/Build/Products/Runner_iphoneos14.3-arm64.xctestrun" -destination id= +``` + +Once everything is ok, you can upload the resulting zip to Firebase Test Lab (change the model with your values): + +```sh +gcloud firebase test ios run --test "build/ios_integ/ios_tests.zip" --device model=iphone11pro,version=14.1,locale=fr_FR,orientation=portrait +``` diff --git a/packages/ios_platform_images/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 a3dc5a9f2d2c..2ebd1d1d2d7c 100644 --- a/packages/ios_platform_images/CHANGELOG.md +++ b/packages/ios_platform_images/CHANGELOG.md @@ -1,3 +1,32 @@ +## 0.2.0+2 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. + +## 0.2.0+1 + +* Add iOS unit test target. +* Fix repository link in pubspec.yaml. + +## 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. + +## 0.1.2+2 + +* Post-v2 Android embedding cleanups. + +## 0.1.2+1 + +* Remove Android folder from `ios_platform_images`. + ## 0.1.2 * Fix crash when parameter extension is null. diff --git a/packages/ios_platform_images/LICENSE b/packages/ios_platform_images/LICENSE index 18c6ba2786d6..c6823b81eb84 100644 --- a/packages/ios_platform_images/LICENSE +++ b/packages/ios_platform_images/LICENSE @@ -1,27 +1,25 @@ -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: +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. + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +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/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/android/.gitignore b/packages/ios_platform_images/android/.gitignore deleted file mode 100644 index c6cbe562a427..000000000000 --- a/packages/ios_platform_images/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/ios_platform_images/android/build.gradle b/packages/ios_platform_images/android/build.gradle deleted file mode 100644 index b88416a92d02..000000000000 --- a/packages/ios_platform_images/android/build.gradle +++ /dev/null @@ -1,45 +0,0 @@ -group 'io.flutter.ios_platform_images' -version '1.0-SNAPSHOT' - -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.4.2' - } -} - -rootProject.allprojects { - repositories { - google() - jcenter() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 28 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - } - testOptions { - unitTests.includeAndroidResources = true - } -} - -dependencies { - compileOnly 'androidx.annotation:annotation:1.0.0' - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:1.10.19' - testImplementation 'androidx.test:core:1.0.0' - testImplementation 'org.robolectric:robolectric:4.3' -} diff --git a/packages/ios_platform_images/android/gradle.properties b/packages/ios_platform_images/android/gradle.properties deleted file mode 100644 index 38c8d4544ff1..000000000000 --- a/packages/ios_platform_images/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true -android.useAndroidX=true -android.enableJetifier=true diff --git a/packages/ios_platform_images/android/settings.gradle b/packages/ios_platform_images/android/settings.gradle deleted file mode 100644 index 8f3feeb7946f..000000000000 --- a/packages/ios_platform_images/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'ios_platform_images' diff --git a/packages/ios_platform_images/android/src/main/AndroidManifest.xml b/packages/ios_platform_images/android/src/main/AndroidManifest.xml deleted file mode 100644 index b3b546029567..000000000000 --- a/packages/ios_platform_images/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/packages/ios_platform_images/android/src/main/java/io/flutter/ios_platform_images/IosPlatformImagesPlugin.java b/packages/ios_platform_images/android/src/main/java/io/flutter/ios_platform_images/IosPlatformImagesPlugin.java deleted file mode 100644 index 810e81c9801e..000000000000 --- a/packages/ios_platform_images/android/src/main/java/io/flutter/ios_platform_images/IosPlatformImagesPlugin.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.flutter.ios_platform_images; - -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugin.common.PluginRegistry.Registrar; - -public class IosPlatformImagesPlugin implements MethodCallHandler { - public static void registerWith(Registrar registrar) { - MethodChannel channel = - new MethodChannel(registrar.messenger(), "plugins.flutter.io/ios_platform_images"); - final IosPlatformImagesPlugin instance = new IosPlatformImagesPlugin(); - channel.setMethodCallHandler(instance); - } - - @Override - public void onMethodCall(MethodCall call, Result result) { - result.error("AndroidNotSupported", "This plugin is for iOS only.", null); - } -} diff --git a/packages/ios_platform_images/example/android/.gitignore b/packages/ios_platform_images/example/android/.gitignore deleted file mode 100644 index bc2100d8f75e..000000000000 --- a/packages/ios_platform_images/example/android/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -gradle-wrapper.jar -/.gradle -/captures/ -/gradlew -/gradlew.bat -/local.properties -GeneratedPluginRegistrant.java diff --git a/packages/ios_platform_images/example/android/app/bin/build.gradle b/packages/ios_platform_images/example/android/app/bin/build.gradle deleted file mode 100644 index b70ac6686d86..000000000000 --- a/packages/ios_platform_images/example/android/app/bin/build.gradle +++ /dev/null @@ -1,67 +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 plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -android { - compileSdkVersion 28 - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.example.ios_platform_images_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 { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - 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/ios_platform_images/example/android/app/bin/src/debug/AndroidManifest.xml b/packages/ios_platform_images/example/android/app/bin/src/debug/AndroidManifest.xml deleted file mode 100644 index 03bcd3294df0..000000000000 --- a/packages/ios_platform_images/example/android/app/bin/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/packages/ios_platform_images/example/android/app/bin/src/main/AndroidManifest.xml b/packages/ios_platform_images/example/android/app/bin/src/main/AndroidManifest.xml deleted file mode 100644 index a16bfcd7c79f..000000000000 --- a/packages/ios_platform_images/example/android/app/bin/src/main/AndroidManifest.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - diff --git a/packages/ios_platform_images/example/android/app/bin/src/main/kotlin/com/example/ios_platform_images_example/MainActivity.kt b/packages/ios_platform_images/example/android/app/bin/src/main/kotlin/com/example/ios_platform_images_example/MainActivity.kt deleted file mode 100644 index d9926774bb85..000000000000 --- a/packages/ios_platform_images/example/android/app/bin/src/main/kotlin/com/example/ios_platform_images_example/MainActivity.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.ios_platform_images_example - -import android.os.Bundle -import io.flutter.app.FlutterActivity -import io.flutter.plugins.GeneratedPluginRegistrant - -class MainActivity: FlutterActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - GeneratedPluginRegistrant.registerWith(this) - } -} diff --git a/packages/ios_platform_images/example/android/app/bin/src/profile/AndroidManifest.xml b/packages/ios_platform_images/example/android/app/bin/src/profile/AndroidManifest.xml deleted file mode 100644 index 03bcd3294df0..000000000000 --- a/packages/ios_platform_images/example/android/app/bin/src/profile/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/packages/ios_platform_images/example/android/app/build.gradle b/packages/ios_platform_images/example/android/app/build.gradle deleted file mode 100644 index b70ac6686d86..000000000000 --- a/packages/ios_platform_images/example/android/app/build.gradle +++ /dev/null @@ -1,67 +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 plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -android { - compileSdkVersion 28 - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.example.ios_platform_images_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 { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - 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/ios_platform_images/example/android/app/src/debug/AndroidManifest.xml b/packages/ios_platform_images/example/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 03bcd3294df0..000000000000 --- a/packages/ios_platform_images/example/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/packages/ios_platform_images/example/android/app/src/main/AndroidManifest.xml b/packages/ios_platform_images/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index a16bfcd7c79f..000000000000 --- a/packages/ios_platform_images/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - diff --git a/packages/ios_platform_images/example/android/app/src/main/kotlin/com/example/ios_platform_images_example/MainActivity.kt b/packages/ios_platform_images/example/android/app/src/main/kotlin/com/example/ios_platform_images_example/MainActivity.kt deleted file mode 100644 index d9926774bb85..000000000000 --- a/packages/ios_platform_images/example/android/app/src/main/kotlin/com/example/ios_platform_images_example/MainActivity.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.ios_platform_images_example - -import android.os.Bundle -import io.flutter.app.FlutterActivity -import io.flutter.plugins.GeneratedPluginRegistrant - -class MainActivity: FlutterActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - GeneratedPluginRegistrant.registerWith(this) - } -} diff --git a/packages/ios_platform_images/example/android/app/src/profile/AndroidManifest.xml b/packages/ios_platform_images/example/android/app/src/profile/AndroidManifest.xml deleted file mode 100644 index 03bcd3294df0..000000000000 --- a/packages/ios_platform_images/example/android/app/src/profile/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/packages/ios_platform_images/example/android/build.gradle b/packages/ios_platform_images/example/android/build.gradle deleted file mode 100644 index 3100ad2d5553..000000000000 --- a/packages/ios_platform_images/example/android/build.gradle +++ /dev/null @@ -1,31 +0,0 @@ -buildscript { - ext.kotlin_version = '1.3.50' - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -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/ios_platform_images/example/android/gradle.properties b/packages/ios_platform_images/example/android/gradle.properties deleted file mode 100644 index 38c8d4544ff1..000000000000 --- a/packages/ios_platform_images/example/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true -android.useAndroidX=true -android.enableJetifier=true diff --git a/packages/ios_platform_images/example/ios/Flutter/AppFrameworkInfo.plist b/packages/ios_platform_images/example/ios/Flutter/AppFrameworkInfo.plist index 6b4c0f78a785..f2872cf474ee 100644 --- a/packages/ios_platform_images/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/ios_platform_images/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 9.0 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..02e41bc13711 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; @@ -350,7 +457,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -387,7 +494,6 @@ }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -433,7 +539,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -443,7 +549,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; @@ -483,7 +588,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -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..97241b677295 100644 --- a/packages/ios_platform_images/example/pubspec.yaml +++ b/packages/ios_platform_images/example/pubspec.yaml @@ -1,64 +1,28 @@ 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.14.0 <3.0.0" + flutter: ">=2.5.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/ios/ios_platform_images.podspec b/packages/ios_platform_images/ios/ios_platform_images.podspec index 485a0e52ffb3..ccbb9f9bda8a 100644 --- a/packages/ios_platform_images/ios/ios_platform_images.podspec +++ b/packages/ios_platform_images/ios/ios_platform_images.podspec @@ -17,9 +17,9 @@ Downloaded by pub (not CocoaPods). s.documentation_url = 'https://pub.dev/packages/ios_platform_images' s.source_files = 'Classes/**/*' s.dependency 'Flutter' - s.platform = :ios, '8.0' + s.platform = :ios, '9.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.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.swift_version = '5.0' end 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 ad2317deb523..adc8dc08011e 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 -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 +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+ios_platform_images%22 +version: 0.2.0+2 environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.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 b6941dc4f234..f4129f77f5d4 100644 --- a/packages/local_auth/CHANGELOG.md +++ b/packages/local_auth/CHANGELOG.md @@ -1,3 +1,84 @@ +## 1.1.8 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. +* Updated Android lint settings. + +## 1.1.7 + +* Remove references to the Android V1 embedding. + +## 1.1.6 + +* Migrate maven repository from jcenter to mavenCentral. + +## 1.1.5 + +* Updated grammatical errors and inaccurate information in README. + +## 1.1.4 + +* Add debug assertion that `localizedReason` in `LocalAuthentication.authenticateWithBiometrics` must not be empty. + +## 1.1.3 + +* Fix crashes due to threading issues in iOS implementation. + +## 1.1.2 + +* Update Jetpack dependencies to latest stable versions. + +## 1.1.1 + +* Update flutter_plugin_android_lifecycle dependency to 2.0.1 to fix an R8 issue + on some versions. + +## 1.1.0 + +* Migrate to null safety. +* Allow pin, passcode, and pattern authentication with `authenticate` method. +* Fix incorrect error handling switch case fallthrough. +* Update README for Android Integration. +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)). +* **Breaking change**. Parameter names refactored to use the generic `biometric` prefix in place of `fingerprint` in the `AndroidAuthMessages` class + * `fingerprintHint` is now `biometricHint` + * `fingerprintNotRecognized`is now `biometricNotRecognized` + * `fingerprintSuccess`is now `biometricSuccess` + * `fingerprintRequiredTitle` is now `biometricRequiredTitle` + +## 0.6.3+5 + +* Update Flutter SDK constraint. + +## 0.6.3+4 + +* Update Dart SDK constraint in example. + +## 0.6.3+3 + +* Update android compileSdkVersion to 29. + +## 0.6.3+2 + +* Keep handling deprecated Android v1 classes for backward compatibility. + +## 0.6.3+1 + +* Update package:e2e -> package:integration_test + +## 0.6.3 + +* Increase upper range of `package:platform` constraint to allow 3.X versions. + +## 0.6.2+4 + +* Update package:e2e reference to use the local version in the flutter/plugins + repository. + +## 0.6.2+3 + +* Post-v2 Android embedding cleanup. + ## 0.6.2+2 * Update lower bound of dart dependency to 2.1.0. diff --git a/packages/local_auth/LICENSE b/packages/local_auth/LICENSE index c89293372cf3..c6823b81eb84 100644 --- a/packages/local_auth/LICENSE +++ b/packages/local_auth/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/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..dc282e78ced0 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() } } @@ -30,13 +30,30 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' + baseline file("lint-baseline.xml") + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } } } dependencies { - api "androidx.core:core:1.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" + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-inline:3.9.0' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test:rules:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' diff --git a/packages/local_auth/android/gradle.properties b/packages/local_auth/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/local_auth/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/packages/local_auth/android/lint-baseline.xml b/packages/local_auth/android/lint-baseline.xml new file mode 100644 index 000000000000..e89eaadb3e6d --- /dev/null +++ b/packages/local_auth/android/lint-baseline.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + 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 ff0234708f37..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,7 @@ 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; @@ -30,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 +78,13 @@ public class LocalAuthPlugin implements MethodCallHandler, FlutterPlugin, Activi * io.flutter.plugin.common.MethodChannel.MethodCallHandler} to the registrar's {@link * io.flutter.plugin.common.BinaryMessenger}. */ + @SuppressWarnings("deprecation") 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/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java b/packages/local_auth/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java new file mode 100644 index 000000000000..522185fc9dd3 --- /dev/null +++ b/packages/local_auth/android/src/test/java/io/flutter/plugins/localauth/LocalAuthTest.java @@ -0,0 +1,22 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.localauth; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import org.junit.Test; + +public class LocalAuthTest { + @Test + public void isDeviceSupportedReturnsFalse() { + final LocalAuthPlugin plugin = new LocalAuthPlugin(); + final MethodChannel.Result mockResult = mock(MethodChannel.Result.class); + plugin.onMethodCall(new MethodCall("isDeviceSupported", null), mockResult); + verify(mockResult).success(false); + } +} diff --git a/packages/local_auth/example/README.md b/packages/local_auth/example/README.md 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/DartIntegrationTest.java b/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/local_auth/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 deleted file mode 100644 index b2cea8dc57a2..000000000000 --- a/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.flutter.plugins.localauth; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import io.flutter.plugins.localauthexample.EmbeddingV1Activity; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} 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..68c22371d7dd --- /dev/null +++ b/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/FlutterFragmentActivityTest.java @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.localauth; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterFragmentActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class FlutterFragmentActivityTest { + @Rule + public ActivityTestRule rule = + new ActivityTestRule<>(FlutterFragmentActivity.class); +} diff --git a/packages/local_auth/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 9114170aab6f..000000000000 --- a/packages/local_auth/example/android/app/src/androidTest/java/io/flutter/plugins/localauth/MainActivityTest.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.flutter.plugins.localauth; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import io.flutter.plugins.localauthexample.MainActivity; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class MainActivityTest { - @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); -} diff --git a/packages/local_auth/example/android/app/src/main/AndroidManifest.xml b/packages/local_auth/example/android/app/src/main/AndroidManifest.xml index 25d749d29713..8c091772107a 100644 --- a/packages/local_auth/example/android/app/src/main/AndroidManifest.xml +++ b/packages/local_auth/example/android/app/src/main/AndroidManifest.xml @@ -4,8 +4,8 @@ - - + - - + 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 deleted file mode 100644 index 8191bebc3f1b..000000000000 --- a/packages/local_auth/example/android/app/src/main/java/io/flutter/plugins/localauthexample/EmbeddingV1Activity.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.flutter.plugins.localauthexample; - -import android.os.Bundle; -import io.flutter.app.FlutterFragmentActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; - -public class EmbeddingV1Activity extends FlutterFragmentActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); - } -} diff --git a/packages/local_auth/example/android/app/src/main/java/io/flutter/plugins/localauthexample/MainActivity.java b/packages/local_auth/example/android/app/src/main/java/io/flutter/plugins/localauthexample/MainActivity.java deleted file mode 100644 index 87172b03454b..000000000000 --- a/packages/local_auth/example/android/app/src/main/java/io/flutter/plugins/localauthexample/MainActivity.java +++ /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. - -package io.flutter.plugins.localauthexample; - -import io.flutter.embedding.android.FlutterFragmentActivity; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.plugins.localauth.LocalAuthPlugin; - -public class MainActivity extends FlutterFragmentActivity { - // TODO(bparrishMines): Remove this once v2 of GeneratedPluginRegistrant rolls to stable. https://github.com/flutter/flutter/issues/42694 - @Override - public void configureFlutterEngine(FlutterEngine flutterEngine) { - flutterEngine.getPlugins().add(new LocalAuthPlugin()); - } -} 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/Flutter/AppFrameworkInfo.plist b/packages/local_auth/example/ios/Flutter/AppFrameworkInfo.plist index 6c2de8086bcd..3a9c234f96d4 100644 --- a/packages/local_auth/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/local_auth/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 8.0 + 9.0 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..3de4b94f9d5c 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; @@ -362,7 +494,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -372,7 +504,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -413,7 +544,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -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 f6e7f669656f..3aa8fd848057 100644 --- a/packages/local_auth/example/pubspec.yaml +++ b/packages/local_auth/example/pubspec.yaml @@ -1,17 +1,28 @@ name: local_auth_example description: Demonstrates how to use the local_auth plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" dependencies: flutter: sdk: flutter local_auth: + # When depending on this package from a real application you should use: + # local_auth: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. path: ../ dev_dependencies: - e2e: ^0.2.1 + 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/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..c2dc9db25fc8 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,67 @@ - (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 ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&authError]) { + [context evaluatePolicy:kLAPolicyDeviceOwnerAuthentication + localizedReason:arguments[@"localizedReason"] + reply:^(BOOL success, NSError *error) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self handleAuthReplyWithSuccess:success + error:error + flutterArguments:arguments + flutterResult:result]; + }); + }]; + } else { + [self handleErrors:authError flutterArguments:arguments withFlutterResult:result]; + } +} + +- (void)handleAuthReplyWithSuccess:(BOOL)success + error:(NSError *)error + flutterArguments:(NSDictionary *)arguments + flutterResult:(FlutterResult)result { + NSAssert([NSThread isMainThread], @"Response handling must be done on the main thread."); + if (success) { + result(@YES); + } else { + switch (error.code) { + case LAErrorPasscodeNotSet: + case LAErrorTouchIDNotAvailable: + case LAErrorTouchIDNotEnrolled: + case LAErrorTouchIDLockout: + [self handleErrors:error flutterArguments:arguments withFlutterResult:result]; + return; + case LAErrorSystemCancel: + if ([arguments[@"stickyAuth"] boolValue]) { + self->_lastCallArgs = arguments; + self->_lastResult = result; + return; + } + } + result(@NO); + } +} + - (void)handleErrors:(NSError *)authError flutterArguments:(NSDictionary *)arguments withFlutterResult:(FlutterResult)result { diff --git a/packages/local_auth/ios/local_auth.podspec b/packages/local_auth/ios/local_auth.podspec index b411ddd36067..917c4bf2a0eb 100644 --- a/packages/local_auth/ios/local_auth.podspec +++ b/packages/local_auth/ios/local_auth.podspec @@ -17,7 +17,7 @@ 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' } + s.platform = :ios, '9.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/packages/local_auth/lib/auth_strings.dart b/packages/local_auth/lib/auth_strings.dart 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 b43f6b91d80c..4f5ef26d9fb1 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.2+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.8 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" flutter: plugin: @@ -16,19 +21,16 @@ flutter: dependencies: flutter: sdk: flutter - meta: ^1.0.5 - intl: ">=0.15.1 <0.17.0" - platform: ^2.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: - e2e: ^0.2.1 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_e2e.dart b/packages/local_auth/test/local_auth_e2e.dart deleted file mode 100644 index 5e9dc57b73f6..000000000000 --- a/packages/local_auth/test/local_auth_e2e.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:e2e/e2e.dart'; - -import 'package:local_auth/local_auth.dart'; - -void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('canCheckBiometrics', (WidgetTester tester) async { - expect(LocalAuthentication().getAvailableBiometrics(), completion(isList)); - }); -} 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 2344f37484bc..0fe91175cf6b 100644 --- a/packages/package_info/CHANGELOG.md +++ b/packages/package_info/CHANGELOG.md @@ -1,3 +1,46 @@ +## NEXT + +* Remove references to the Android v1 embedding. +* Updated Android lint settings. + +## 2.0.2 + +* Update README to point to Plus Plugins version. + +## 2.0.1 + +* Migrate maven repository from jcenter to mavenCentral. + +## 2.0.0 + +* Migrate to null safety. + +## 0.4.3+4 + +* Ensure `IntegrationTestPlugin` is registered in `example` app, so Firebase Test Lab tests report test results correctly. [Issue](https://github.com/flutter/flutter/issues/74944). + +## 0.4.3+3 + +* Update Flutter SDK constraint. + +## 0.4.3+2 + +* Remove unused `test` dependency. +* Update Dart SDK constraint in example. + +## 0.4.3+1 + +* Update android compileSdkVersion to 29. + +## 0.4.3 + +* Update package:e2e -> package:integration_test + +## 0.4.2 + +* Update package:e2e reference to use the local version in the flutter/plugins + repository. + ## 0.4.1 * Add support for macOS. diff --git a/packages/package_info/LICENSE b/packages/package_info/LICENSE index c89293372cf3..c6823b81eb84 100644 --- a/packages/package_info/LICENSE +++ b/packages/package_info/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/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..e21d911ff490 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 @@ -30,5 +30,19 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } } } diff --git a/packages/package_info/android/gradle.properties b/packages/package_info/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/package_info/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M 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 ca1041467262..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. @@ -14,7 +14,6 @@ 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.Registrar; import java.util.HashMap; import java.util.Map; @@ -24,7 +23,8 @@ public class PackageInfoPlugin implements MethodCallHandler, FlutterPlugin { private MethodChannel methodChannel; /** Plugin registration. */ - public static void registerWith(Registrar registrar) { + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { final PackageInfoPlugin instance = new PackageInfoPlugin(); instance.onAttachedToEngine(registrar.context(), registrar.messenger()); } 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/DartIntegrationTest.java b/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/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 deleted file mode 100644 index 47362ba64a9d..000000000000 --- a/packages/package_info/example/android/app/src/androidTest/java/io/flutter/plugins/packageinfoexample/EmbedderV1ActivityTest.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.packageinfoexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class EmbedderV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbedderV1Activity.class); -} 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 7a1dfdb775f1..fb63f6f8c88b 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,16 +1,18 @@ -// 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.packageinfoexample; import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; +import dev.flutter.plugins.integration_test.FlutterTestRunner; import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; -@RunWith(FlutterRunner.class) +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) public class MainActivityTest { @Rule public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); diff --git a/packages/package_info/example/android/app/src/main/AndroidManifest.xml b/packages/package_info/example/android/app/src/main/AndroidManifest.xml index e4d033e8d8dd..efb42ac02c5c 100644 --- a/packages/package_info/example/android/app/src/main/AndroidManifest.xml +++ b/packages/package_info/example/android/app/src/main/AndroidManifest.xml @@ -3,7 +3,7 @@ - + - - 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 deleted file mode 100644 index a32c50484838..000000000000 --- a/packages/package_info/example/android/app/src/main/java/io/flutter/plugins/packageinfoexample/EmbedderV1Activity.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.packageinfoexample; - -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.packageinfo.PackageInfoPlugin; - -public class EmbedderV1Activity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - PackageInfoPlugin.registerWith( - registrarFor("io.flutter.plugins.packageinfo.PackageInfoPlugin")); - } -} 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 new file mode 100644 index 000000000000..ab8f5f38b472 --- /dev/null +++ b/packages/package_info/example/integration_test/package_info_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. + +// @dart=2.9 + +import 'dart:io'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:package_info/package_info.dart'; +import 'package:package_info_example/main.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('fromPlatform', (WidgetTester tester) async { + final PackageInfo info = await PackageInfo.fromPlatform(); + // These tests are based on the example app. The tests should be updated if any related info changes. + if (Platform.isAndroid) { + expect(info.appName, 'package_info_example'); + expect(info.buildNumber, '1'); + expect(info.packageName, 'io.flutter.plugins.packageinfoexample'); + expect(info.version, '1.0'); + } else if (Platform.isIOS) { + expect(info.appName, 'Package Info Example'); + expect(info.buildNumber, '1'); + expect(info.packageName, 'dev.flutter.plugins.packageInfoExample'); + expect(info.version, '1.0'); + } else if (Platform.isMacOS) { + expect(info.appName, 'Package Info Example'); + expect(info.buildNumber, '1'); + expect(info.packageName, 'dev.flutter.plugins.packageInfoExample'); + expect(info.version, '1.0.0'); + } else { + throw (UnsupportedError('platform not supported')); + } + }); + + testWidgets('example', (WidgetTester tester) async { + await tester.pumpWidget(MyApp()); + await tester.pumpAndSettle(); + if (Platform.isAndroid) { + expect(find.text('package_info_example'), findsOneWidget); + expect(find.text('1'), findsOneWidget); + expect( + find.text('io.flutter.plugins.packageinfoexample'), findsOneWidget); + expect(find.text('1.0'), findsOneWidget); + } else if (Platform.isIOS) { + expect(find.text('Package Info Example'), findsOneWidget); + expect(find.text('1'), findsOneWidget); + expect( + find.text('dev.flutter.plugins.packageInfoExample'), findsOneWidget); + expect(find.text('1.0'), findsOneWidget); + } else if (Platform.isMacOS) { + expect(find.text('Package Info Example'), findsOneWidget); + expect(find.text('1'), findsOneWidget); + expect( + find.text('dev.flutter.plugins.packageInfoExample'), findsOneWidget); + expect(find.text('1.0.0'), findsOneWidget); + } else { + throw (UnsupportedError('platform not supported')); + } + }); +} diff --git a/packages/package_info/example/ios/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..f21209190faa 100644 --- a/packages/package_info/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/package_info/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,10 +9,6 @@ /* 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 */; }; @@ -28,8 +24,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -41,7 +35,6 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 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 = ""; }; @@ -49,7 +42,6 @@ 8F88DBCB0DD2793F05ADE394 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -63,8 +55,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, C824856099B606661DF36830 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -84,9 +74,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, @@ -159,7 +147,6 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 1B10719E4FA771B320770278 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -177,7 +164,7 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; + ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; @@ -217,21 +204,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 1B10719E4FA771B320770278 /* [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 +216,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin"; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -315,7 +287,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 +343,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -437,7 +407,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 +428,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.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/package_info/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16ed0f..919434a6254f 100644 --- a/packages/package_info/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/packages/package_info/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> 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 f6ea2f40527e..9c1c3cd0ad6e 100644 --- a/packages/package_info/example/pubspec.yaml +++ b/packages/package_info/example/pubspec.yaml @@ -1,18 +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: ../ - e2e: "^0.2.1" + 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 new file mode 100644 index 000000000000..6a0e6fa82dbe --- /dev/null +++ b/packages/package_info/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/package_info/example/test_driver/package_info_e2e.dart b/packages/package_info/example/test_driver/package_info_e2e.dart deleted file mode 100644 index d701c8030bd9..000000000000 --- a/packages/package_info/example/test_driver/package_info_e2e.dart +++ /dev/null @@ -1,62 +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:io'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:e2e/e2e.dart'; -import 'package:package_info/package_info.dart'; -import 'package:package_info_example/main.dart'; - -void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('fromPlatform', (WidgetTester tester) async { - final PackageInfo info = await PackageInfo.fromPlatform(); - // These tests are based on the example app. The tests should be updated if any related info changes. - if (Platform.isAndroid) { - expect(info.appName, 'package_info_example'); - expect(info.buildNumber, '1'); - expect(info.packageName, 'io.flutter.plugins.packageinfoexample'); - expect(info.version, '1.0'); - } else if (Platform.isIOS) { - expect(info.appName, 'Package Info Example'); - expect(info.buildNumber, '1'); - expect(info.packageName, 'io.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.version, '1.0.0'); - } else { - throw (UnsupportedError('platform not supported')); - } - }); - - testWidgets('example', (WidgetTester tester) async { - await tester.pumpWidget(MyApp()); - await tester.pumpAndSettle(); - if (Platform.isAndroid) { - expect(find.text('package_info_example'), findsOneWidget); - expect(find.text('1'), findsOneWidget); - expect( - find.text('io.flutter.plugins.packageinfoexample'), findsOneWidget); - expect(find.text('1.0'), findsOneWidget); - } else if (Platform.isIOS) { - expect(find.text('Package Info Example'), findsOneWidget); - expect(find.text('1'), findsOneWidget); - expect( - find.text('io.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); - expect(find.text('1.0.0'), findsOneWidget); - } else { - throw (UnsupportedError('platform not supported')); - } - }); -} diff --git a/packages/package_info/example/test_driver/package_info_e2e_test.dart b/packages/package_info/example/test_driver/package_info_e2e_test.dart deleted file mode 100644 index 1bcd0d37f450..000000000000 --- a/packages/package_info/example/test_driver/package_info_e2e_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 'dart:io'; - -import 'package:flutter_driver/flutter_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); -} 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 bb18407fd862..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.1 +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,10 +29,6 @@ dev_dependencies: sdk: flutter flutter_driver: sdk: flutter - test: any - e2e: "^0.2.1" - 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/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 4a94b4c046b4..764662d5d84b 100644 --- a/packages/path_provider/path_provider/CHANGELOG.md +++ b/packages/path_provider/path_provider/CHANGELOG.md @@ -1,3 +1,117 @@ +## 2.0.5 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. + +## 2.0.4 + +* Updated Android lint settings. +* Specify Java 8 for Android build. + +## 2.0.3 + +* Add iOS unit test target. +* Remove references to the Android V1 embedding. + +## 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. + +## 1.6.18 + +* Keep handling deprecated Android v1 classes for backward compatibility. + +## 1.6.17 + +* Update Windows endorsement verison again, to pick up the fix for + web compilation in projects that include path_provider. + +## 1.6.16 + +* Update Windows endorsement verison + +## 1.6.15 + +* Endorse Windows implementation. +* Remove the need to call disablePathProviderPlatformOverride in tests + +## 1.6.14 + +* Update package:e2e -> package:integration_test + +## 1.6.13 + +* Update package:e2e reference to use the local version in the flutter/plugins + repository. + +## 1.6.12 + +* Fixed a Java lint in a test. + +## 1.6.11 + +* Updated documentation to reflect the need for changes in testing for federated plugins + +## 1.6.10 + +* Linux implementation endorsement + +## 1.6.9 + +* Post-v2 Android embedding cleanups. + ## 1.6.8 * Update lower bound of dart dependency to 2.1.0. diff --git a/packages/path_provider/path_provider/LICENSE b/packages/path_provider/path_provider/LICENSE index 566f5b5e7c78..c6823b81eb84 100644 --- a/packages/path_provider/path_provider/LICENSE +++ b/packages/path_provider/path_provider/LICENSE @@ -1,7 +1,7 @@ -Copyright 2017, the Flutter project 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: +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. @@ -13,14 +13,13 @@ met: contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +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/path_provider/path_provider/README.md b/packages/path_provider/path_provider/README.md index 944137f4631c..47ae6891d294 100644 --- a/packages/path_provider/path_provider/README.md +++ b/packages/path_provider/path_provider/README.md @@ -1,12 +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 and Android. +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 @@ -19,3 +20,10 @@ String appDocPath = appDocDir.path; ``` Please see the example app of this plugin for a full example. + +### Usage in tests + +`path_provider` now uses a `PlatformInterface`, meaning that not all platforms share the a single `PlatformChannel`-based implementation. +With that change, tests should be updated to mock `PathProviderPlatform` rather than `PlatformChannel`. + +See this `path_provider` [test](https://github.com/flutter/plugins/blob/master/packages/path_provider/path_provider/test/path_provider_test.dart) for an example. diff --git a/packages/path_provider/path_provider/android/build.gradle b/packages/path_provider/path_provider/android/build.gradle index 93460e761568..1a22f135fe5a 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 @@ -30,10 +30,29 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } } } dependencies { implementation 'androidx.annotation:annotation:1.1.0' + implementation 'com.google.guava:guava:28.1-android' testImplementation 'junit:junit:4.12' } diff --git a/packages/path_provider/path_provider/android/gradle.properties b/packages/path_provider/path_provider/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/path_provider/path_provider/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M 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 2dbf4f75f12b..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. @@ -7,26 +7,42 @@ import android.content.Context; import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; +import android.os.Handler; +import android.os.Looper; import androidx.annotation.NonNull; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.SettableFuture; +import com.google.common.util.concurrent.ThreadFactoryBuilder; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugin.common.PluginRegistry.Registrar; import io.flutter.util.PathUtils; import java.io.File; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; public class PathProviderPlugin implements FlutterPlugin, MethodCallHandler { private Context context; private MethodChannel channel; + private final Executor uiThreadExecutor = new UiThreadExecutor(); + private final Executor executor = + Executors.newSingleThreadExecutor( + new ThreadFactoryBuilder() + .setNameFormat("path-provider-background-%d") + .setPriority(Thread.NORM_PRIORITY) + .build()); public PathProviderPlugin() {} - public static void registerWith(Registrar registrar) { + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { PathProviderPlugin instance = new PathProviderPlugin(); instance.channel = new MethodChannel(registrar.messenger(), "plugins.flutter.io/path_provider"); instance.context = registrar.context(); @@ -46,28 +62,52 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { channel = null; } + private void executeInBackground(Callable task, Result result) { + final SettableFuture future = SettableFuture.create(); + Futures.addCallback( + future, + new FutureCallback() { + public void onSuccess(T answer) { + result.success(answer); + } + + public void onFailure(Throwable t) { + result.error(t.getClass().getName(), t.getMessage(), null); + } + }, + uiThreadExecutor); + executor.execute( + () -> { + try { + future.set(task.call()); + } catch (Throwable t) { + future.setException(t); + } + }); + } + @Override public void onMethodCall(MethodCall call, @NonNull Result result) { switch (call.method) { case "getTemporaryDirectory": - result.success(getPathProviderTemporaryDirectory()); + executeInBackground(() -> getPathProviderTemporaryDirectory(), result); break; case "getApplicationDocumentsDirectory": - result.success(getPathProviderApplicationDocumentsDirectory()); + executeInBackground(() -> getPathProviderApplicationDocumentsDirectory(), result); break; case "getStorageDirectory": - result.success(getPathProviderStorageDirectory()); + executeInBackground(() -> getPathProviderStorageDirectory(), result); break; case "getExternalCacheDirectories": - result.success(getPathProviderExternalCacheDirectories()); + executeInBackground(() -> getPathProviderExternalCacheDirectories(), result); break; case "getExternalStorageDirectories": final Integer type = call.argument("type"); final String directoryName = StorageDirectoryMapper.androidType(type); - result.success(getPathProviderExternalStorageDirectories(directoryName)); + executeInBackground(() -> getPathProviderExternalStorageDirectories(directoryName), result); break; case "getApplicationSupportDirectory": - result.success(getApplicationSupportDirectory()); + executeInBackground(() -> getApplicationSupportDirectory(), result); break; default: result.notImplemented(); @@ -131,4 +171,13 @@ private List getPathProviderExternalStorageDirectories(String type) { return paths; } + + private static class UiThreadExecutor implements Executor { + private final Handler handler = new Handler(Looper.getMainLooper()); + + @Override + public void execute(Runnable command) { + handler.post(command); + } + } } diff --git a/packages/path_provider/path_provider/android/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 deleted file mode 100644 index cce04b79f516..000000000000 --- a/packages/path_provider/path_provider/example/android/app/src/androidTest/java/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,15 +0,0 @@ - -package io.flutter.plugins.pathprovider; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import io.flutter.plugins.pathproviderexample.EmbeddingV1Activity; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} 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 deleted file mode 100644 index 7bdd449981f5..000000000000 --- a/packages/path_provider/path_provider/example/android/app/src/androidTest/java/MainActivityTest.java +++ /dev/null @@ -1,13 +0,0 @@ - -package io.flutter.plugins.pathprovider; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import io.flutter.plugins.pathproviderexample.MainActivity; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class MainActivityTest { - @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); -} diff --git a/packages/path_provider/path_provider/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/path_provider/path_provider/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/path_provider/path_provider/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/path_provider/path_provider/example/android/app/src/androidTest/java/io/flutter/plugins/pathprovider/MainActivityTest.java b/packages/path_provider/path_provider/example/android/app/src/androidTest/java/io/flutter/plugins/pathprovider/MainActivityTest.java new file mode 100644 index 000000000000..d56458bd753c --- /dev/null +++ b/packages/path_provider/path_provider/example/android/app/src/androidTest/java/io/flutter/plugins/pathprovider/MainActivityTest.java @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.pathprovider; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class MainActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/path_provider/path_provider/example/android/app/src/main/AndroidManifest.xml b/packages/path_provider/path_provider/example/android/app/src/main/AndroidManifest.xml index 9e03a9373e33..df8cee7bc3be 100644 --- a/packages/path_provider/path_provider/example/android/app/src/main/AndroidManifest.xml +++ b/packages/path_provider/path_provider/example/android/app/src/main/AndroidManifest.xml @@ -3,14 +3,8 @@ - - - - + + 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 deleted file mode 100644 index a826af36a9d3..000000000000 --- a/packages/path_provider/path_provider/example/android/app/src/main/java/io/flutter/plugins/pathproviderexample/EmbeddingV1Activity.java +++ /dev/null @@ -1,14 +0,0 @@ - -package io.flutter.plugins.pathproviderexample; - -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; - -public class EmbeddingV1Activity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); - } -} diff --git a/packages/path_provider/path_provider/example/android/app/src/main/java/io/flutter/plugins/pathproviderexample/MainActivity.java b/packages/path_provider/path_provider/example/android/app/src/main/java/io/flutter/plugins/pathproviderexample/MainActivity.java deleted file mode 100644 index 36372960fe9c..000000000000 --- a/packages/path_provider/path_provider/example/android/app/src/main/java/io/flutter/plugins/pathproviderexample/MainActivity.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.pathproviderexample; - -import dev.flutter.plugins.e2e.E2EPlugin; -import io.flutter.embedding.android.FlutterActivity; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.plugins.pathprovider.PathProviderPlugin; - -public class MainActivity extends FlutterActivity { - // TODO(xster): Remove this once v2 of GeneratedPluginRegistrant rolls to stable. https://github.com/flutter/flutter/issues/42694 - @Override - public void configureFlutterEngine(FlutterEngine flutterEngine) { - flutterEngine.getPlugins().add(new PathProviderPlugin()); - flutterEngine.getPlugins().add(new E2EPlugin()); - } -} 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 new file mode 100644 index 000000000000..9f8feee99ee2 --- /dev/null +++ b/packages/path_provider/path_provider/example/integration_test/path_provider_test.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:io'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:path_provider/path_provider.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('getTemporaryDirectory', (WidgetTester tester) async { + final Directory result = await getTemporaryDirectory(); + _verifySampleFile(result, 'temporaryDirectory'); + }); + + testWidgets('getApplicationDocumentsDirectory', (WidgetTester tester) async { + final Directory result = await getApplicationDocumentsDirectory(); + _verifySampleFile(result, 'applicationDocuments'); + }); + + testWidgets('getApplicationSupportDirectory', (WidgetTester tester) async { + final Directory result = await getApplicationSupportDirectory(); + _verifySampleFile(result, 'applicationSupport'); + }); + + testWidgets('getLibraryDirectory', (WidgetTester tester) async { + if (Platform.isIOS) { + final Directory result = await getLibraryDirectory(); + _verifySampleFile(result, 'library'); + } else if (Platform.isAndroid) { + final Future result = getLibraryDirectory(); + expect(result, throwsA(isInstanceOf())); + } + }); + + testWidgets('getExternalStorageDirectory', (WidgetTester tester) async { + if (Platform.isIOS) { + final Future result = getExternalStorageDirectory(); + expect(result, throwsA(isInstanceOf())); + } else if (Platform.isAndroid) { + final Directory? result = await getExternalStorageDirectory(); + _verifySampleFile(result, 'externalStorage'); + } + }); + + testWidgets('getExternalCacheDirectories', (WidgetTester tester) async { + if (Platform.isIOS) { + final Future?> result = getExternalCacheDirectories(); + expect(result, throwsA(isInstanceOf())); + } else if (Platform.isAndroid) { + final List? directories = await getExternalCacheDirectories(); + expect(directories, isNotNull); + for (final Directory result in directories!) { + _verifySampleFile(result, 'externalCache'); + } + } + }); + + final List _allDirs = [ + null, + StorageDirectory.music, + StorageDirectory.podcasts, + StorageDirectory.ringtones, + StorageDirectory.alarms, + StorageDirectory.notifications, + StorageDirectory.pictures, + StorageDirectory.movies, + ]; + + for (final StorageDirectory? type in _allDirs) { + test('getExternalStorageDirectories (type: $type)', () async { + if (Platform.isIOS) { + final Future?> result = + getExternalStorageDirectories(type: null); + expect(result, throwsA(isInstanceOf())); + } else if (Platform.isAndroid) { + final List? directories = + await getExternalStorageDirectories(type: type); + expect(directories, isNotNull); + for (final Directory result in directories!) { + _verifySampleFile(result, '$type'); + } + } + }); + } +} + +/// Verify a file called [name] in [directory] by recreating it with test +/// contents when necessary. +void _verifySampleFile(Directory? directory, String name) { + expect(directory, isNotNull); + if (directory == null) { + return; + } + final File file = File('${directory.path}/$name'); + + if (file.existsSync()) { + file.deleteSync(); + expect(file.existsSync(), isFalse); + } + + file.writeAsStringSync('Hello world!'); + expect(file.readAsStringSync(), 'Hello world!'); + expect(directory.listSync(), isNotEmpty); + file.deleteSync(); +} diff --git a/packages/path_provider/path_provider/example/ios/Flutter/AppFrameworkInfo.plist b/packages/path_provider/path_provider/example/ios/Flutter/AppFrameworkInfo.plist index 6c2de8086bcd..3a9c234f96d4 100644 --- a/packages/path_provider/path_provider/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/path_provider/path_provider/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 8.0 + 9.0 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..86528407809b 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; @@ -362,7 +443,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -372,7 +453,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -413,7 +493,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -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 1d6a50c2ca0f..0001fe580e78 100644 --- a/packages/path_provider/path_provider/example/pubspec.yaml +++ b/packages/path_provider/path_provider/example/pubspec.yaml @@ -1,18 +1,28 @@ name: path_provider_example description: Demonstrates how to use the path_provider plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" 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: - 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/example/test_driver/integration_test.dart b/packages/path_provider/path_provider/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/path_provider/path_provider/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/path_provider/path_provider/example/test_driver/path_provider_e2e.dart b/packages/path_provider/path_provider/example/test_driver/path_provider_e2e.dart deleted file mode 100644 index d3a1019a7c23..000000000000 --- a/packages/path_provider/path_provider/example/test_driver/path_provider_e2e.dart +++ /dev/null @@ -1,104 +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:io'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:e2e/e2e.dart'; - -void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('getTemporaryDirectory', (WidgetTester tester) async { - final Directory result = await getTemporaryDirectory(); - _verifySampleFile(result, 'temporaryDirectory'); - }); - - testWidgets('getApplicationDocumentsDirectory', (WidgetTester tester) async { - final Directory result = await getApplicationDocumentsDirectory(); - _verifySampleFile(result, 'applicationDocuments'); - }); - - testWidgets('getApplicationSupportDirectory', (WidgetTester tester) async { - final Directory result = await getApplicationSupportDirectory(); - _verifySampleFile(result, 'applicationSupport'); - }); - - testWidgets('getLibraryDirectory', (WidgetTester tester) async { - if (Platform.isIOS) { - final Directory result = await getLibraryDirectory(); - _verifySampleFile(result, 'library'); - } else if (Platform.isAndroid) { - final Future result = getLibraryDirectory(); - expect(result, throwsA(isInstanceOf())); - } - }); - - testWidgets('getExternalStorageDirectory', (WidgetTester tester) async { - if (Platform.isIOS) { - final Future result = getExternalStorageDirectory(); - expect(result, throwsA(isInstanceOf())); - } else if (Platform.isAndroid) { - final Directory result = await getExternalStorageDirectory(); - _verifySampleFile(result, 'externalStorage'); - } - }); - - testWidgets('getExternalCacheDirectories', (WidgetTester tester) async { - if (Platform.isIOS) { - final Future> result = getExternalCacheDirectories(); - expect(result, throwsA(isInstanceOf())); - } else if (Platform.isAndroid) { - final List directories = await getExternalCacheDirectories(); - for (Directory result in directories) { - _verifySampleFile(result, 'externalCache'); - } - } - }); - - final List _allDirs = [ - null, - StorageDirectory.music, - StorageDirectory.podcasts, - StorageDirectory.ringtones, - StorageDirectory.alarms, - StorageDirectory.notifications, - StorageDirectory.pictures, - StorageDirectory.movies, - ]; - - for (StorageDirectory type in _allDirs) { - test('getExternalStorageDirectories (type: $type)', () async { - if (Platform.isIOS) { - final Future> result = - getExternalStorageDirectories(type: null); - expect(result, throwsA(isInstanceOf())); - } else if (Platform.isAndroid) { - final List directories = - await getExternalStorageDirectories(type: type); - for (Directory result in directories) { - _verifySampleFile(result, '$type'); - } - } - }); - } -} - -/// Verify a file called [name] in [directory] by recreating it with test -/// contents when necessary. -void _verifySampleFile(Directory directory, String name) { - final File file = File('${directory.path}/$name'); - - if (file.existsSync()) { - file.deleteSync(); - expect(file.existsSync(), isFalse); - } - - file.writeAsStringSync('Hello world!'); - expect(file.readAsStringSync(), 'Hello world!'); - expect(directory.listSync(), isNotEmpty); - file.deleteSync(); -} diff --git a/packages/path_provider/path_provider/example/test_driver/path_provider_e2e_test.dart b/packages/path_provider/path_provider/example/test_driver/path_provider_e2e_test.dart deleted file mode 100644 index f3aa9e218d82..000000000000 --- a/packages/path_provider/path_provider/example/test_driver/path_provider_e2e_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 'dart:async'; -import 'dart:io'; -import 'package:flutter_driver/flutter_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); -} 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/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/ios/path_provider.podspec b/packages/path_provider/path_provider/ios/path_provider.podspec index fcadef593d36..86f27c6c8fa5 100644 --- a/packages/path_provider/path_provider/ios/path_provider.podspec +++ b/packages/path_provider/path_provider/ios/path_provider.podspec @@ -17,7 +17,7 @@ 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' } + s.platform = :ios, '9.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/packages/path_provider/path_provider/lib/path_provider.dart b/packages/path_provider/path_provider/lib/path_provider.dart index a7643264d52a..e690b7f92960 100644 --- a/packages/path_provider/path_provider/lib/path_provider.dart +++ b/packages/path_provider/path_provider/lib/path_provider.dart @@ -1,16 +1,64 @@ -// 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; +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_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; -PathProviderPlatform get _platform => PathProviderPlatform.instance; +@visibleForTesting +@Deprecated('This is no longer necessary, and is now a no-op') +set disablePathProviderPlatformOverride(bool override) {} + +bool _manualDartRegistrationNeeded = true; + +/// An exception thrown when a directory that should always be available on +/// the current platform cannot be obtained. +class MissingPlatformDirectoryException implements Exception { + /// 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 { + // TODO(egarciad): Remove once auto registration lands on Flutter stable. + // https://github.com/flutter/flutter/issues/81421. + if (_manualDartRegistrationNeeded) { + // Only do the initial registration if it hasn't already been overridden + // with a non-default instance. + if (!kIsWeb && PathProviderPlatform.instance is MethodChannelPathProvider) { + if (Platform.isLinux) { + PathProviderPlatform.instance = PathProviderLinux(); + } else if (Platform.isWindows) { + PathProviderPlatform.instance = PathProviderWindows(); + } + } + _manualDartRegistrationNeeded = false; + } + + return PathProviderPlatform.instance; +} /// Path to the temporary directory on the device that is not backed up and is /// suitable for storing caches of downloaded files. @@ -23,10 +71,14 @@ PathProviderPlatform get _platform => PathProviderPlatform.instance; /// 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); } @@ -41,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); @@ -55,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); } @@ -72,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); } @@ -88,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; } @@ -109,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(); } @@ -127,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(); } @@ -143,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 28ac7744ed9c..5e9bc0b0e7c4 100644 --- a/packages/path_provider/path_provider/pubspec.yaml +++ b/packages/path_provider/path_provider/pubspec.yaml @@ -1,8 +1,12 @@ name: path_provider -description: Flutter plugin for getting commonly used locations on the Android & - iOS 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.8 +description: Flutter plugin for getting commonly used locations on host platform file systems, such as the temp and app data directories. +repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 +version: 2.0.5 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" flutter: plugin: @@ -14,26 +18,26 @@ flutter: pluginClass: FLTPathProviderPlugin macos: default_package: path_provider_macos - + linux: + default_package: path_provider_linux + windows: + default_package: path_provider_windows dependencies: flutter: sdk: flutter - path_provider_platform_interface: ^1.0.1 - path_provider_macos: ^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: - e2e: ^0.2.1 + 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_e2e.dart b/packages/path_provider/path_provider/test/path_provider_e2e.dart deleted file mode 100644 index 545671e32b01..000000000000 --- a/packages/path_provider/path_provider/test/path_provider_e2e.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:e2e/e2e.dart'; - -void main() { - E2EWidgetsFlutterBinding.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/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/.gitignore b/packages/path_provider/path_provider_linux/.gitignore new file mode 100644 index 000000000000..e9dc58d3d6e2 --- /dev/null +++ b/packages/path_provider/path_provider_linux/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ diff --git a/packages/path_provider/path_provider_linux/.metadata b/packages/path_provider/path_provider_linux/.metadata new file mode 100644 index 000000000000..9615744e96d1 --- /dev/null +++ b/packages/path_provider/path_provider_linux/.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: e491544588e8d34fdf31d5f840b4649850ef167a + channel: master + +project_type: plugin 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 new file mode 100644 index 000000000000..6f18d0d6ae58 --- /dev/null +++ b/packages/path_provider/path_provider_linux/CHANGELOG.md @@ -0,0 +1,46 @@ +## 2.1.0 + +* Now `getTemporaryPath` returns the value of the `TMPDIR` environment variable primarily. If `TMPDIR` is not set, `/tmp` is returned. + +## 2.0.2 + +* Updated installation instructions in README. + +## 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. + + +## 0.1.0 - NOT PUBLISHED +* This release updates getApplicationSupportPath to use the application ID instead of the executable name. + * No migration is provided, so any older apps that were using this path will now have a different directory. + +## 0.0.1+2 +* This release updates the example to depend on the endorsed plugin rather than relative path + +## 0.0.1+1 +* This updates the readme and pubspec and example to reflect the endorsement of this implementation of `path_provider` + +## 0.0.1 +* The initial implementation of path_provider for Linux + * Implements getApplicationSupportPath, getApplicationDocumentsPath, getDownloadsPath, and getTemporaryPath diff --git a/packages/path_provider/path_provider_linux/LICENSE b/packages/path_provider/path_provider_linux/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/path_provider/path_provider_linux/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/path_provider/path_provider_linux/README.md b/packages/path_provider/path_provider_linux/README.md new file mode 100644 index 000000000000..b0b73dcb0ecd --- /dev/null +++ b/packages/path_provider/path_provider_linux/README.md @@ -0,0 +1,11 @@ +# path\_provider\_linux + +The linux implementation of [`path_provider`]. + +## Usage + +This package is [endorsed][2], which means you can simply use `path_provider` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/path_provider +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/path_provider/path_provider_linux/example/.gitignore b/packages/path_provider/path_provider_linux/example/.gitignore new file mode 100644 index 000000000000..1ba9c339effb --- /dev/null +++ b/packages/path_provider/path_provider_linux/example/.gitignore @@ -0,0 +1,43 @@ +# 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/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Exceptions to above rules. +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/packages/path_provider/path_provider_linux/example/.metadata b/packages/path_provider/path_provider_linux/example/.metadata new file mode 100644 index 000000000000..c0bc9a90268a --- /dev/null +++ b/packages/path_provider/path_provider_linux/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: e491544588e8d34fdf31d5f840b4649850ef167a + channel: master + +project_type: app diff --git a/packages/path_provider/path_provider_linux/example/README.md b/packages/path_provider/path_provider_linux/example/README.md new file mode 100644 index 000000000000..751fe4b811f0 --- /dev/null +++ b/packages/path_provider/path_provider_linux/example/README.md @@ -0,0 +1,16 @@ +# path_provider_linux_example + +Demonstrates how to use the path_provider_linux plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/path_provider/path_provider_linux/example/integration_test/path_provider_test.dart b/packages/path_provider/path_provider_linux/example/integration_test/path_provider_test.dart new file mode 100644 index 000000000000..3bd644f69763 --- /dev/null +++ b/packages/path_provider/path_provider_linux/example/integration_test/path_provider_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 'dart:io'; +import 'package:flutter_test/flutter_test.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 PathProviderLinux provider = PathProviderLinux(); + final String? result = await provider.getTemporaryPath(); + _verifySampleFile(result, 'temporaryDirectory'); + }); + + testWidgets('getDownloadDirectory', (WidgetTester tester) async { + if (!Platform.isLinux) { + return; + } + final PathProviderLinux provider = PathProviderLinux(); + final String? result = await provider.getDownloadsPath(); + _verifySampleFile(result, 'downloadDirectory'); + }); + + testWidgets('getApplicationDocumentsDirectory', (WidgetTester tester) async { + final PathProviderLinux provider = PathProviderLinux(); + final String? result = await provider.getApplicationDocumentsPath(); + _verifySampleFile(result, 'applicationDocuments'); + }); + + testWidgets('getApplicationSupportDirectory', (WidgetTester tester) async { + final PathProviderLinux provider = PathProviderLinux(); + final String? result = await provider.getApplicationSupportPath(); + _verifySampleFile(result, 'applicationSupport'); + }); +} + +/// Verify a file called [name] in [directoryPath] by recreating it with test +/// contents when necessary. +void _verifySampleFile(String? directoryPath, String name) { + expect(directoryPath, isNotNull); + if (directoryPath == null) { + return; + } + final Directory directory = Directory(directoryPath); + final File file = File('${directory.path}${Platform.pathSeparator}$name'); + + if (file.existsSync()) { + file.deleteSync(); + expect(file.existsSync(), isFalse); + } + + file.writeAsStringSync('Hello world!'); + expect(file.readAsStringSync(), 'Hello world!'); + expect(directory.listSync(), isNotEmpty); + file.deleteSync(); +} diff --git a/packages/path_provider/path_provider_linux/example/lib/main.dart b/packages/path_provider/path_provider_linux/example/lib/main.dart new file mode 100644 index 000000000000..d365e6bdeab4 --- /dev/null +++ b/packages/path_provider/path_provider_linux/example/lib/main.dart @@ -0,0 +1,100 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:path_provider_linux/path_provider_linux.dart'; + +void main() { + runApp(MyApp()); +} + +/// Sample app +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State { + String? _tempDirectory = 'Unknown'; + String? _downloadsDirectory = 'Unknown'; + String? _appSupportDirectory = 'Unknown'; + String? _documentsDirectory = 'Unknown'; + final PathProviderLinux _provider = PathProviderLinux(); + + @override + void initState() { + super.initState(); + initDirectories(); + } + + // Platform messages are asynchronous, so we initialize in an async method. + Future initDirectories() async { + String? tempDirectory; + String? downloadsDirectory; + String? appSupportDirectory; + String? documentsDirectory; + // Platform messages may fail, so we use a try/catch PlatformException. + try { + tempDirectory = await _provider.getTemporaryPath(); + } on PlatformException catch (e, stackTrace) { + tempDirectory = 'Failed to get temp directory.'; + print('$tempDirectory $e $stackTrace'); + } + try { + downloadsDirectory = await _provider.getDownloadsPath(); + } on PlatformException catch (e, stackTrace) { + downloadsDirectory = 'Failed to get downloads directory.'; + print('$downloadsDirectory $e $stackTrace'); + } + + try { + documentsDirectory = await _provider.getApplicationDocumentsPath(); + } on PlatformException catch (e, stackTrace) { + documentsDirectory = 'Failed to get documents directory.'; + print('$documentsDirectory $e $stackTrace'); + } + + try { + 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; + } + + setState(() { + _tempDirectory = tempDirectory; + _downloadsDirectory = downloadsDirectory; + _appSupportDirectory = appSupportDirectory; + _documentsDirectory = documentsDirectory; + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Path Provider Linux 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_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 new file mode 100644 index 000000000000..252f3510a789 --- /dev/null +++ b/packages/path_provider/path_provider_linux/example/pubspec.yaml @@ -0,0 +1,30 @@ +name: pathproviderexample +description: Demonstrates how to use the path_provider_linux plugin. +publish_to: "none" + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" + +dependencies: + flutter: + sdk: flutter + + 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_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + +flutter: + uses-material-design: true 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 new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/path_provider/path_provider_linux/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/path_provider/path_provider_linux/lib/path_provider_linux.dart b/packages/path_provider/path_provider_linux/lib/path_provider_linux.dart new file mode 100644 index 000000000000..ab18db69ddfb --- /dev/null +++ b/packages/path_provider/path_provider_linux/lib/path_provider_linux.dart @@ -0,0 +1,62 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; +import 'package:xdg_directories/xdg_directories.dart' as xdg; + +/// The linux implementation of [PathProviderPlatform] +/// +/// This class implements the `package:path_provider` functionality for linux +class PathProviderLinux extends PathProviderPlatform { + /// Constructs an instance of [PathProviderLinux] + PathProviderLinux() : _environment = Platform.environment; + + /// Constructs an instance of [PathProviderLinux] with the given [environment] + @visibleForTesting + PathProviderLinux.private({ + required Map environment, + }) : _environment = environment; + + final Map _environment; + + /// Registers this class as the default instance of [PathProviderPlatform] + static void registerWith() { + PathProviderPlatform.instance = PathProviderLinux(); + } + + @override + Future getTemporaryPath() { + final String environmentTmpDir = _environment['TMPDIR'] ?? ''; + return Future.value( + environmentTmpDir.isEmpty ? '/tmp' : environmentTmpDir, + ); + } + + @override + Future getApplicationSupportPath() async { + final String processName = path.basenameWithoutExtension( + await File('/proc/self/exe').resolveSymbolicLinks()); + final Directory directory = + Directory(path.join(xdg.dataHome.path, processName)); + // Creating the directory if it doesn't exist, because mobile implementations assume the directory exists + if (!directory.existsSync()) { + await directory.create(recursive: true); + } + return directory.path; + } + + @override + Future getApplicationDocumentsPath() { + return Future.value(xdg.getUserDirectory('DOCUMENTS')?.path); + } + + @override + Future getDownloadsPath() { + return Future.value(xdg.getUserDirectory('DOWNLOAD')?.path); + } +} diff --git a/packages/path_provider/path_provider_linux/pubspec.yaml b/packages/path_provider/path_provider_linux/pubspec.yaml new file mode 100644 index 000000000000..f5b7a88ca232 --- /dev/null +++ b/packages/path_provider/path_provider_linux/pubspec.yaml @@ -0,0 +1,29 @@ +name: path_provider_linux +description: Linux implementation of the path_provider plugin +repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_linux +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 +version: 2.1.0 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +flutter: + plugin: + implements: path_provider + platforms: + linux: + dartPluginClass: PathProviderLinux + pluginClass: none + +dependencies: + 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.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 new file mode 100644 index 000000000000..6dd35000a8ea --- /dev/null +++ b/packages/path_provider/path_provider_linux/test/path_provider_linux_test.dart @@ -0,0 +1,51 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +import 'package:flutter_test/flutter_test.dart'; +import 'package:path_provider_linux/path_provider_linux.dart'; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + PathProviderLinux.registerWith(); + + test('registered instance', () { + expect(PathProviderPlatform.instance, isA()); + }); + + test('getTemporaryPath defaults to TMPDIR', () async { + final PathProviderPlatform plugin = PathProviderLinux.private( + environment: {'TMPDIR': '/run/user/0/tmp'}, + ); + expect(await plugin.getTemporaryPath(), '/run/user/0/tmp'); + }); + + test('getTemporaryPath uses fallback if TMPDIR is empty', () async { + final PathProviderPlatform plugin = PathProviderLinux.private( + environment: {'TMPDIR': ''}, + ); + expect(await plugin.getTemporaryPath(), '/tmp'); + }); + + test('getTemporaryPath uses fallback if TMPDIR is unset', () async { + final PathProviderPlatform plugin = PathProviderLinux.private( + environment: {}, + ); + 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 a42f1e29cf60..1d0738c3757a 100644 --- a/packages/path_provider/path_provider_macos/CHANGELOG.md +++ b/packages/path_provider/path_provider_macos/CHANGELOG.md @@ -1,3 +1,46 @@ +# 2.0.2 + +* Add Swift language version to podspec. +* Add native unit tests. +* Updated installation instructions in README. + +## 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. + +## 0.0.4+4 + +* Remove no-op android folder in the example app. + +## 0.0.4+3 + +* Remove Android folder from `path_provider_macos`. + ## 0.0.4+2 * Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). diff --git a/packages/path_provider/path_provider_macos/LICENSE b/packages/path_provider/path_provider_macos/LICENSE index 7b995420294b..c6823b81eb84 100644 --- a/packages/path_provider/path_provider_macos/LICENSE +++ b/packages/path_provider/path_provider_macos/LICENSE @@ -1,27 +1,25 @@ -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: +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. + * 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. +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/path_provider/path_provider_macos/README.md b/packages/path_provider/path_provider_macos/README.md index b97d9e81b7db..00abdf24cd79 100644 --- a/packages/path_provider/path_provider_macos/README.md +++ b/packages/path_provider/path_provider_macos/README.md @@ -1,37 +1,11 @@ -# path_provider_macos +# path\_provider\_macos 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 - -To use this plugin in your Flutter macos app, simply add it as a dependency in -your `pubspec.yaml` alongside the base `path_provider` plugin. - -_(This is only temporary: in the future we hope to make this package an -"endorsed" implementation of `path_provider`, so that it is automatically -included in your Flutter macos app when you depend on `package:path_provider`.)_ - -This is what the above means to your `pubspec.yaml`: - -```yaml -... -dependencies: - ... - path_provider: ^1.5.1 - path_provider_macos: ^0.0.1 - ... -``` - -### Use the plugin +This package is [endorsed][2], which means you can simply use `path_provider` +normally. This package will be automatically included in your app when you do. -Once you have the `path_provider_macos` dependency in your pubspec, you should -be able to use `package:path_provider` as normal. +[1]: https://pub.dev/packages/path_provider +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/path_provider/path_provider_macos/android/build.gradle b/packages/path_provider/path_provider_macos/android/build.gradle deleted file mode 100644 index cc13f0d9e493..000000000000 --- a/packages/path_provider/path_provider_macos/android/build.gradle +++ /dev/null @@ -1,37 +0,0 @@ -group 'com.example.path_provider' -version '1.0' - -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.2.1' - } -} - -rootProject.allprojects { - repositories { - google() - jcenter() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 28 - - defaultConfig { - minSdkVersion 16 - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" - } - lintOptions { - disable 'InvalidPackage' - } - dependencies { - implementation 'androidx.annotation:annotation:1.0.0' - } -} diff --git a/packages/path_provider/path_provider_macos/android/gradle.properties b/packages/path_provider/path_provider_macos/android/gradle.properties deleted file mode 100644 index 2bd6f4fda009..000000000000 --- a/packages/path_provider/path_provider_macos/android/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M - diff --git a/packages/path_provider/path_provider_macos/android/gradle/wrapper/gradle-wrapper.properties b/packages/path_provider/path_provider_macos/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 019065d1d650..000000000000 --- a/packages/path_provider/path_provider_macos/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/path_provider/path_provider_macos/android/settings.gradle b/packages/path_provider/path_provider_macos/android/settings.gradle deleted file mode 100644 index 71bc90768477..000000000000 --- a/packages/path_provider/path_provider_macos/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'path_provider' diff --git a/packages/path_provider/path_provider_macos/android/src/main/AndroidManifest.xml b/packages/path_provider/path_provider_macos/android/src/main/AndroidManifest.xml deleted file mode 100644 index 14f0296045d3..000000000000 --- a/packages/path_provider/path_provider_macos/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/packages/path_provider/path_provider_macos/android/src/main/java/com/example/path_provider/PathProviderPlugin.java b/packages/path_provider/path_provider_macos/android/src/main/java/com/example/path_provider/PathProviderPlugin.java deleted file mode 100644 index b46b1dc03b28..000000000000 --- a/packages/path_provider/path_provider_macos/android/src/main/java/com/example/path_provider/PathProviderPlugin.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.path_provider; - -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugin.common.PluginRegistry.Registrar; - -/** PathProviderPlugin */ -public class PathProviderPlugin implements MethodCallHandler { - /** Plugin registration. */ - public static void registerWith(Registrar registrar) {} - - @Override - public void onMethodCall(MethodCall call, Result result) {} -} 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/android/app/build.gradle b/packages/path_provider/path_provider_macos/example/android/app/build.gradle deleted file mode 100644 index 0404c7203903..000000000000 --- a/packages/path_provider/path_provider_macos/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.pathproviderexample" - minSdkVersion 16 - targetSdkVersion 28 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} - -dependencies { - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test:rules:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' - - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' -} diff --git a/packages/path_provider/path_provider_macos/example/android/app/src/androidTest/java/EmbeddingV1ActivityTest.java b/packages/path_provider/path_provider_macos/example/android/app/src/androidTest/java/EmbeddingV1ActivityTest.java deleted file mode 100644 index cce04b79f516..000000000000 --- a/packages/path_provider/path_provider_macos/example/android/app/src/androidTest/java/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,15 +0,0 @@ - -package io.flutter.plugins.pathprovider; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import io.flutter.plugins.pathproviderexample.EmbeddingV1Activity; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/path_provider/path_provider_macos/example/android/app/src/androidTest/java/MainActivityTest.java b/packages/path_provider/path_provider_macos/example/android/app/src/androidTest/java/MainActivityTest.java deleted file mode 100644 index 7bdd449981f5..000000000000 --- a/packages/path_provider/path_provider_macos/example/android/app/src/androidTest/java/MainActivityTest.java +++ /dev/null @@ -1,13 +0,0 @@ - -package io.flutter.plugins.pathprovider; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import io.flutter.plugins.pathproviderexample.MainActivity; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class MainActivityTest { - @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); -} diff --git a/packages/path_provider/path_provider_macos/example/android/app/src/main/AndroidManifest.xml b/packages/path_provider/path_provider_macos/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index 9e03a9373e33..000000000000 --- a/packages/path_provider/path_provider_macos/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/packages/path_provider/path_provider_macos/example/android/app/src/main/java/io/flutter/plugins/pathproviderexample/EmbeddingV1Activity.java b/packages/path_provider/path_provider_macos/example/android/app/src/main/java/io/flutter/plugins/pathproviderexample/EmbeddingV1Activity.java deleted file mode 100644 index a826af36a9d3..000000000000 --- a/packages/path_provider/path_provider_macos/example/android/app/src/main/java/io/flutter/plugins/pathproviderexample/EmbeddingV1Activity.java +++ /dev/null @@ -1,14 +0,0 @@ - -package io.flutter.plugins.pathproviderexample; - -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; - -public class EmbeddingV1Activity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); - } -} diff --git a/packages/path_provider/path_provider_macos/example/android/app/src/main/java/io/flutter/plugins/pathproviderexample/MainActivity.java b/packages/path_provider/path_provider_macos/example/android/app/src/main/java/io/flutter/plugins/pathproviderexample/MainActivity.java deleted file mode 100644 index 36372960fe9c..000000000000 --- a/packages/path_provider/path_provider_macos/example/android/app/src/main/java/io/flutter/plugins/pathproviderexample/MainActivity.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.pathproviderexample; - -import dev.flutter.plugins.e2e.E2EPlugin; -import io.flutter.embedding.android.FlutterActivity; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.plugins.pathprovider.PathProviderPlugin; - -public class MainActivity extends FlutterActivity { - // TODO(xster): Remove this once v2 of GeneratedPluginRegistrant rolls to stable. https://github.com/flutter/flutter/issues/42694 - @Override - public void configureFlutterEngine(FlutterEngine flutterEngine) { - flutterEngine.getPlugins().add(new PathProviderPlugin()); - flutterEngine.getPlugins().add(new E2EPlugin()); - } -} diff --git a/packages/path_provider/path_provider_macos/example/android/build.gradle b/packages/path_provider/path_provider_macos/example/android/build.gradle deleted file mode 100644 index 541636cc492a..000000000000 --- a/packages/path_provider/path_provider_macos/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/path_provider/path_provider_macos/example/android/gradle.properties b/packages/path_provider/path_provider_macos/example/android/gradle.properties deleted file mode 100644 index 38c8d4544ff1..000000000000 --- a/packages/path_provider/path_provider_macos/example/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true -android.useAndroidX=true -android.enableJetifier=true diff --git a/packages/path_provider/path_provider_macos/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/path_provider/path_provider_macos/example/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index caf54fa2801c..000000000000 --- a/packages/path_provider/path_provider_macos/example/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip diff --git a/packages/path_provider/path_provider_macos/example/android/settings.gradle b/packages/path_provider/path_provider_macos/example/android/settings.gradle deleted file mode 100644 index 6cb349eef1b6..000000000000 --- a/packages/path_provider/path_provider_macos/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 -} \ No newline at end of file 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 new file mode 100644 index 000000000000..09ed8e69be24 --- /dev/null +++ b/packages/path_provider/path_provider_macos/example/integration_test/path_provider_test.dart @@ -0,0 +1,67 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('getTemporaryDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getTemporaryPath(); + _verifySampleFile(result, 'temporaryDirectory'); + }); + + testWidgets('getApplicationDocumentsDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getApplicationDocumentsPath(); + _verifySampleFile(result, 'applicationDocuments'); + }); + + testWidgets('getApplicationSupportDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getApplicationSupportPath(); + _verifySampleFile(result, 'applicationSupport'); + }); + + testWidgets('getLibraryDirectory', (WidgetTester tester) async { + final PathProviderPlatform provider = PathProviderPlatform.instance; + final String? result = await provider.getLibraryPath(); + _verifySampleFile(result, 'library'); + }); + + testWidgets('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 [directoryPath] by recreating it with test +/// contents when necessary. +/// +/// 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(); + expect(file.existsSync(), isFalse); + } + + file.writeAsStringSync('Hello world!'); + expect(file.readAsStringSync(), 'Hello world!'); + expect(directory.listSync(), isNotEmpty); + file.deleteSync(); +} diff --git a/packages/path_provider/path_provider_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/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/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 e3242b83ad99..d8b93545ed53 100644 --- a/packages/path_provider/path_provider_macos/example/pubspec.yaml +++ b/packages/path_provider/path_provider_macos/example/pubspec.yaml @@ -1,18 +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: any 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: - e2e: ^0.2.1 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 new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/path_provider/path_provider_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/path_provider/path_provider_macos/example/test_driver/path_provider_e2e.dart b/packages/path_provider/path_provider_macos/example/test_driver/path_provider_e2e.dart deleted file mode 100644 index 6cb8362b76d5..000000000000 --- a/packages/path_provider/path_provider_macos/example/test_driver/path_provider_e2e.dart +++ /dev/null @@ -1,51 +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:io'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:e2e/e2e.dart'; - -void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('getTemporaryDirectory', (WidgetTester tester) async { - final Directory result = await getTemporaryDirectory(); - _verifySampleFile(result, 'temporaryDirectory'); - }); - - testWidgets('getApplicationDocumentsDirectory', (WidgetTester tester) async { - final Directory result = await getApplicationDocumentsDirectory(); - _verifySampleFile(result, 'applicationDocuments'); - }); - - testWidgets('getApplicationSupportDirectory', (WidgetTester tester) async { - final Directory result = await getApplicationSupportDirectory(); - _verifySampleFile(result, 'applicationSupport'); - }); - - testWidgets('getLibraryDirectory', (WidgetTester tester) async { - if (!Platform.isMacOS) { - return; - } - final Directory result = await getLibraryDirectory(); - _verifySampleFile(result, 'library'); - }); -} - -/// Verify a file called [name] in [directory] by recreating it with test -/// contents when necessary. -void _verifySampleFile(Directory directory, String name) { - final File file = File('${directory.path}/$name'); - - if (file.existsSync()) { - file.deleteSync(); - expect(file.existsSync(), isFalse); - } - - file.writeAsStringSync('Hello world!'); - expect(file.readAsStringSync(), 'Hello world!'); - expect(directory.listSync(), isNotEmpty); - file.deleteSync(); -} diff --git a/packages/path_provider/path_provider_macos/example/test_driver/path_provider_e2e_test.dart b/packages/path_provider/path_provider_macos/example/test_driver/path_provider_e2e_test.dart deleted file mode 100644 index f3aa9e218d82..000000000000 --- a/packages/path_provider/path_provider_macos/example/test_driver/path_provider_e2e_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 'dart:async'; -import 'dart:io'; -import 'package:flutter_driver/flutter_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); -} 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 cb56fc49769c..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,16 +1,6 @@ -// Copyright 2019 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. import FlutterMacOS import Foundation 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 a049af1e6ebd..140e4cde9d58 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+2 -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.2 + +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 605ab6560ac5..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,23 @@ +## 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. + ## 1.0.2 * Update lower bound of dart dependency to 2.1.0. diff --git a/packages/path_provider/path_provider_platform_interface/LICENSE b/packages/path_provider/path_provider_platform_interface/LICENSE index 0c91662b3f2f..c6823b81eb84 100644 --- a/packages/path_provider/path_provider_platform_interface/LICENSE +++ b/packages/path_provider/path_provider_platform_interface/LICENSE @@ -1,27 +1,25 @@ -// Copyright 2020 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/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 d87224717221..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.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 - platform: ^2.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/.gitignore b/packages/path_provider/path_provider_windows/.gitignore new file mode 100644 index 000000000000..53e92cc4181f --- /dev/null +++ b/packages/path_provider/path_provider_windows/.gitignore @@ -0,0 +1,3 @@ +.packages +.flutter-plugins +pubspec.lock 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 new file mode 100644 index 000000000000..953bb894de09 --- /dev/null +++ b/packages/path_provider/path_provider_windows/CHANGELOG.md @@ -0,0 +1,58 @@ +## 2.0.3 + +* Updated installation instructions in README. + +## 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 + fakes that override it. +* export 'folders.dart' rather than importing it, since it's intended to be + public. + +## 0.0.4 + +* Move the actual implementation behind a conditional import, exporting + a stub for platforms that don't support FFI. Fixes web builds in + projects with transitive dependencies on path_provider. + +## 0.0.3 + +* Add missing `pluginClass: none` for compatibilty with stable channel. + +## 0.0.2 + +* README update for endorsement. +* Changed getApplicationSupportPath location. +* Removed getLibraryPath. + +## 0.0.1+2 + +* The initial implementation of path_provider for Windows + * Implements getTemporaryPath, getApplicationSupportPath, getLibraryPath, + getApplicationDocumentsPath and getDownloadsPath. diff --git a/packages/path_provider/path_provider_windows/LICENSE b/packages/path_provider/path_provider_windows/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/path_provider/path_provider_windows/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/path_provider/path_provider_windows/README.md b/packages/path_provider/path_provider_windows/README.md new file mode 100644 index 000000000000..31813edf21d1 --- /dev/null +++ b/packages/path_provider/path_provider_windows/README.md @@ -0,0 +1,11 @@ +# path\_provider\_windows + +The Windows implementation of [`path_provider`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `path_provider` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/path_provider +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/path_provider/path_provider_windows/example/.gitignore b/packages/path_provider/path_provider_windows/example/.gitignore new file mode 100644 index 000000000000..f3c205341e7d --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/.gitignore @@ -0,0 +1,44 @@ +# 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 + +# Exceptions to above rules. +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/packages/path_provider/path_provider_windows/example/.metadata b/packages/path_provider/path_provider_windows/example/.metadata new file mode 100644 index 000000000000..bc654e753a99 --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: f2320c3b7a42bc27e7f038212eed1b01f4269641 + channel: master + +project_type: app diff --git a/packages/path_provider/path_provider_windows/example/README.md b/packages/path_provider/path_provider_windows/example/README.md new file mode 100644 index 000000000000..32f66a86d11d --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/README.md @@ -0,0 +1,8 @@ +# path_provider_windows_example + +Demonstrates how to use the path_provider_windows plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.dev/). diff --git a/packages/path_provider/path_provider_windows/example/integration_test/path_provider_test.dart b/packages/path_provider/path_provider_windows/example/integration_test/path_provider_test.dart new file mode 100644 index 000000000000..a8285963adb6 --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/integration_test/path_provider_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 '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'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('getTemporaryDirectory', (WidgetTester tester) async { + final PathProviderWindows provider = PathProviderWindows(); + final String? result = await provider.getTemporaryPath(); + _verifySampleFile(result, 'temporaryDirectory'); + }); + + testWidgets('getApplicationDocumentsDirectory', (WidgetTester tester) async { + final PathProviderWindows provider = PathProviderWindows(); + final String? result = await provider.getApplicationDocumentsPath(); + _verifySampleFile(result, 'applicationDocuments'); + }); + + testWidgets('getApplicationSupportDirectory', (WidgetTester tester) async { + final PathProviderWindows provider = PathProviderWindows(); + final String? result = await provider.getApplicationSupportPath(); + _verifySampleFile(result, 'applicationSupport'); + }); + + testWidgets('getDownloadsDirectory', (WidgetTester tester) async { + final PathProviderWindows provider = PathProviderWindows(); + 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) { + expect(directoryPath, isNotNull); + if (directoryPath == null) { + return; + } + final Directory directory = Directory(directoryPath); + final File file = File('${directory.path}${Platform.pathSeparator}$name'); + + if (file.existsSync()) { + file.deleteSync(); + expect(file.existsSync(), isFalse); + } + + file.writeAsStringSync('Hello world!'); + expect(file.readAsStringSync(), 'Hello world!'); + expect(directory.listSync(), isNotEmpty); + file.deleteSync(); +} diff --git a/packages/path_provider/path_provider_windows/example/lib/main.dart b/packages/path_provider/path_provider_windows/example/lib/main.dart new file mode 100644 index 000000000000..509292bf7405 --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/lib/main.dart @@ -0,0 +1,91 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; +import 'package:path_provider_windows/path_provider_windows.dart'; + +void main() { + runApp(MyApp()); +} + +/// Sample app +class MyApp extends StatefulWidget { + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State { + String? _tempDirectory = 'Unknown'; + String? _downloadsDirectory = 'Unknown'; + String? _appSupportDirectory = 'Unknown'; + String? _documentsDirectory = 'Unknown'; + + @override + void initState() { + super.initState(); + initDirectories(); + } + + // Platform messages are asynchronous, so we initialize in an async method. + Future initDirectories() async { + String? tempDirectory; + String? downloadsDirectory; + String? appSupportDirectory; + String? documentsDirectory; + final PathProviderWindows provider = PathProviderWindows(); + + 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'; + } + + try { + documentsDirectory = await provider.getApplicationDocumentsPath(); + } catch (exception) { + documentsDirectory = 'Failed to get documents directory: $exception'; + } + + try { + appSupportDirectory = await provider.getApplicationSupportPath(); + } catch (exception) { + appSupportDirectory = 'Failed to get app support directory: $exception'; + } + + setState(() { + _tempDirectory = tempDirectory; + _downloadsDirectory = downloadsDirectory; + _appSupportDirectory = appSupportDirectory; + _documentsDirectory = documentsDirectory; + }); + } + + @override + Widget build(BuildContext context) { + 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_windows/example/pubspec.yaml b/packages/path_provider/path_provider_windows/example/pubspec.yaml new file mode 100644 index 000000000000..26a796fca90c --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/pubspec.yaml @@ -0,0 +1,28 @@ +name: path_provider_example +description: Demonstrates how to use the path_provider plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" + +dependencies: + flutter: + sdk: flutter + 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: + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + pedantic: ^1.10.0 + +flutter: + uses-material-design: true diff --git a/packages/path_provider/path_provider_windows/example/test_driver/integration_test.dart b/packages/path_provider/path_provider_windows/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/path_provider/path_provider_windows/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/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/lib/path_provider_windows.dart b/packages/path_provider/path_provider_windows/lib/path_provider_windows.dart new file mode 100644 index 000000000000..9af55ac2616c --- /dev/null +++ b/packages/path_provider/path_provider_windows/lib/path_provider_windows.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. + +// path_provider_windows is implemented using FFI; export a stub for platforms +// that don't support FFI (e.g., web) to avoid having transitive dependencies +// break web compilation. +export 'src/folders_stub.dart' if (dart.library.ffi) 'src/folders.dart'; +export 'src/path_provider_windows_stub.dart' + if (dart.library.ffi) 'src/path_provider_windows_real.dart'; diff --git a/packages/path_provider/path_provider_windows/lib/src/folders.dart b/packages/path_provider/path_provider_windows/lib/src/folders.dart new file mode 100644 index 000000000000..55def29df2d7 --- /dev/null +++ b/packages/path_provider/path_provider_windows/lib/src/folders.dart @@ -0,0 +1,243 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +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 +/// Windows. +class WindowsKnownFolder { + /// The file system directory that is used to store administrative tools for + /// an individual user. The MMC will save customized consoles to this + /// directory, and it will roam with the user. + static String get AdminTools => FOLDERID_AdminTools; + + /// The file system directory that acts as a staging area for files waiting to + /// be written to a CD. A typical path is C:\Documents and + /// Settings\username\Local Settings\Application Data\Microsoft\CD Burning. + static String get CDBurning => FOLDERID_CDBurning; + + /// The file system directory that contains administrative tools for all users + /// of the computer. + static String get CommonAdminTools => FOLDERID_CommonAdminTools; + + /// The file system directory that contains the directories for the common + /// program groups that appear on the Start menu for all users. A typical path + /// is C:\Documents and Settings\All Users\Start Menu\Programs. + static String get CommonPrograms => FOLDERID_CommonPrograms; + + /// The file system directory that contains the programs and folders that + /// appear on the Start menu for all users. A typical path is C:\Documents and + /// Settings\All Users\Start Menu. + static String get CommonStartMenu => FOLDERID_CommonStartMenu; + + /// The file system directory that contains the programs that appear in the + /// Startup folder for all users. A typical path is C:\Documents and + /// Settings\All Users\Start Menu\Programs\Startup. + static String get CommonStartup => FOLDERID_CommonStartup; + + /// The file system directory that contains the templates that are available + /// to all users. A typical path is C:\Documents and Settings\All + /// Users\Templates. + static String get CommonTemplates => FOLDERID_CommonTemplates; + + /// The virtual folder that represents My Computer, containing everything on + /// the local computer: storage devices, printers, and Control Panel. The + /// folder can also contain mapped network drives. + static String get ComputerFolder => FOLDERID_ComputerFolder; + + /// The virtual folder that represents Network Connections, that contains + /// network and dial-up connections. + static String get ConnectionsFolder => FOLDERID_ConnectionsFolder; + + /// The virtual folder that contains icons for the Control Panel applications. + static String get ControlPanelFolder => FOLDERID_ControlPanelFolder; + + /// The file system directory that serves as a common repository for Internet + /// cookies. A typical path is C:\Documents and Settings\username\Cookies. + static String get Cookies => FOLDERID_Cookies; + + /// The virtual folder that represents the Windows desktop, the root of the + /// namespace. + static String get Desktop => FOLDERID_Desktop; + + /// The virtual folder that represents the My Documents desktop item. + static String get Documents => FOLDERID_Documents; + + /// The file system directory that serves as a repository for Internet + /// downloads. + static String get Downloads => FOLDERID_Downloads; + + /// The file system directory that serves as a common repository for the + /// user's favorite items. A typical path is C:\Documents and + /// Settings\username\Favorites. + static String get Favorites => FOLDERID_Favorites; + + /// A virtual folder that contains fonts. A typical path is C:\Windows\Fonts. + static String get Fonts => FOLDERID_Fonts; + + /// The file system directory that serves as a common repository for Internet + /// history items. + static String get History => FOLDERID_History; + + /// The file system directory that serves as a common repository for temporary + /// Internet files. A typical path is C:\Documents and Settings\username\Local + /// Settings\Temporary Internet Files. + static String get InternetCache => FOLDERID_InternetCache; + + /// A virtual folder for Internet Explorer. + static String get InternetFolder => FOLDERID_InternetFolder; + + /// The file system directory that serves as a data repository for local + /// (nonroaming) applications. A typical path is C:\Documents and + /// Settings\username\Local Settings\Application Data. + static String get LocalAppData => FOLDERID_LocalAppData; + + /// The file system directory that serves as a common repository for music + /// files. A typical path is C:\Documents and Settings\User\My Documents\My + /// Music. + static String get Music => FOLDERID_Music; + + /// A file system directory that contains the link objects that may exist in + /// the My Network Places virtual folder. A typical path is C:\Documents and + /// Settings\username\NetHood. + static String get NetHood => FOLDERID_NetHood; + + /// The folder that represents other computers in your workgroup. + static String get NetworkFolder => FOLDERID_NetworkFolder; + + /// The file system directory that serves as a common repository for image + /// files. A typical path is C:\Documents and Settings\username\My + /// Documents\My Pictures. + static String get Pictures => FOLDERID_Pictures; + + /// The file system directory that contains the link objects that can exist in + /// the Printers virtual folder. A typical path is C:\Documents and + /// Settings\username\PrintHood. + static String get PrintHood => FOLDERID_PrintHood; + + /// The virtual folder that contains installed printers. + static String get PrintersFolder => FOLDERID_PrintersFolder; + + /// The user's profile folder. A typical path is C:\Users\username. + /// Applications should not create files or folders at this level. + static String get Profile => FOLDERID_Profile; + + /// The file system directory that contains application data for all users. A + /// typical path is C:\Documents and Settings\All Users\Application Data. This + /// folder is used for application data that is not user specific. For + /// example, an application can store a spell-check dictionary, a database of + /// clip art, or a log file in the CSIDL_COMMON_APPDATA folder. This + /// information will not roam and is available to anyone using the computer. + static String get ProgramData => FOLDERID_ProgramData; + + /// The Program Files folder. A typical path is C:\Program Files. + static String get ProgramFiles => FOLDERID_ProgramFiles; + + /// The common Program Files folder. A typical path is C:\Program + /// Files\Common. + static String get ProgramFilesCommon => FOLDERID_ProgramFilesCommon; + + /// On 64-bit systems, a link to the common Program Files folder. A typical path is + /// C:\Program Files\Common Files. + static String get ProgramFilesCommonX64 => FOLDERID_ProgramFilesCommonX64; + + /// On 64-bit systems, a link to the 32-bit common Program Files folder. A + /// typical path is C:\Program Files (x86)\Common Files. On 32-bit systems, a + /// link to the Common Program Files folder. + static String get ProgramFilesCommonX86 => FOLDERID_ProgramFilesCommonX86; + + /// On 64-bit systems, a link to the Program Files folder. A typical path is + /// C:\Program Files. + static String get ProgramFilesX64 => FOLDERID_ProgramFilesX64; + + /// On 64-bit systems, a link to the 32-bit Program Files folder. A typical + /// path is C:\Program Files (x86). On 32-bit systems, a link to the Common + /// Program Files folder. + static String get ProgramFilesX86 => FOLDERID_ProgramFilesX86; + + /// The file system directory that contains the user's program groups (which + /// are themselves file system directories). + static String get Programs => FOLDERID_Programs; + + /// The file system directory that contains files and folders that appear on + /// the desktop for all users. A typical path is C:\Documents and Settings\All + /// Users\Desktop. + static String get PublicDesktop => FOLDERID_PublicDesktop; + + /// The file system directory that contains documents that are common to all + /// users. A typical path is C:\Documents and Settings\All Users\Documents. + static String get PublicDocuments => FOLDERID_PublicDocuments; + + /// The file system directory that serves as a repository for music files + /// common to all users. A typical path is C:\Documents and Settings\All + /// Users\Documents\My Music. + static String get PublicMusic => FOLDERID_PublicMusic; + + /// The file system directory that serves as a repository for image files + /// common to all users. A typical path is C:\Documents and Settings\All + /// Users\Documents\My Pictures. + static String get PublicPictures => FOLDERID_PublicPictures; + + /// The file system directory that serves as a repository for video files + /// common to all users. A typical path is C:\Documents and Settings\All + /// Users\Documents\My Videos. + static String get PublicVideos => FOLDERID_PublicVideos; + + /// The file system directory that contains shortcuts to the user's most + /// recently used documents. A typical path is C:\Documents and + /// Settings\username\My Recent Documents. + static String get Recent => FOLDERID_Recent; + + /// The virtual folder that contains the objects in the user's Recycle Bin. + static String get RecycleBinFolder => FOLDERID_RecycleBinFolder; + + /// The file system directory that contains resource data. A typical path is + /// C:\Windows\Resources. + static String get ResourceDir => FOLDERID_ResourceDir; + + /// The file system directory that serves as a common repository for + /// application-specific data. A typical path is C:\Documents and + /// Settings\username\Application Data. + static String get RoamingAppData => FOLDERID_RoamingAppData; + + /// The file system directory that contains Send To menu items. A typical path + /// is C:\Documents and Settings\username\SendTo. + static String get SendTo => FOLDERID_SendTo; + + /// The file system directory that contains Start menu items. A typical path + /// is C:\Documents and Settings\username\Start Menu. + static String get StartMenu => FOLDERID_StartMenu; + + /// The file system directory that corresponds to the user's Startup program + /// group. The system starts these programs whenever the associated user logs + /// on. A typical path is C:\Documents and Settings\username\Start + /// Menu\Programs\Startup. + static String get Startup => FOLDERID_Startup; + + /// The Windows System folder. A typical path is C:\Windows\System32. + static String get System => FOLDERID_System; + + /// The 32-bit Windows System folder. On 32-bit systems, this is typically + /// C:\Windows\system32. On 64-bit systems, this is typically + /// C:\Windows\syswow64. + static String get SystemX86 => FOLDERID_SystemX86; + + /// The file system directory that serves as a common repository for document + /// templates. A typical path is C:\Documents and Settings\username\Templates. + static String get Templates => FOLDERID_Templates; + + /// The file system directory that serves as a common repository for video + /// files. A typical path is C:\Documents and Settings\username\My + /// Documents\My Videos. + static String get Videos => FOLDERID_Videos; + + /// The Windows directory or SYSROOT. This corresponds to the %windir% or + /// %SYSTEMROOT% environment variables. A typical path is C:\Windows. + static String get Windows => FOLDERID_Windows; +} 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 new file mode 100644 index 000000000000..34e9e6118f7d --- /dev/null +++ b/packages/path_provider/path_provider_windows/lib/src/folders_stub.dart @@ -0,0 +1,6 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Stub version of the actual class. +class WindowsKnownFolder {} 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 new file mode 100644 index 000000000000..2b87d51c1c49 --- /dev/null +++ b/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_real.dart @@ -0,0 +1,232 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ffi'; +import 'dart:io'; + +import 'package:ffi/ffi.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; +import 'package:win32/win32.dart'; + +import 'folders.dart'; + +/// Wraps the Win32 VerQueryValue API call. +/// +/// This class exists to allow injecting alternate metadata in tests without +/// building multiple custom test binaries. +@visibleForTesting +class VersionInfoQuerier { + /// Returns the value for [key] in [versionInfo]s English strings section, or + /// null if there is no such entry, or if versionInfo is null. + String? getStringValue(Pointer? versionInfo, String key) { + if (versionInfo == null) { + return null; + } + 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 valueAddress.value.toDartString(); + } finally { + calloc.free(keyPath); + calloc.free(length); + calloc.free(valueAddress); + } + } +} + +/// The Windows implementation of [PathProviderPlatform] +/// +/// 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 Pointer buffer = calloc(MAX_PATH + 1).cast(); + String path; + + try { + final int length = GetTempPath(MAX_PATH, buffer); + + if (length == 0) { + final int error = GetLastError(); + throw WindowsException(error); + } else { + 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(r'\')) { + path = path.substring(0, path.length - 1); + } + } + + // Ensure that the directory exists, since GetTempPath doesn't. + final Directory directory = Directory(path); + if (!directory.existsSync()) { + await directory.create(recursive: true); + } + + return path; + } finally { + calloc.free(buffer); + } + } + + @override + 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 + // skip that step; it's up to the client to decide what to do with the path + // in that case (e.g., using a short path). + if (directory.path.length <= MAX_PATH) { + if (!directory.existsSync()) { + await directory.create(recursive: true); + } + } + return directory.path; + } + + @override + Future getApplicationDocumentsPath() => + getPath(WindowsKnownFolder.Documents); + + @override + 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 Pointer> pathPtrPtr = calloc>(); + final Pointer knownFolderID = calloc()..ref.setGUID(folderID); + + try { + 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); + } + + final String path = pathPtrPtr.value.toDartString(); + return Future.value(path); + } finally { + calloc.free(pathPtrPtr); + calloc.free(knownFolderID); + } + } + + /// Returns the relative path string to append to the root directory returned + /// by Win32 APIs for application storage (such as RoamingAppDir) to get a + /// directory that is unique to the application. + /// + /// The convention is to use company-name\product-name\. This will use that if + /// possible, using the data in the VERSIONINFO resource, with the following + /// fallbacks: + /// - If the company name isn't there, that component will be dropped. + /// - If the product name isn't there, it will use the exe's filename (without + /// extension). + String _getApplicationSpecificSubdirectory() { + String? companyName; + String? productName; + + final Pointer moduleNameBuffer = + calloc(MAX_PATH + 1).cast(); + final Pointer unused = calloc(); + Pointer? infoBuffer; + try { + // Get the module name. + final int moduleNameLength = + GetModuleFileName(0, moduleNameBuffer, MAX_PATH); + if (moduleNameLength == 0) { + final int error = GetLastError(); + throw WindowsException(error); + } + + // From that, load the VERSIONINFO resource + final int infoSize = GetFileVersionInfoSize(moduleNameBuffer, unused); + if (infoSize != 0) { + infoBuffer = calloc(infoSize); + if (GetFileVersionInfo(moduleNameBuffer, 0, infoSize, infoBuffer) == + 0) { + calloc.free(infoBuffer); + infoBuffer = null; + } + } + companyName = _sanitizedDirectoryName( + versionInfoQuerier.getStringValue(infoBuffer, 'CompanyName')); + productName = _sanitizedDirectoryName( + versionInfoQuerier.getStringValue(infoBuffer, 'ProductName')); + + // If there was no product name, use the executable name. + productName ??= + path.basenameWithoutExtension(moduleNameBuffer.toDartString()); + + return companyName != null + ? path.join(companyName, productName) + : productName; + } finally { + calloc.free(moduleNameBuffer); + calloc.free(unused); + if (infoBuffer != null) { + calloc.free(infoBuffer); + } + } + } + + /// Makes [rawString] safe as a directory component. See + /// 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) { + if (rawString == null) { + return null; + } + String sanitized = rawString + // Replace banned characters. + .replaceAll(RegExp(r'[<>:"/\\|?*]'), '_') + // Remove trailing whitespace. + .trimRight() + // Ensure that it does not end with a '.'. + .replaceAll(RegExp(r'[.]+$'), ''); + const int kMaxComponentLength = 255; + if (sanitized.length > kMaxComponentLength) { + sanitized = sanitized.substring(0, kMaxComponentLength); + } + return sanitized.isEmpty ? null : sanitized; + } +} 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 new file mode 100644 index 000000000000..bc851831bf54 --- /dev/null +++ b/packages/path_provider/path_provider_windows/lib/src/path_provider_windows_stub.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 'package:path_provider_platform_interface/path_provider_platform_interface.dart'; + +/// A stub implementation to satisfy compilation of multi-platform packages that +/// depend on path_provider_windows. This should never actually be created. +/// +/// Notably, because path_provider needs to manually register +/// path_provider_windows, anything with a transitive dependency on +/// path_provider will also depend on path_provider_windows, not just at the +/// pubspec level but the code level. +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); + + /// Registers the Windows implementation. + static void registerWith() { + PathProviderPlatform.instance = PathProviderWindows(); + } + + /// Stub; see comment on VersionInfoQuerier. + VersionInfoQuerier versionInfoQuerier = VersionInfoQuerier(); + + /// Match PathProviderWindows so that the analyzer won't report invalid + /// overrides if tests provide fake PathProviderWindows implementations. + Future getPath(String folderID) async => ''; +} + +/// Stub to satisfy the analyzer, which doesn't seem to handle conditional +/// exports correctly. +class VersionInfoQuerier {} diff --git a/packages/path_provider/path_provider_windows/pubspec.yaml b/packages/path_provider/path_provider_windows/pubspec.yaml new file mode 100644 index 000000000000..0353574b6235 --- /dev/null +++ b/packages/path_provider/path_provider_windows/pubspec.yaml @@ -0,0 +1,31 @@ +name: path_provider_windows +description: Windows implementation of the path_provider plugin +repository: https://github.com/flutter/plugins/tree/master/packages/path_provider/path_provider_windows +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+path_provider%22 +version: 2.0.3 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +flutter: + plugin: + implements: path_provider + platforms: + windows: + dartPluginClass: PathProviderWindows + pluginClass: none + +dependencies: + ffi: ^1.0.0 + flutter: + sdk: flutter + 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.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 new file mode 100644 index 000000000000..e977e07d99e6 --- /dev/null +++ b/packages/path_provider/path_provider_windows/test/path_provider_windows_test.dart @@ -0,0 +1,128 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +import '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. +class FakeVersionInfoQuerier implements VersionInfoQuerier { + FakeVersionInfoQuerier(this.responses); + + final Map responses; + + String? getStringValue(Pointer? versionInfo, String key) => + responses[key]; +} + +void main() { + test('registered instance', () { + PathProviderWindows.registerWith(); + expect(PathProviderPlatform.instance, isA()); + }); + + test('getTemporaryPath', () async { + final PathProviderWindows pathProvider = PathProviderWindows(); + expect(await pathProvider.getTemporaryPath(), contains(r'C:\')); + }, skip: !Platform.isWindows); + + test('getApplicationSupportPath with no version info', () async { + final PathProviderWindows pathProvider = PathProviderWindows(); + pathProvider.versionInfoQuerier = + FakeVersionInfoQuerier({}); + final String? path = await pathProvider.getApplicationSupportPath(); + expect(path, contains(r'C:\')); + expect(path, contains(r'AppData')); + // The last path component should be the executable name. + expect(path, endsWith(r'flutter_tester')); + }, skip: !Platform.isWindows); + + test('getApplicationSupportPath with full version info', () async { + final PathProviderWindows pathProvider = PathProviderWindows(); + pathProvider.versionInfoQuerier = FakeVersionInfoQuerier({ + 'CompanyName': 'A Company', + 'ProductName': 'Amazing App', + }); + 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 PathProviderWindows pathProvider = PathProviderWindows(); + pathProvider.versionInfoQuerier = FakeVersionInfoQuerier({ + 'ProductName': 'Amazing App', + }); + 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 PathProviderWindows pathProvider = PathProviderWindows(); + pathProvider.versionInfoQuerier = FakeVersionInfoQuerier({ + 'CompanyName': r'A Company: Name.', + 'ProductName': r'A"/Terrible\|App?*Name', + }); + 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 PathProviderWindows pathProvider = PathProviderWindows(); + pathProvider.versionInfoQuerier = FakeVersionInfoQuerier({ + 'CompanyName': r'..', + 'ProductName': r'Amazing App', + }); + 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 PathProviderWindows pathProvider = PathProviderWindows(); + final String truncatedName = 'A' * 255; + pathProvider.versionInfoQuerier = FakeVersionInfoQuerier({ + 'CompanyName': 'A Company', + 'ProductName': truncatedName * 2, + }); + 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 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 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 1c240eaa901d..af79d119c5f6 100644 --- a/packages/plugin_platform_interface/CHANGELOG.md +++ b/packages/plugin_platform_interface/CHANGELOG.md @@ -1,3 +1,19 @@ +## 2.0.2 + +* Update package description. + +## 2.0.1 + +* Fix `federated flutter plugins` link in the README.md. + +## 2.0.0 + +* Migrate to null safety. + +## 1.0.3 + +* Fix homepage in `pubspec.yaml`. + ## 1.0.2 * Make the pedantic dev_dependency explicit. diff --git a/packages/plugin_platform_interface/LICENSE b/packages/plugin_platform_interface/LICENSE index 03118dc2b39b..c6823b81eb84 100644 --- a/packages/plugin_platform_interface/LICENSE +++ b/packages/plugin_platform_interface/LICENSE @@ -1,27 +1,25 @@ -// 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. \ No newline at end of file +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/plugin_platform_interface/README.md b/packages/plugin_platform_interface/README.md index 9fdbd8a75fe2..2fe44328c7dc 100644 --- a/packages/plugin_platform_interface/README.md +++ b/packages/plugin_platform_interface/README.md @@ -1,6 +1,6 @@ # plugin_platform_interface -This package provides a base class for platform interfaces of [federated flutter plugins](https://fluter.dev/go/federated-plugins). +This package provides a base class for platform interfaces of [federated flutter plugins](https://flutter.dev/go/federated-plugins). Platform implementations should extend their platform interface classes rather than implement it as newly added methods to platform interfaces are not considered as breaking changes. Extending a platform 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 b5f263ca4e69..0b4b1782b526 100644 --- a/packages/plugin_platform_interface/pubspec.yaml +++ b/packages/plugin_platform_interface/pubspec.yaml @@ -1,5 +1,8 @@ name: plugin_platform_interface -description: Reusable base class for Flutter plugin platform interfaces. +description: Reusable base class for platform interfaces of Flutter federated + plugins, to help enforce best practices. +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 +15,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.2 - -homepage: https://github.com/flutter/plugins/plugin_platform_interface +version: 2.0.2 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 + pedantic: ^1.10.0 + test: ^1.16.0 diff --git a/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart b/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart index 0488c20f3efb..967fa79d6dc3 100644 --- a/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart +++ b/packages/plugin_platform_interface/test/plugin_platform_interface_test.dart @@ -1,11 +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:mockito/mockito.dart'; -import 'package:test/test.dart'; - import 'package:plugin_platform_interface/plugin_platform_interface.dart'; +import 'package:test/test.dart'; class SamplePluginPlatform extends PlatformInterface { SamplePluginPlatform() : super(token: _token); diff --git a/packages/quick_actions/CHANGELOG.md b/packages/quick_actions/CHANGELOG.md deleted file mode 100644 index 9c76245a49a5..000000000000 --- a/packages/quick_actions/CHANGELOG.md +++ /dev/null @@ -1,123 +0,0 @@ -## 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 c89293372cf3..000000000000 --- a/packages/quick_actions/LICENSE +++ /dev/null @@ -1,27 +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/gradle.properties b/packages/quick_actions/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/quick_actions/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M 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/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java b/packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java deleted file mode 100644 index c2ebad1ddcdc..000000000000 --- a/packages/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java +++ /dev/null @@ -1,74 +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.quickactions; - -import android.app.Activity; -import android.content.Context; -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.embedding.engine.plugins.activity.ActivityAware; -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.PluginRegistry.Registrar; - -/** QuickActionsPlugin */ -public class QuickActionsPlugin implements FlutterPlugin, ActivityAware { - private static final String CHANNEL_ID = "plugins.flutter.io/quick_actions"; - - private MethodChannel channel; - private MethodCallHandlerImpl handler; - - /** - * Plugin registration. - * - *

      Must be called when the application is created. - */ - public static void registerWith(Registrar registrar) { - final QuickActionsPlugin plugin = new QuickActionsPlugin(); - plugin.setupChannel(registrar.messenger(), registrar.context(), registrar.activity()); - } - - @Override - public void onAttachedToEngine(FlutterPluginBinding binding) { - setupChannel(binding.getBinaryMessenger(), binding.getApplicationContext(), null); - } - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) { - teardownChannel(); - } - - @Override - public void onAttachedToActivity(ActivityPluginBinding binding) { - handler.setActivity(binding.getActivity()); - } - - @Override - public void onDetachedFromActivity() { - handler.setActivity(null); - } - - @Override - public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { - onAttachedToActivity(binding); - } - - @Override - public void onDetachedFromActivityForConfigChanges() { - onDetachedFromActivity(); - } - - private void setupChannel(BinaryMessenger messenger, Context context, Activity activity) { - channel = new MethodChannel(messenger, CHANNEL_ID); - handler = new MethodCallHandlerImpl(context, activity); - channel.setMethodCallHandler(handler); - } - - private void teardownChannel() { - channel.setMethodCallHandler(null); - channel = null; - handler = null; - } -} 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/AndroidManifest.xml b/packages/quick_actions/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index ef39e9c54100..000000000000 --- a/packages/quick_actions/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - - - - - - - - - - - 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 3cbd168e620a..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.app.FlutterActivity; -import io.flutter.plugins.quickactions.QuickActionsPlugin; - -public class EmbeddingV1Activity extends 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 b3e2a08c44b4..000000000000 --- a/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/EmbeddingV1ActivityTest.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.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -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/MainActivityTest.java b/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/MainActivityTest.java deleted file mode 100644 index 8966983f76b3..000000000000 --- a/packages/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/MainActivityTest.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.e2e.FlutterRunner; -import io.flutter.embedding.android.FlutterActivity; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class MainActivityTest { - @Rule - public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); -} diff --git a/packages/quick_actions/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/quick_actions/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4b7b09..000000000000 Binary files a/packages/quick_actions/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/packages/quick_actions/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/quick_actions/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17987b79bb8a..000000000000 Binary files a/packages/quick_actions/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/packages/quick_actions/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/quick_actions/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 09d4391482be..000000000000 Binary files a/packages/quick_actions/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/quick_actions/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/quick_actions/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d34e7a..000000000000 Binary files a/packages/quick_actions/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/quick_actions/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/quick_actions/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372eebdb2..000000000000 Binary files a/packages/quick_actions/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ 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.properties b/packages/quick_actions/example/android/gradle.properties deleted file mode 100644 index 38c8d4544ff1..000000000000 --- a/packages/quick_actions/example/android/gradle.properties +++ /dev/null @@ -1,4 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true -android.useAndroidX=true -android.enableJetifier=true diff --git a/packages/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/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/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 058d208a7690..000000000000 --- a/packages/quick_actions/example/pubspec.yaml +++ /dev/null @@ -1,21 +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 - e2e: ^0.2.0 - 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/quick_actions_e2e.dart b/packages/quick_actions/example/test_driver/quick_actions_e2e.dart deleted file mode 100644 index 41d35b874640..000000000000 --- a/packages/quick_actions/example/test_driver/quick_actions_e2e.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:e2e/e2e.dart'; -import 'package:quick_actions/quick_actions.dart'; - -void main() { - E2EWidgetsFlutterBinding.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/test_driver/quick_actions_e2e_test.dart b/packages/quick_actions/example/test_driver/quick_actions_e2e_test.dart deleted file mode 100644 index f3aa9e218d82..000000000000 --- a/packages/quick_actions/example/test_driver/quick_actions_e2e_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 'dart:async'; -import 'dart:io'; -import 'package:flutter_driver/flutter_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); -} 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/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 c9155b66d796..000000000000 --- a/packages/quick_actions/pubspec.yaml +++ /dev/null @@ -1,31 +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+5 - -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 - e2e: ^0.2.0 - 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/AUTHORS b/packages/quick_actions/quick_actions/AUTHORS new file mode 100644 index 000000000000..0ca697b6a756 --- /dev/null +++ b/packages/quick_actions/quick_actions/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> +Daniel Roek diff --git a/packages/quick_actions/quick_actions/CHANGELOG.md b/packages/quick_actions/quick_actions/CHANGELOG.md new file mode 100644 index 000000000000..d2d628cad428 --- /dev/null +++ b/packages/quick_actions/quick_actions/CHANGELOG.md @@ -0,0 +1,194 @@ +## 0.6.0+7 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. + +## 0.6.0+6 + +* Updated Android lint settings. +* Fix repository link in pubspec.yaml. + +## 0.6.0+5 + +* Support only calling initialize once. + +## 0.6.0+4 + +* Remove references to the Android V1 embedding. + +## 0.6.0+3 + +* Added a `const` constructor for the `QuickActions` class, so the plugin will behave as documented in the sample code mentioned in the [README.md](https://github.com/flutter/plugins/blob/59e16a556e273c2d69189b2dcdfa92d101ea6408/packages/quick_actions/quick_actions/README.md). + +## 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..ec3f84eab4cf --- /dev/null +++ b/packages/quick_actions/quick_actions/android/build.gradle @@ -0,0 +1,57 @@ +group 'io.flutter.plugins.quickactions' +version '1.0-SNAPSHOT' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.3.0' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 29 + + defaultConfig { + minSdkVersion 16 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + lintOptions { + disable 'InvalidPackage' + disable 'GradleDependency' + } + + dependencies { + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-core:3.2.4' + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/packages/quick_actions/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..2d89352f3e09 --- /dev/null +++ b/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/MethodCallHandlerImpl.java @@ -0,0 +1,130 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.quickactions; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.content.res.Resources; +import android.graphics.drawable.Icon; +import android.os.Build; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +class MethodCallHandlerImpl implements MethodChannel.MethodCallHandler { + protected static final String EXTRA_ACTION = "some unique action key"; + private static final String CHANNEL_ID = "plugins.flutter.io/quick_actions"; + + private final Context context; + private Activity activity; + + MethodCallHandlerImpl(Context context, Activity activity) { + this.context = context; + this.activity = activity; + } + + void setActivity(Activity activity) { + this.activity = activity; + } + + @Override + public void onMethodCall(MethodCall call, MethodChannel.Result result) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { + // We already know that this functionality does not work for anything + // lower than API 25 so we chose not to return error. Instead we do nothing. + result.success(null); + return; + } + ShortcutManager shortcutManager = + (ShortcutManager) context.getSystemService(Context.SHORTCUT_SERVICE); + switch (call.method) { + case "setShortcutItems": + List> serializedShortcuts = call.arguments(); + List shortcuts = deserializeShortcuts(serializedShortcuts); + shortcutManager.setDynamicShortcuts(shortcuts); + break; + case "clearShortcutItems": + shortcutManager.removeAllDynamicShortcuts(); + break; + case "getLaunchAction": + if (activity == null) { + result.error( + "quick_action_getlaunchaction_no_activity", + "There is no activity available when launching action", + null); + return; + } + final Intent intent = activity.getIntent(); + final String launchAction = intent.getStringExtra(EXTRA_ACTION); + if (launchAction != null && !launchAction.isEmpty()) { + shortcutManager.reportShortcutUsed(launchAction); + intent.removeExtra(EXTRA_ACTION); + } + result.success(launchAction); + return; + default: + result.notImplemented(); + return; + } + result.success(null); + } + + @TargetApi(Build.VERSION_CODES.N_MR1) + private List deserializeShortcuts(List> shortcuts) { + final List shortcutInfos = new ArrayList<>(); + + for (Map shortcut : shortcuts) { + final String icon = shortcut.get("icon"); + final String type = shortcut.get("type"); + final String title = shortcut.get("localizedTitle"); + final ShortcutInfo.Builder shortcutBuilder = new ShortcutInfo.Builder(context, type); + + final int resourceId = loadResourceId(context, icon); + final Intent intent = getIntentToOpenMainActivity(type); + + if (resourceId > 0) { + shortcutBuilder.setIcon(Icon.createWithResource(context, resourceId)); + } + + final ShortcutInfo shortcutInfo = + shortcutBuilder.setLongLabel(title).setShortLabel(title).setIntent(intent).build(); + shortcutInfos.add(shortcutInfo); + } + return shortcutInfos; + } + + private int loadResourceId(Context context, String icon) { + if (icon == null) { + return 0; + } + final String packageName = context.getPackageName(); + final Resources res = context.getResources(); + final int resourceId = res.getIdentifier(icon, "drawable", packageName); + + if (resourceId == 0) { + return res.getIdentifier(icon, "mipmap", packageName); + } else { + return resourceId; + } + } + + private Intent getIntentToOpenMainActivity(String type) { + final String packageName = context.getPackageName(); + + return context + .getPackageManager() + .getLaunchIntentForPackage(packageName) + .setAction(Intent.ACTION_RUN) + .putExtra(EXTRA_ACTION, type) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); + } +} diff --git a/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java b/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java new file mode 100644 index 000000000000..b2f80ad0a271 --- /dev/null +++ b/packages/quick_actions/quick_actions/android/src/main/java/io/flutter/plugins/quickactions/QuickActionsPlugin.java @@ -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. + +package io.flutter.plugins.quickactions; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Build; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.PluginRegistry.NewIntentListener; + +/** QuickActionsPlugin */ +public class QuickActionsPlugin implements FlutterPlugin, ActivityAware, NewIntentListener { + private static final String CHANNEL_ID = "plugins.flutter.io/quick_actions"; + + private MethodChannel channel; + private MethodCallHandlerImpl handler; + + /** + * Plugin registration. + * + *

      Must be called when the application is created. + */ + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { + final QuickActionsPlugin plugin = new QuickActionsPlugin(); + plugin.setupChannel(registrar.messenger(), registrar.context(), registrar.activity()); + } + + @Override + public void onAttachedToEngine(FlutterPluginBinding binding) { + setupChannel(binding.getBinaryMessenger(), binding.getApplicationContext(), null); + } + + @Override + public void onDetachedFromEngine(FlutterPluginBinding binding) { + teardownChannel(); + } + + @Override + public void onAttachedToActivity(ActivityPluginBinding binding) { + handler.setActivity(binding.getActivity()); + binding.addOnNewIntentListener(this); + onNewIntent(binding.getActivity().getIntent()); + } + + @Override + public void onDetachedFromActivity() { + handler.setActivity(null); + } + + @Override + public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) { + binding.removeOnNewIntentListener(this); + onAttachedToActivity(binding); + } + + @Override + public void onDetachedFromActivityForConfigChanges() { + onDetachedFromActivity(); + } + + @Override + public boolean onNewIntent(Intent intent) { + // Do nothing for anything lower than API 25 as the functionality isn't supported. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) { + return false; + } + // Notify the Dart side if the launch intent has the intent extra relevant to quick actions. + if (intent.hasExtra(MethodCallHandlerImpl.EXTRA_ACTION) && channel != null) { + channel.invokeMethod("launch", intent.getStringExtra(MethodCallHandlerImpl.EXTRA_ACTION)); + } + return false; + } + + private void setupChannel(BinaryMessenger messenger, Context context, Activity activity) { + channel = new MethodChannel(messenger, CHANNEL_ID); + handler = new MethodCallHandlerImpl(context, activity); + channel.setMethodCallHandler(handler); + } + + private void teardownChannel() { + channel.setMethodCallHandler(null); + channel = null; + handler = null; + } +} diff --git a/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java b/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java new file mode 100644 index 000000000000..208a119efafe --- /dev/null +++ b/packages/quick_actions/quick_actions/android/src/test/java/io/flutter/plugins/quickactions/QuickActionsTest.java @@ -0,0 +1,165 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.quickactions; + +import static io.flutter.plugins.quickactions.MethodCallHandlerImpl.EXTRA_ACTION; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.app.Activity; +import android.content.Intent; +import android.os.Build; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.StandardMethodCodec; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.nio.ByteBuffer; +import org.junit.After; +import org.junit.Test; +import org.mockito.internal.util.reflection.FieldSetter; + +public class QuickActionsTest { + private static class TestBinaryMessenger implements BinaryMessenger { + public MethodCall lastMethodCall; + + @Override + public void send(@NonNull String channel, @Nullable ByteBuffer message) { + send(channel, message, null); + } + + @Override + public void send( + @NonNull String channel, + @Nullable ByteBuffer message, + @Nullable final BinaryReply callback) { + if (channel.equals("plugins.flutter.io/quick_actions")) { + lastMethodCall = + StandardMethodCodec.INSTANCE.decodeMethodCall((ByteBuffer) message.position(0)); + } + } + + @Override + public void setMessageHandler(@NonNull String channel, @Nullable BinaryMessageHandler handler) { + // Do nothing. + } + } + + static final int SUPPORTED_BUILD = 25; + static final int UNSUPPORTED_BUILD = 24; + static final String SHORTCUT_TYPE = "action_one"; + + @Test + public void canAttachToEngine() { + final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger(); + final FlutterPluginBinding mockPluginBinding = mock(FlutterPluginBinding.class); + when(mockPluginBinding.getBinaryMessenger()).thenReturn(testBinaryMessenger); + + final QuickActionsPlugin plugin = new QuickActionsPlugin(); + plugin.onAttachedToEngine(mockPluginBinding); + } + + @Test + public void onAttachedToActivity_buildVersionSupported_invokesLaunchMethod() + throws NoSuchFieldException, IllegalAccessException { + // Arrange + final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger(); + final QuickActionsPlugin plugin = new QuickActionsPlugin(); + setUpMessengerAndFlutterPluginBinding(testBinaryMessenger, plugin); + setBuildVersion(SUPPORTED_BUILD); + FieldSetter.setField( + plugin, + QuickActionsPlugin.class.getDeclaredField("handler"), + mock(MethodCallHandlerImpl.class)); + final Intent mockIntent = createMockIntentWithQuickActionExtra(); + final Activity mockMainActivity = mock(Activity.class); + when(mockMainActivity.getIntent()).thenReturn(mockIntent); + final ActivityPluginBinding mockActivityPluginBinding = mock(ActivityPluginBinding.class); + when(mockActivityPluginBinding.getActivity()).thenReturn(mockMainActivity); + + // Act + plugin.onAttachedToActivity(mockActivityPluginBinding); + + // Assert + assertNotNull(testBinaryMessenger.lastMethodCall); + assertEquals(testBinaryMessenger.lastMethodCall.method, "launch"); + assertEquals(testBinaryMessenger.lastMethodCall.arguments, SHORTCUT_TYPE); + } + + @Test + public void onNewIntent_buildVersionUnsupported_doesNotInvokeMethod() + throws NoSuchFieldException, IllegalAccessException { + // Arrange + final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger(); + final QuickActionsPlugin plugin = new QuickActionsPlugin(); + setUpMessengerAndFlutterPluginBinding(testBinaryMessenger, plugin); + setBuildVersion(UNSUPPORTED_BUILD); + final Intent mockIntent = createMockIntentWithQuickActionExtra(); + + // Act + final boolean onNewIntentReturn = plugin.onNewIntent(mockIntent); + + // Assert + assertNull(testBinaryMessenger.lastMethodCall); + assertFalse(onNewIntentReturn); + } + + @Test + public void onNewIntent_buildVersionSupported_invokesLaunchMethod() + throws NoSuchFieldException, IllegalAccessException { + // Arrange + final TestBinaryMessenger testBinaryMessenger = new TestBinaryMessenger(); + final QuickActionsPlugin plugin = new QuickActionsPlugin(); + setUpMessengerAndFlutterPluginBinding(testBinaryMessenger, plugin); + setBuildVersion(SUPPORTED_BUILD); + final Intent mockIntent = createMockIntentWithQuickActionExtra(); + + // Act + final boolean onNewIntentReturn = plugin.onNewIntent(mockIntent); + + // Assert + assertNotNull(testBinaryMessenger.lastMethodCall); + assertEquals(testBinaryMessenger.lastMethodCall.method, "launch"); + assertEquals(testBinaryMessenger.lastMethodCall.arguments, SHORTCUT_TYPE); + assertFalse(onNewIntentReturn); + } + + private void setUpMessengerAndFlutterPluginBinding( + TestBinaryMessenger testBinaryMessenger, QuickActionsPlugin plugin) { + final FlutterPluginBinding mockPluginBinding = mock(FlutterPluginBinding.class); + when(mockPluginBinding.getBinaryMessenger()).thenReturn(testBinaryMessenger); + plugin.onAttachedToEngine(mockPluginBinding); + } + + private Intent createMockIntentWithQuickActionExtra() { + final Intent mockIntent = mock(Intent.class); + when(mockIntent.hasExtra(EXTRA_ACTION)).thenReturn(true); + when(mockIntent.getStringExtra(EXTRA_ACTION)).thenReturn(QuickActionsTest.SHORTCUT_TYPE); + return mockIntent; + } + + private void setBuildVersion(int buildVersion) + throws NoSuchFieldException, IllegalAccessException { + Field buildSdkField = Build.VERSION.class.getField("SDK_INT"); + buildSdkField.setAccessible(true); + final Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(buildSdkField, buildSdkField.getModifiers() & ~Modifier.FINAL); + buildSdkField.set(null, buildVersion); + } + + @After + public void tearDown() throws NoSuchFieldException, IllegalAccessException { + setBuildVersion(0); + } +} diff --git a/packages/quick_actions/quick_actions/example/README.md b/packages/quick_actions/quick_actions/example/README.md 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..485ae5511063 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/android/app/build.gradle @@ -0,0 +1,59 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply 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.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + api 'androidx.test:core:1.2.0' +} diff --git a/packages/path_provider/path_provider_macos/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/path_provider/path_provider_macos/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/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/FlutterActivityTest.java b/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/FlutterActivityTest.java new file mode 100644 index 000000000000..e96548da291a --- /dev/null +++ b/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/FlutterActivityTest.java @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.quickactionsexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class FlutterActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.java b/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.java new file mode 100644 index 000000000000..9d2fed13fc27 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/android/app/src/androidTest/java/io/flutter/plugins/quickactionsexample/QuickActionsTest.java @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.quickactionsexample; + +import static org.junit.Assert.assertTrue; + +import androidx.test.core.app.ActivityScenario; +import io.flutter.plugins.quickactions.QuickActionsPlugin; +import org.junit.Test; + +public class QuickActionsTest { + @Test + public void imagePickerPluginIsAdded() { + final ActivityScenario scenario = + ActivityScenario.launch(QuickActionsTestActivity.class); + scenario.onActivity( + activity -> { + assertTrue(activity.engine.getPlugins().has(QuickActionsPlugin.class)); + }); + } +} diff --git a/packages/quick_actions/quick_actions/example/android/app/src/debug/AndroidManifest.xml b/packages/quick_actions/quick_actions/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..bee689df1735 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/packages/quick_actions/quick_actions/example/android/app/src/main/AndroidManifest.xml b/packages/quick_actions/quick_actions/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..4f384b7c6b13 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + diff --git a/packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/QuickActionsTestActivity.java b/packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/QuickActionsTestActivity.java new file mode 100644 index 000000000000..4ff3a27cd5c0 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/android/app/src/main/java/io/flutter/plugins/quickactionsexample/QuickActionsTestActivity.java @@ -0,0 +1,20 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.quickactionsexample; + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; + +// Makes the FlutterEngine accessible for testing. +public class QuickActionsTestActivity extends FlutterActivity { + public FlutterEngine engine; + + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + engine = flutterEngine; + } +} diff --git a/packages/quick_actions/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/in_app_purchase/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/in_app_purchase/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/in_app_purchase/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/in_app_purchase/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/in_app_purchase/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/in_app_purchase/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/in_app_purchase/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/in_app_purchase/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/in_app_purchase/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/in_app_purchase/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/espresso/android/gradle.properties b/packages/quick_actions/quick_actions/example/android/gradle.properties similarity index 100% rename from packages/espresso/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/quick_actions/example/android/settings.gradle b/packages/quick_actions/quick_actions/example/android/settings.gradle similarity index 100% rename from packages/quick_actions/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/quick_actions/quick_actions/example/ios/Flutter/AppFrameworkInfo.plist b/packages/quick_actions/quick_actions/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..3a9c234f96d4 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + UIRequiredDeviceCapabilities + + arm64 + + MinimumOSVersion + 9.0 + + diff --git a/packages/connectivity/connectivity_macos/example/ios/Flutter/Debug.xcconfig b/packages/quick_actions/quick_actions/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/connectivity/connectivity_macos/example/ios/Flutter/Debug.xcconfig rename to packages/quick_actions/quick_actions/example/ios/Flutter/Debug.xcconfig diff --git a/packages/connectivity/connectivity_macos/example/ios/Flutter/Release.xcconfig b/packages/quick_actions/quick_actions/example/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/connectivity/connectivity_macos/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..3924e59aa0f9 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/ios/Podfile @@ -0,0 +1,41 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '9.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj b/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..d6cb74d0658b --- /dev/null +++ b/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,731 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 33E20B3526EFCDFC00A4A191 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33E20B3426EFCDFC00A4A191 /* RunnerTests.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 50EB54C1FE43DB743F5DEC7C /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = D1A69703A518C37D73BF8B91 /* libPods-RunnerTests.a */; }; + 686BE83025E58CCF00862533 /* RunnerUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = 686BE82F25E58CCF00862533 /* RunnerUITests.m */; }; + 83C36CAF23D629E5ABE75B2A /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = CCC799F2B0AB50A9C34344F0 /* libPods-Runner.a */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33E20B3726EFCDFC00A4A191 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; + 686BE83225E58CCF00862533 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 33E20B3226EFCDFC00A4A191 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 33E20B3426EFCDFC00A4A191 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = ""; }; + 33E20B3626EFCDFC00A4A191 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 5278439583922091276A37C9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 686BE82D25E58CCF00862533 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 686BE82F25E58CCF00862533 /* RunnerUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerUITests.m; sourceTree = ""; }; + 686BE83125E58CCF00862533 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 96F949A6B78E2DC62B93C4F8 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9D27FE1F0F21D4D47DDA16DE /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + CCC799F2B0AB50A9C34344F0 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + D1A69703A518C37D73BF8B91 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + F0609304FBCAEC2289164BD5 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33E20B2F26EFCDFC00A4A191 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 50EB54C1FE43DB743F5DEC7C /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 686BE82A25E58CCF00862533 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 83C36CAF23D629E5ABE75B2A /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33E20B3326EFCDFC00A4A191 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 33E20B3426EFCDFC00A4A191 /* RunnerTests.m */, + 33E20B3626EFCDFC00A4A191 /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 686BE82E25E58CCF00862533 /* RunnerUITests */ = { + isa = PBXGroup; + children = ( + 686BE82F25E58CCF00862533 /* RunnerUITests.m */, + 686BE83125E58CCF00862533 /* Info.plist */, + ); + path = RunnerUITests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 686BE82E25E58CCF00862533 /* RunnerUITests */, + 33E20B3326EFCDFC00A4A191 /* RunnerTests */, + 97C146EF1CF9000F007C117D /* Products */, + D0FE95BE2380323DD75CB891 /* Pods */, + A44AD0D63DEF785A2A2DEE28 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 686BE82D25E58CCF00862533 /* RunnerUITests.xctest */, + 33E20B3226EFCDFC00A4A191 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + A44AD0D63DEF785A2A2DEE28 /* Frameworks */ = { + isa = PBXGroup; + children = ( + CCC799F2B0AB50A9C34344F0 /* libPods-Runner.a */, + D1A69703A518C37D73BF8B91 /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + D0FE95BE2380323DD75CB891 /* Pods */ = { + isa = PBXGroup; + children = ( + 5278439583922091276A37C9 /* Pods-Runner.debug.xcconfig */, + F0609304FBCAEC2289164BD5 /* Pods-Runner.release.xcconfig */, + 9D27FE1F0F21D4D47DDA16DE /* Pods-RunnerTests.debug.xcconfig */, + 96F949A6B78E2DC62B93C4F8 /* Pods-RunnerTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33E20B3126EFCDFC00A4A191 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33E20B3B26EFCDFC00A4A191 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 3B2E8279C112D7129C8D23F1 /* [CP] Check Pods Manifest.lock */, + 33E20B2E26EFCDFC00A4A191 /* Sources */, + 33E20B2F26EFCDFC00A4A191 /* Frameworks */, + 33E20B3026EFCDFC00A4A191 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 33E20B3826EFCDFC00A4A191 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 33E20B3226EFCDFC00A4A191 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 686BE82C25E58CCF00862533 /* RunnerUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 686BE83625E58CCF00862533 /* Build configuration list for PBXNativeTarget "RunnerUITests" */; + buildPhases = ( + 686BE82925E58CCF00862533 /* Sources */, + 686BE82A25E58CCF00862533 /* Frameworks */, + 686BE82B25E58CCF00862533 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 686BE83325E58CCF00862533 /* PBXTargetDependency */, + ); + name = RunnerUITests; + productName = RunnerUITests; + productReference = 686BE82D25E58CCF00862533 /* RunnerUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + C6989ECD8FF0836301D734B4 /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1100; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 33E20B3126EFCDFC00A4A191 = { + CreatedOnToolsVersion = 12.5; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 686BE82C25E58CCF00862533 = { + CreatedOnToolsVersion = 12.4; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 686BE82C25E58CCF00862533 /* RunnerUITests */, + 33E20B3126EFCDFC00A4A191 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33E20B3026EFCDFC00A4A191 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 686BE82B25E58CCF00862533 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 3B2E8279C112D7129C8D23F1 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + C6989ECD8FF0836301D734B4 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33E20B2E26EFCDFC00A4A191 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33E20B3526EFCDFC00A4A191 /* RunnerTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 686BE82925E58CCF00862533 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 686BE83025E58CCF00862533 /* RunnerUITests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33E20B3826EFCDFC00A4A191 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 33E20B3726EFCDFC00A4A191 /* PBXContainerItemProxy */; + }; + 686BE83325E58CCF00862533 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 686BE83225E58CCF00862533 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 33E20B3926EFCDFC00A4A191 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9D27FE1F0F21D4D47DDA16DE /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Debug; + }; + 33E20B3A26EFCDFC00A4A191 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 96F949A6B78E2DC62B93C4F8 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Release; + }; + 686BE83425E58CCF00862533 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = RunnerUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Runner; + }; + name = Debug; + }; + 686BE83525E58CCF00862533 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = RunnerUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Runner; + }; + name = Release; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.quickActionsExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.quickActionsExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33E20B3B26EFCDFC00A4A191 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33E20B3926EFCDFC00A4A191 /* Debug */, + 33E20B3A26EFCDFC00A4A191 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 686BE83625E58CCF00862533 /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 686BE83425E58CCF00862533 /* Debug */, + 686BE83525E58CCF00862533 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/quick_actions/quick_actions/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..ac798eda8c17 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/in_app_purchase/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/quick_actions/quick_actions/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/in_app_purchase/example/ios/Runner.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.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/quick_actions/quick_actions/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + 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/battery/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/battery/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/device_info/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/device_info/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/device_info/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/device_info/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/device_info/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/device_info/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/device_info/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/device_info/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/device_info/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/device_info/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/device_info/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/device_info/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/device_info/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/device_info/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/device_info/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/device_info/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/device_info/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/device_info/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/device_info/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/device_info/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/device_info/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/device_info/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/device_info/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/device_info/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/device_info/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/device_info/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/device_info/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/device_info/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/connectivity/connectivity_macos/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/connectivity/connectivity_macos/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/device_info/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/device_info/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/RunnerTests/Info.plist b/packages/quick_actions/quick_actions/example/ios/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/quick_actions/quick_actions/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/quick_actions/quick_actions/example/ios/RunnerTests/RunnerTests.m b/packages/quick_actions/quick_actions/example/ios/RunnerTests/RunnerTests.m new file mode 100644 index 000000000000..64e0f7e1d8b2 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/ios/RunnerTests/RunnerTests.m @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import quick_actions; +@import XCTest; + +@interface QuickActionsTests : XCTestCase +@end + +@implementation QuickActionsTests + +- (void)testPlugin { + FLTQuickActionsPlugin* plugin = [[FLTQuickActionsPlugin alloc] init]; + XCTAssertNotNil(plugin); +} + +@end diff --git a/packages/quick_actions/quick_actions/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..c4ee86039761 --- /dev/null +++ b/packages/quick_actions/quick_actions/example/pubspec.yaml @@ -0,0 +1,29 @@ +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: + espresso: ^0.1.0+2 + 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/device_info/ios/Assets/.gitkeep b/packages/quick_actions/quick_actions/ios/Assets/.gitkeep similarity index 100% rename from packages/device_info/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..a099b696387c --- /dev/null +++ b/packages/quick_actions/quick_actions/ios/Classes/FLTQuickActionsPlugin.m @@ -0,0 +1,110 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "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 ([call.method isEqualToString:@"setShortcutItems"]) { + _setShortcutItems(call.arguments); + result(nil); + } else if ([call.method isEqualToString:@"clearShortcutItems"]) { + [UIApplication sharedApplication].shortcutItems = @[]; + result(nil); + } else if ([call.method isEqualToString:@"getLaunchAction"]) { + result(nil); + } else { + result(FlutterMethodNotImplemented); + } +} + +- (void)dealloc { + [_channel setMethodCallHandler:nil]; + _channel = nil; +} + +- (BOOL)application:(UIApplication *)application + performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem + completionHandler:(void (^)(BOOL succeeded))completionHandler + API_AVAILABLE(ios(9.0)) { + [self handleShortcut:shortcutItem.type]; + return YES; +} + +- (BOOL)application:(UIApplication *)application + didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + UIApplicationShortcutItem *shortcutItem = + launchOptions[UIApplicationLaunchOptionsShortcutItemKey]; + if (shortcutItem) { + // Keep hold of the shortcut type and handle it in the + // `applicationDidBecomeActure:` method once the Dart MethodChannel + // is initialized. + self.shortcutType = shortcutItem.type; + + // Return NO to indicate we handled the quick action to ensure + // the `application:performActionFor:` method is not called (as + // per Apple's documentation: + // https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1622935-application?language=objc). + return NO; + } + return YES; +} + +- (void)applicationDidBecomeActive:(UIApplication *)application { + if (self.shortcutType) { + [self handleShortcut:self.shortcutType]; + self.shortcutType = nil; + } +} + +#pragma mark Private functions + +- (void)handleShortcut:(NSString *)shortcut { + [self.channel invokeMethod:@"launch" arguments:shortcut]; +} + +NS_INLINE void _setShortcutItems(NSArray *items) API_AVAILABLE(ios(9.0)) { + NSMutableArray *newShortcuts = [[NSMutableArray alloc] init]; + + for (id item in items) { + UIApplicationShortcutItem *shortcut = _deserializeShortcutItem(item); + [newShortcuts addObject:shortcut]; + } + + [UIApplication sharedApplication].shortcutItems = newShortcuts; +} + +NS_INLINE UIApplicationShortcutItem *_deserializeShortcutItem(NSDictionary *serialized) + API_AVAILABLE(ios(9.0)) { + UIApplicationShortcutIcon *icon = + [serialized[@"icon"] isKindOfClass:[NSNull class]] + ? nil + : [UIApplicationShortcutIcon iconWithTemplateImageName:serialized[@"icon"]]; + + return [[UIApplicationShortcutItem alloc] initWithType:serialized[@"type"] + localizedTitle:serialized[@"localizedTitle"] + localizedSubtitle:nil + icon:icon + userInfo:nil]; +} + +@end diff --git a/packages/quick_actions/ios/quick_actions.podspec b/packages/quick_actions/quick_actions/ios/quick_actions.podspec similarity index 87% rename from packages/quick_actions/ios/quick_actions.podspec rename to packages/quick_actions/quick_actions/ios/quick_actions.podspec index b7d56bf3d818..9452fd8c983d 100644 --- a/packages/quick_actions/ios/quick_actions.podspec +++ b/packages/quick_actions/quick_actions/ios/quick_actions.podspec @@ -17,6 +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' } + s.platform = :ios, '9.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end 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..7d3d4ad1ef3b --- /dev/null +++ b/packages/quick_actions/quick_actions/lib/quick_actions.dart @@ -0,0 +1,30 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '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 { + /// Creates a new instance of [QuickActions]. + const QuickActions(); + + /// Initializes this plugin. + /// + /// Call this once before any further interaction with 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..9531b7027cdf --- /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/quick_actions +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+quick_actions%22 +version: 0.6.0+7 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.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..27d3c81a809a --- /dev/null +++ b/packages/quick_actions/quick_actions/test/quick_actions_test.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: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('constructor() should return valid QuickActions instance', () { + const QuickActions quickActions = QuickActions(); + expect(quickActions, isNotNull); + }); + + test('initialize() PlatformInterface', () async { + const QuickActions quickActions = QuickActions(); + QuickActionHandler handler = (type) {}; + + await quickActions.initialize(handler); + verify(QuickActionsPlatform.instance.initialize(handler)).called(1); + }); + + test('setShortcutItems() PlatformInterface', () { + const 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', () { + const 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/AUTHORS b/packages/quick_actions/quick_actions_platform_interface/AUTHORS new file mode 100644 index 000000000000..0ca697b6a756 --- /dev/null +++ b/packages/quick_actions/quick_actions_platform_interface/AUTHORS @@ -0,0 +1,67 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Daniel Roek 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..2e06935ccb09 --- /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 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..d1c8798aa65f --- /dev/null +++ b/packages/quick_actions/quick_actions_platform_interface/test/quick_actions_platform_interface_test.dart @@ -0,0 +1,76 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_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(); + + // Store the initial instance before any tests change it. + final QuickActionsPlatform initialInstance = QuickActionsPlatform.instance; + + group('$QuickActionsPlatform', () { + test('$MethodChannelQuickActions is the default instance', () { + expect(initialInstance, 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 1e9d172f21bc..acea470855fb 100644 --- a/packages/sensors/CHANGELOG.md +++ b/packages/sensors/CHANGELOG.md @@ -1,3 +1,53 @@ +## NEXT + +* Remove references to the Android V1 embedding. +* Updated Android lint settings. + +## 2.0.3 + +* Update README to point to Plus Plugins version. + +## 2.0.2 + +* Fix -Wstrict-prototypes analyzer warning in iOS plugin. + +## 2.0.1 + +* Migrate maven repository from jcenter to mavenCentral. + +## 2.0.0 + +* Migrate to null safety. + +## 0.4.2+8 + +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) + +## 0.4.2+7 + +* Update Flutter SDK constraint. + +## 0.4.2+6 + +* Update android compileSdkVersion to 29. + +## 0.4.2+5 + +* Keep handling deprecated Android v1 classes for backward compatibility. + +## 0.4.2+4 + +* Update package:e2e -> package:integration_test + +## 0.4.2+3 + +* Update package:e2e reference to use the local version in the flutter/plugins + repository. + +## 0.4.2+2 + +* Post-v2 Android embedding cleanup. + ## 0.4.2+1 * Update lower bound of dart dependency to 2.1.0. diff --git a/packages/sensors/LICENSE b/packages/sensors/LICENSE index c89293372cf3..c6823b81eb84 100644 --- a/packages/sensors/LICENSE +++ b/packages/sensors/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/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..7e1087764dee 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 @@ -30,5 +30,19 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } } } diff --git a/packages/sensors/android/gradle.properties b/packages/sensors/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/sensors/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M 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 5c9cad2dd6f8..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. @@ -10,7 +10,6 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.EventChannel; -import io.flutter.plugin.common.PluginRegistry.Registrar; /** SensorsPlugin */ public class SensorsPlugin implements FlutterPlugin { @@ -25,7 +24,8 @@ public class SensorsPlugin implements FlutterPlugin { private EventChannel gyroscopeChannel; /** Plugin registration. */ - public static void registerWith(Registrar registrar) { + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { SensorsPlugin plugin = new SensorsPlugin(); plugin.setupEventChannels(registrar.context(), registrar.messenger()); } 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/AndroidManifest.xml b/packages/sensors/example/android/app/src/main/AndroidManifest.xml index 67d4eb5e4a5f..ea3155cb9722 100644 --- a/packages/sensors/example/android/app/src/main/AndroidManifest.xml +++ b/packages/sensors/example/android/app/src/main/AndroidManifest.xml @@ -3,15 +3,8 @@ - - - - + + diff --git a/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java b/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/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 deleted file mode 100644 index c91a3a942ba5..000000000000 --- a/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/EmbeddingV1Activity.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.sensorsexample; - -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; - -public class EmbeddingV1Activity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); - } -} 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 deleted file mode 100644 index 6d0274f50e2c..000000000000 --- a/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.flutter.plugins.sensorsexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} 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 new file mode 100644 index 000000000000..52a6b8bebaf3 --- /dev/null +++ b/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/FlutterActivityTest.java @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.sensorsexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class FlutterActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/MainActivity.java b/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/MainActivity.java deleted file mode 100644 index 38d05d82afb5..000000000000 --- a/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/MainActivity.java +++ /dev/null @@ -1,20 +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.sensorsexample; - -import io.flutter.embedding.android.FlutterActivity; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.plugins.sensors.SensorsPlugin; - -public class MainActivity extends FlutterActivity { - - // TODO(cyanglaz): Remove this once v2 of GeneratedPluginRegistrant rolls to stable. - // https://github.com/flutter/flutter/issues/42694 - @Override - public void configureFlutterEngine(FlutterEngine flutterEngine) { - super.configureFlutterEngine(flutterEngine); - flutterEngine.getPlugins().add(new SensorsPlugin()); - } -} diff --git a/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/MainActivityTest.java b/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/MainActivityTest.java deleted file mode 100644 index dee5dffe3553..000000000000 --- a/packages/sensors/example/android/app/src/main/java/io/flutter/plugins/sensorsexample/MainActivityTest.java +++ /dev/null @@ -1,15 +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.sensorsexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class MainActivityTest { - @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); -} 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/example/integration_test/sensors_test.dart b/packages/sensors/example/integration_test/sensors_test.dart new file mode 100644 index 000000000000..3b8f614d2dcb --- /dev/null +++ b/packages/sensors/example/integration_test/sensors_test.dart @@ -0,0 +1,26 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// @dart = 2.9 + +import 'dart:async'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:sensors/sensors.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('Can subscript to accelerometerEvents and get non-null events', + (WidgetTester tester) async { + final Completer completer = + Completer(); + StreamSubscription subscription; + subscription = accelerometerEvents.listen((AccelerometerEvent event) { + completer.complete(event); + subscription.cancel(); + }); + expect(await completer.future, isNotNull); + }); +} diff --git a/packages/sensors/example/ios/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..69cd37f9ab86 100644 --- a/packages/sensors/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/sensors/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,10 +9,6 @@ /* 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 */; }; @@ -28,8 +24,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -40,7 +34,6 @@ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 3B80C3931E831B6300D905FE /* App.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = App.framework; path = Flutter/App.framework; sourceTree = ""; }; 518BFCF6A33590E963FE1FA9 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 65D7779632A59CFED1723B85 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; @@ -48,7 +41,6 @@ 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 9740EEBA1CF902C7004384FC /* Flutter.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Flutter.framework; path = Flutter/Flutter.framework; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; @@ -63,8 +55,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, A5B646543530B300A487D9B1 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -84,9 +74,7 @@ 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( - 3B80C3931E831B6300D905FE /* App.framework */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEBA1CF902C7004384FC /* Flutter.framework */, 9740EEB21CF90195004384FC /* Debug.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */, @@ -159,7 +147,6 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 2EB2E4FB0B576731DB30F0C4 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -177,7 +164,7 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; + ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; @@ -217,21 +204,6 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 2EB2E4FB0B576731DB30F0C4 /* [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 +216,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"; }; 7B77DB2BA78582CC43C8E79F /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; @@ -315,7 +287,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 +343,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -438,7 +408,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 +430,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.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/sensors/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16ed0f..919434a6254f 100644 --- a/packages/sensors/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/packages/sensors/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> 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 69aa54ca94e4..fee7bd61f736 100644 --- a/packages/sensors/example/pubspec.yaml +++ b/packages/sensors/example/pubspec.yaml @@ -1,22 +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 - e2e: ^0.2.0 - pedantic: ^1.8.0 + integration_test: + 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/sensors_e2e_test.dart b/packages/sensors/example/test_driver/test/sensors_e2e_test.dart deleted file mode 100644 index f3aa9e218d82..000000000000 --- a/packages/sensors/example/test_driver/test/sensors_e2e_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 'dart:async'; -import 'dart:io'; -import 'package:flutter_driver/flutter_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); -} 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 6b085646c2ce..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+1 +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,13 +23,10 @@ dependencies: sdk: flutter dev_dependencies: - test: ^1.3.0 + test: ^1.16.0 flutter_test: sdk: flutter - e2e: ^0.2.0 - 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" + integration_test: + sdk: flutter + mockito: ^5.0.0 + pedantic: ^1.10.0 diff --git a/packages/sensors/test/sensors_e2e.dart b/packages/sensors/test/sensors_e2e.dart deleted file mode 100644 index acc356dfc235..000000000000 --- a/packages/sensors/test/sensors_e2e.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 'dart:async'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:sensors/sensors.dart'; -import 'package:e2e/e2e.dart'; - -void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('Can subscript to accelerometerEvents and get non-null events', - (WidgetTester tester) async { - final Completer completer = - Completer(); - StreamSubscription subscription; - subscription = accelerometerEvents.listen((AccelerometerEvent event) { - completer.complete(event); - subscription.cancel(); - }); - expect(await completer.future, isNotNull); - }); -} diff --git a/packages/sensors/test/sensors_test.dart b/packages/sensors/test/sensors_test.dart index 832a2f8524b7..bce3afe6205b 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,19 @@ void main() { void _initializeFakeSensorChannel(String channelName, List sensorData) { const StandardMethodCodec standardMethod = StandardMethodCodec(); - void _emitEvent(ByteData event) { - ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage( - channelName, - event, - (ByteData reply) {}, - ); + void _emitEvent(ByteData? event) { + _ambiguate(ServicesBinding.instance)! + .defaultBinaryMessenger + .handlePlatformMessage( + channelName, + event, + (ByteData? reply) {}, + ); } - ServicesBinding.instance.defaultBinaryMessenger - .setMockMessageHandler(channelName, (ByteData message) async { + _ambiguate(ServicesBinding.instance)! + .defaultBinaryMessenger + .setMockMessageHandler(channelName, (ByteData? message) async { final MethodCall methodCall = standardMethod.decodeMethodCall(message); if (methodCall.method == 'listen') { _emitEvent(standardMethod.encodeSuccessEnvelope(sensorData)); @@ -74,3 +76,10 @@ void _initializeFakeSensorChannel(String channelName, List sensorData) { } }); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +// TODO(ianh): Remove this once we roll stable in late 2021. +T? _ambiguate(T? value) => value; diff --git a/packages/share/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 d77b96a95673..c9a468d925a7 100644 --- a/packages/share/CHANGELOG.md +++ b/packages/share/CHANGELOG.md @@ -1,3 +1,69 @@ +## NEXT + +* Remove references to the Android V1 embedding. +* Updated Android lint settings. + +## 2.0.4 + +* Update README to point to Plus Plugins version. + +## 2.0.3 + +* Do not tear down method channel onDetachedFromActivity. + +## 2.0.2 + +* Migrate maven repository from jcenter to mavenCentral. + +## 2.0.1 + +* Migrate unit tests to sound null safety. + +## 2.0.0 + +* Migrate to null safety. +* Update the example app: remove the deprecated `RaisedButton` and `FlatButton` widgets. +* Fix outdated links across a number of markdown files ([#3276](https://github.com/flutter/plugins/pull/3276)) +* Update README with the new documentation urls. + +## 0.6.5+5 + +* Update Flutter SDK constraint. + +## 0.6.5+4 + +* Fix iPad share window not showing when `origin` is null. + +## 0.6.5+3 + +* Replace deprecated `Environment.getExternalStorageDirectory()` call on Android. +* Upgrade to Android Gradle plugin 3.5.0 & target API level 29. + +## 0.6.5+2 + +* Keep handling deprecated Android v1 classes for backward compatibility. + +## 0.6.5+1 + +* Avoiding uses unchecked or unsafe Object Type Casting + +## 0.6.5 + +* Added support for sharing files + +## 0.6.4+5 + +* Update package:e2e -> package:integration_test + +## 0.6.4+4 + +* Update package:e2e reference to use the local version in the flutter/plugins + repository. + +## 0.6.4+3 + +* Post-v2 Android embedding cleanup. + ## 0.6.4+2 * Update lower bound of dart dependency to 2.1.0. diff --git a/packages/share/LICENSE b/packages/share/LICENSE index 176a661f7e48..c6823b81eb84 100644 --- a/packages/share/LICENSE +++ b/packages/share/LICENSE @@ -1,27 +1,25 @@ -// Copyright 2017, the Flutter project 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/share/README.md b/packages/share/README.md index 14be8da7d10e..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 @@ -39,3 +47,9 @@ sharing to email. ``` dart Share.share('check out my website https://example.com', subject: 'Look what I made!'); ``` + +To share one or multiple files invoke the static `shareFiles` method anywhere in your Dart code. Optionally you can also pass in `text` and `subject`. +``` dart +Share.shareFiles(['${directory.path}/image.jpg'], text: 'Great picture'); +Share.shareFiles(['${directory.path}/image1.jpg', '${directory.path}/image2.jpg']); +``` diff --git a/packages/share/analysis_options.yaml b/packages/share/analysis_options.yaml 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 e154b068c5dd..b2ea363a3e11 100644 --- a/packages/share/android/build.gradle +++ b/packages/share/android/build.gradle @@ -4,25 +4,25 @@ 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:3.5.0' } } rootProject.allprojects { repositories { google() - jcenter() + mavenCentral() } } apply plugin: 'com.android.library' android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { minSdkVersion 16 @@ -30,5 +30,24 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' + } + + dependencies { + implementation 'androidx.core:core:1.3.1' + implementation 'androidx.annotation:annotation:1.1.0' + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } } } diff --git a/packages/share/android/gradle.properties b/packages/share/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/share/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M diff --git a/packages/share/android/src/main/AndroidManifest.xml b/packages/share/android/src/main/AndroidManifest.xml index 407eae4d8128..c141a5c67928 100644 --- a/packages/share/android/src/main/AndroidManifest.xml +++ b/packages/share/android/src/main/AndroidManifest.xml @@ -1,3 +1,14 @@ + + + + + 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 f7e4d579e7a2..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. @@ -6,6 +6,8 @@ import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; +import java.io.*; +import java.util.List; import java.util.Map; /** Handles the method calls for the plugin. */ @@ -19,15 +21,39 @@ class MethodCallHandler implements MethodChannel.MethodCallHandler { @Override public void onMethodCall(MethodCall call, MethodChannel.Result result) { - if (call.method.equals("share")) { - if (!(call.arguments instanceof Map)) { - throw new IllegalArgumentException("Map argument expected"); - } - // Android does not support showing the share sheet at a particular point on screen. - share.share((String) call.argument("text"), (String) call.argument("subject")); - result.success(null); - } else { - result.notImplemented(); + switch (call.method) { + case "share": + expectMapArguments(call); + // Android does not support showing the share sheet at a particular point on screen. + String text = call.argument("text"); + String subject = call.argument("subject"); + share.share(text, subject); + result.success(null); + break; + case "shareFiles": + expectMapArguments(call); + + List paths = call.argument("paths"); + List mimeTypes = call.argument("mimeTypes"); + text = call.argument("text"); + subject = call.argument("subject"); + // Android does not support showing the share sheet at a particular point on screen. + try { + share.shareFiles(paths, mimeTypes, text, subject); + result.success(null); + } catch (IOException e) { + result.error(e.getMessage(), null, null); + } + break; + default: + result.notImplemented(); + break; + } + } + + private void expectMapArguments(MethodCall call) throws IllegalArgumentException { + if (!(call.arguments instanceof Map)) { + throw new IllegalArgumentException("Map argument expected"); } } } diff --git a/packages/share/android/src/main/java/io/flutter/plugins/share/Share.java b/packages/share/android/src/main/java/io/flutter/plugins/share/Share.java index 8c9e833ee9d3..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,23 +1,39 @@ -// 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. package io.flutter.plugins.share; import android.app.Activity; +import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import androidx.annotation.NonNull; +import androidx.core.content.FileProvider; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; /** Handles share intent. */ class Share { + private Context context; private Activity activity; /** - * Constructs a Share object. The {@code activity} is used to start the share intent. It might be - * null when constructing the {@link Share} object and set to non-null when an activity is - * available using {@link #setActivity(Activity)}. + * Constructs a Share object. The {@code context} and {@code activity} are used to start the share + * intent. The {@code activity} might be null when constructing the {@link Share} object and set + * to non-null when an activity is available using {@link #setActivity(Activity)}. */ - Share(Activity activity) { + Share(Context context, Activity activity) { + this.context = context; this.activity = activity; } @@ -40,11 +56,177 @@ void share(String text, String subject) { shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject); shareIntent.setType("text/plain"); Intent chooserIntent = Intent.createChooser(shareIntent, null /* dialog title optional */); + startActivity(chooserIntent); + } + + void shareFiles(List paths, List mimeTypes, String text, String subject) + throws IOException { + if (paths == null || paths.isEmpty()) { + throw new IllegalArgumentException("Non-empty path expected"); + } + + clearExternalShareFolder(); + ArrayList fileUris = getUrisForPaths(paths); + + Intent shareIntent = new Intent(); + if (fileUris.isEmpty()) { + share(text, subject); + return; + } else if (fileUris.size() == 1) { + shareIntent.setAction(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_STREAM, fileUris.get(0)); + shareIntent.setType( + !mimeTypes.isEmpty() && mimeTypes.get(0) != null ? mimeTypes.get(0) : "*/*"); + } else { + shareIntent.setAction(Intent.ACTION_SEND_MULTIPLE); + shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, fileUris); + shareIntent.setType(reduceMimeTypes(mimeTypes)); + } + if (text != null) shareIntent.putExtra(Intent.EXTRA_TEXT, text); + if (subject != null) shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject); + shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + Intent chooserIntent = Intent.createChooser(shareIntent, null /* dialog title optional */); + + List resInfoList = + getContext() + .getPackageManager() + .queryIntentActivities(chooserIntent, PackageManager.MATCH_DEFAULT_ONLY); + for (ResolveInfo resolveInfo : resInfoList) { + String packageName = resolveInfo.activityInfo.packageName; + for (Uri fileUri : fileUris) { + getContext() + .grantUriPermission( + packageName, + fileUri, + Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + } + + startActivity(chooserIntent); + } + + private void startActivity(Intent intent) { if (activity != null) { - activity.startActivity(chooserIntent); + activity.startActivity(intent); + } else if (context != null) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } else { + throw new IllegalStateException("Both context and activity are null"); + } + } + + private ArrayList getUrisForPaths(List paths) throws IOException { + ArrayList uris = new ArrayList<>(paths.size()); + for (String path : paths) { + File file = new File(path); + if (!fileIsOnExternal(file)) { + file = copyToExternalShareFolder(file); + } + + uris.add( + FileProvider.getUriForFile( + getContext(), getContext().getPackageName() + ".flutter.share_provider", file)); + } + return uris; + } + + private String reduceMimeTypes(List mimeTypes) { + if (mimeTypes.size() > 1) { + String reducedMimeType = mimeTypes.get(0); + for (int i = 1; i < mimeTypes.size(); i++) { + String mimeType = mimeTypes.get(i); + if (!reducedMimeType.equals(mimeType)) { + if (getMimeTypeBase(mimeType).equals(getMimeTypeBase(reducedMimeType))) { + reducedMimeType = getMimeTypeBase(mimeType) + "/*"; + } else { + reducedMimeType = "*/*"; + break; + } + } + } + return reducedMimeType; + } else if (mimeTypes.size() == 1) { + return mimeTypes.get(0); } else { - chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - activity.startActivity(chooserIntent); + return "*/*"; + } + } + + @NonNull + private String getMimeTypeBase(String mimeType) { + if (mimeType == null || !mimeType.contains("/")) { + return "*"; + } + + return mimeType.substring(0, mimeType.indexOf("/")); + } + + private boolean fileIsOnExternal(File file) { + try { + String filePath = file.getCanonicalPath(); + File externalDir = context.getExternalFilesDir(null); + return externalDir != null && filePath.startsWith(externalDir.getCanonicalPath()); + } catch (IOException e) { + return false; + } + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + private void clearExternalShareFolder() { + File folder = getExternalShareFolder(); + if (folder.exists()) { + for (File file : folder.listFiles()) { + file.delete(); + } + folder.delete(); + } + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + private File copyToExternalShareFolder(File file) throws IOException { + File folder = getExternalShareFolder(); + if (!folder.exists()) { + folder.mkdirs(); + } + + File newFile = new File(folder, file.getName()); + copy(file, newFile); + return newFile; + } + + @NonNull + private File getExternalShareFolder() { + return new File(getContext().getExternalCacheDir(), "share"); + } + + private Context getContext() { + if (activity != null) { + return activity; + } + if (context != null) { + return context; + } + + throw new IllegalStateException("Both context and activity are null"); + } + + private static void copy(File src, File dst) throws IOException { + InputStream in = new FileInputStream(src); + try { + OutputStream out = new FileOutputStream(dst); + try { + // Transfer bytes from in to out + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + } finally { + out.close(); + } + } finally { + in.close(); } } } diff --git a/packages/share/android/src/main/java/io/flutter/plugins/share/ShareFileProvider.java b/packages/share/android/src/main/java/io/flutter/plugins/share/ShareFileProvider.java new file mode 100644 index 000000000000..fff48a6bad14 --- /dev/null +++ b/packages/share/android/src/main/java/io/flutter/plugins/share/ShareFileProvider.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.share; + +import androidx.core.content.FileProvider; + +/** + * Providing a custom {@code FileProvider} prevents manifest {@code } name collisions. + * + *

      See https://developer.android.com/guide/topics/manifest/provider-element.html for details. + */ +public class ShareFileProvider extends FileProvider {} diff --git a/packages/share/android/src/main/java/io/flutter/plugins/share/SharePlugin.java b/packages/share/android/src/main/java/io/flutter/plugins/share/SharePlugin.java index fdb9dc4fe644..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,16 +1,16 @@ -// 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. package io.flutter.plugins.share; import android.app.Activity; +import android.content.Context; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.PluginRegistry.Registrar; /** Plugin method host for presenting a share sheet via Intent */ public class SharePlugin implements FlutterPlugin, ActivityAware { @@ -20,14 +20,15 @@ public class SharePlugin implements FlutterPlugin, ActivityAware { private Share share; private MethodChannel methodChannel; - public static void registerWith(Registrar registrar) { + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { SharePlugin plugin = new SharePlugin(); - plugin.setUpChannel(registrar.activity(), registrar.messenger()); + plugin.setUpChannel(registrar.context(), registrar.activity(), registrar.messenger()); } @Override public void onAttachedToEngine(FlutterPluginBinding binding) { - setUpChannel(null, binding.getBinaryMessenger()); + setUpChannel(binding.getApplicationContext(), null, binding.getBinaryMessenger()); } @Override @@ -44,7 +45,7 @@ public void onAttachedToActivity(ActivityPluginBinding binding) { @Override public void onDetachedFromActivity() { - tearDownChannel(); + share.setActivity(null); } @Override @@ -57,15 +58,10 @@ public void onDetachedFromActivityForConfigChanges() { onDetachedFromActivity(); } - private void setUpChannel(Activity activity, BinaryMessenger messenger) { + private void setUpChannel(Context context, Activity activity, BinaryMessenger messenger) { methodChannel = new MethodChannel(messenger, CHANNEL); - share = new Share(activity); + share = new Share(context, activity); handler = new MethodCallHandler(share); methodChannel.setMethodCallHandler(handler); } - - private void tearDownChannel() { - share.setActivity(null); - methodChannel.setMethodCallHandler(null); - } } diff --git a/packages/share/android/src/main/res/xml/flutter_share_file_paths.xml b/packages/share/android/src/main/res/xml/flutter_share_file_paths.xml new file mode 100644 index 000000000000..e68bf916a30b --- /dev/null +++ b/packages/share/android/src/main/res/xml/flutter_share_file_paths.xml @@ -0,0 +1,6 @@ + + + + + + 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/AndroidManifest.xml b/packages/share/example/android/app/src/main/AndroidManifest.xml index d5e5ec8bf39d..d1f1ce953e3a 100644 --- a/packages/share/example/android/app/src/main/AndroidManifest.xml +++ b/packages/share/example/android/app/src/main/AndroidManifest.xml @@ -3,15 +3,8 @@ - - - - + + diff --git a/packages/share/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java b/packages/share/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/share/example/android/app/src/main/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/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 deleted file mode 100644 index 736ac546c55a..000000000000 --- a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/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.shareexample; - -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; - -public class EmbeddingV1Activity extends FlutterActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); - } -} 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 deleted file mode 100644 index 958541165806..000000000000 --- a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.flutter.plugins.shareexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} 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 new file mode 100644 index 000000000000..aba658887d88 --- /dev/null +++ b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/FlutterActivityTest.java @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.shareexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class FlutterActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivity.java b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivity.java deleted file mode 100644 index 3717feb8ca7e..000000000000 --- a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivity.java +++ /dev/null @@ -1,19 +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. - -package io.flutter.plugins.shareexample; - -import io.flutter.embedding.android.FlutterActivity; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.plugins.share.SharePlugin; - -public class MainActivity extends FlutterActivity { - // TODO(cyanglaz): Remove this once v2 of GeneratedPluginRegistrant rolls to stable. - // https://github.com/flutter/flutter/issues/42694 - @Override - public void configureFlutterEngine(FlutterEngine flutterEngine) { - super.configureFlutterEngine(flutterEngine); - flutterEngine.getPlugins().add(new SharePlugin()); - } -} diff --git a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivityTest.java b/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivityTest.java deleted file mode 100644 index fcd936a7dd0f..000000000000 --- a/packages/share/example/android/app/src/main/java/io/flutter/plugins/shareexample/MainActivityTest.java +++ /dev/null @@ -1,15 +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.shareexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class MainActivityTest { - @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); -} 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..3924e59aa0f9 --- /dev/null +++ b/packages/share/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/share/example/ios/Runner.xcodeproj/project.pbxproj b/packages/share/example/ios/Runner.xcodeproj/project.pbxproj index 730c0d437d27..d7e896212533 100644 --- a/packages/share/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/share/example/ios/Runner.xcodeproj/project.pbxproj @@ -9,11 +9,9 @@ /* Begin PBXBuildFile section */ 28918A213BCB94C5470742D8 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 85392794417D70A970945C83 /* libPods-Runner.a */; }; 2D9222511EC45DE6007564B0 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D9222501EC45DE6007564B0 /* GeneratedPluginRegistrant.m */; }; + 33E20B4326EFCEF400A4A191 /* RunnerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 33E20B4226EFCEF400A4A191 /* RunnerTests.m */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 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 +19,23 @@ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 33E20B4526EFCEF400A4A191 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; + 683426B02538D314009B194C /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -28,8 +43,6 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( - 3B80C3951E831B6300D905FE /* App.framework in Embed Frameworks */, - 9705A1C71CF904A300538489 /* Flutter.framework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -41,15 +54,19 @@ 1BCE6CBBA2E91FD0397A29C8 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 2D92224F1EC45DE6007564B0 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 2D9222501EC45DE6007564B0 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 33E20B4026EFCEF400A4A191 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 33E20B4226EFCEF400A4A191 /* RunnerTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerTests.m; sourceTree = ""; }; + 33E20B4426EFCEF400A4A191 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 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 +76,24 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 33E20B3D26EFCEF400A4A191 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 683426A82538D314009B194C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 28918A213BCB94C5470742D8 /* libPods-Runner.a in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -81,6 +110,24 @@ name = Pods; sourceTree = ""; }; + 33E20B4126EFCEF400A4A191 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 33E20B4226EFCEF400A4A191 /* RunnerTests.m */, + 33E20B4426EFCEF400A4A191 /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 683426AC2538D314009B194C /* RunnerUITests */ = { + isa = PBXGroup; + children = ( + 683426AD2538D314009B194C /* FLTShareExampleUITests.m */, + 683426AF2538D314009B194C /* Info.plist */, + ); + path = RunnerUITests; + sourceTree = ""; + }; 8CA31EF57239BF20619316D9 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -92,9 +139,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 +152,8 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, + 683426AC2538D314009B194C /* RunnerUITests */, + 33E20B4126EFCEF400A4A191 /* RunnerTests */, 97C146EF1CF9000F007C117D /* Products */, 16DDF472245BCC3E62219493 /* Pods */, 8CA31EF57239BF20619316D9 /* Frameworks */, @@ -117,6 +164,8 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + 683426AB2538D314009B194C /* RunnerUITests.xctest */, + 33E20B4026EFCEF400A4A191 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -148,6 +197,42 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 33E20B3F26EFCEF400A4A191 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33E20B4926EFCEF400A4A191 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 33E20B3C26EFCEF400A4A191 /* Sources */, + 33E20B3D26EFCEF400A4A191 /* Frameworks */, + 33E20B3E26EFCEF400A4A191 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 33E20B4626EFCEF400A4A191 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 33E20B4026EFCEF400A4A191 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 683426AA2538D314009B194C /* RunnerUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 683426B42538D314009B194C /* Build configuration list for PBXNativeTarget "RunnerUITests" */; + buildPhases = ( + 683426A72538D314009B194C /* Sources */, + 683426A82538D314009B194C /* Frameworks */, + 683426A92538D314009B194C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 683426B12538D314009B194C /* PBXTargetDependency */, + ); + name = RunnerUITests; + productName = RunnerUITests; + productReference = 683426AB2538D314009B194C /* RunnerUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; @@ -159,7 +244,6 @@ 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 12A149CFB1B2610A83692801 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -177,8 +261,17 @@ isa = PBXProject; attributes = { LastUpgradeCheck = 1100; - ORGANIZATIONNAME = "The Chromium Authors"; + ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { + 33E20B3F26EFCEF400A4A191 = { + CreatedOnToolsVersion = 12.5; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 683426AA2538D314009B194C = { + CreatedOnToolsVersion = 11.7; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; }; @@ -198,11 +291,27 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + 683426AA2538D314009B194C /* RunnerUITests */, + 33E20B3F26EFCEF400A4A191 /* RunnerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 33E20B3E26EFCEF400A4A191 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 683426A92538D314009B194C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -217,21 +326,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 +338,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 +375,22 @@ /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 33E20B3C26EFCEF400A4A191 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33E20B4326EFCEF400A4A191 /* RunnerTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 683426A72538D314009B194C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 683426AE2538D314009B194C /* FLTShareExampleUITests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -293,6 +403,19 @@ }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 33E20B4626EFCEF400A4A191 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 33E20B4526EFCEF400A4A191 /* PBXContainerItemProxy */; + }; + 683426B12538D314009B194C /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 683426B02538D314009B194C /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -313,9 +436,77 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 33E20B4726EFCEF400A4A191 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Debug; + }; + 33E20B4826EFCEF400A4A191 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + INFOPLIST_FILE = RunnerTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; + }; + name = Release; + }; + 683426B22538D314009B194C /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = RunnerUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Runner; + }; + name = Debug; + }; + 683426B32538D314009B194C /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + GCC_C_LANGUAGE_STANDARD = gnu11; + INFOPLIST_FILE = RunnerUITests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = com.google.RunnerUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_TARGET_NAME = Runner; + }; + name = Release; + }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -372,7 +563,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -437,7 +627,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 +648,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 +656,24 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 33E20B4926EFCEF400A4A191 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33E20B4726EFCEF400A4A191 /* Debug */, + 33E20B4826EFCEF400A4A191 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 683426B42538D314009B194C /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 683426B22538D314009B194C /* Debug */, + 683426B32538D314009B194C /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/packages/share/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/share/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16ed0f..919434a6254f 100644 --- a/packages/share/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/packages/share/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> 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.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/share/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/share/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + 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/Info.plist b/packages/share/example/ios/Runner/Info.plist index ac44e05ef845..71656105a1fa 100644 --- a/packages/share/example/ios/Runner/Info.plist +++ b/packages/share/example/ios/Runner/Info.plist @@ -45,5 +45,11 @@ UIViewControllerBasedStatusBarAppearance + NSPhotoLibraryUsageDescription + This app requires access to the photo library for sharing images. + NSMicrophoneUsageDescription + This app does not require access to the microphone for sharing images. + NSCameraUsageDescription + This app requires access to the camera for sharing images. diff --git a/packages/share/example/ios/Runner/main.m b/packages/share/example/ios/Runner/main.m 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/RunnerTests/Info.plist b/packages/share/example/ios/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/share/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/share/example/ios/RunnerTests/RunnerTests.m b/packages/share/example/ios/RunnerTests/RunnerTests.m new file mode 100644 index 000000000000..3c4c341fd451 --- /dev/null +++ b/packages/share/example/ios/RunnerTests/RunnerTests.m @@ -0,0 +1,18 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import share; +@import XCTest; + +@interface ShareTests : XCTestCase +@end + +@implementation ShareTests + +- (void)testPlugin { + FLTSharePlugin* plugin = [[FLTSharePlugin alloc] init]; + XCTAssertNotNil(plugin); +} + +@end diff --git a/packages/share/example/ios/RunnerUITests/FLTShareExampleUITests.m b/packages/share/example/ios/RunnerUITests/FLTShareExampleUITests.m 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 new file mode 100644 index 000000000000..9b5b807c77c6 --- /dev/null +++ b/packages/share/example/lib/image_previews.dart @@ -0,0 +1,79 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +/// Widget for displaying a preview of images +class ImagePreviews extends StatelessWidget { + /// The image paths of the displayed images + final List imagePaths; + + /// Callback when an image should be removed + final Function(int)? onDelete; + + /// Creates a widget for preview of images. [imagePaths] can not be empty + /// and all contained paths need to be non empty. + const ImagePreviews(this.imagePaths, {Key? key, this.onDelete}) + : super(key: key); + + @override + Widget build(BuildContext context) { + if (imagePaths.isEmpty) { + return Container(); + } + + List imageWidgets = []; + for (int i = 0; i < imagePaths.length; i++) { + imageWidgets.add(_ImagePreview( + imagePaths[i], + onDelete: onDelete != null ? () => onDelete!(i) : null, + )); + } + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row(children: imageWidgets), + ); + } +} + +class _ImagePreview extends StatelessWidget { + final String imagePath; + final VoidCallback? onDelete; + + const _ImagePreview(this.imagePath, {Key? key, this.onDelete}) + : super(key: key); + + @override + Widget build(BuildContext context) { + File imageFile = File(imagePath); + return Padding( + padding: const EdgeInsets.all(8.0), + child: Stack( + children: [ + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: 200, + maxHeight: 200, + ), + child: Image.file(imageFile), + ), + Positioned( + right: 0, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + child: Icon(Icons.delete), + onPressed: onDelete), + ), + ), + ], + ), + ); + } +} diff --git a/packages/share/example/lib/main.dart b/packages/share/example/lib/main.dart index b68195cd3507..f802b492df6d 100644 --- a/packages/share/example/lib/main.dart +++ b/packages/share/example/lib/main.dart @@ -1,12 +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. // ignore_for_file: public_member_api_docs import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:share/share.dart'; +import 'image_previews.dart'; + void main() { runApp(DemoApp()); } @@ -19,6 +22,7 @@ class DemoApp extends StatefulWidget { class DemoAppState extends State { String text = ''; String subject = ''; + List imagePaths = []; @override Widget build(BuildContext context) { @@ -28,59 +32,105 @@ class DemoAppState extends State { appBar: AppBar( title: const Text('Share Plugin Demo'), ), - body: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextField( - decoration: const InputDecoration( - labelText: 'Share text:', - hintText: 'Enter some text and/or link to share', + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + decoration: const InputDecoration( + labelText: 'Share text:', + hintText: 'Enter some text and/or link to share', + ), + maxLines: 2, + onChanged: (String value) => setState(() { + text = value; + }), + ), + TextField( + decoration: const InputDecoration( + labelText: 'Share subject:', + hintText: 'Enter subject to share (optional)', + ), + maxLines: 2, + onChanged: (String value) => setState(() { + subject = value; + }), + ), + const Padding(padding: EdgeInsets.only(top: 12.0)), + ImagePreviews(imagePaths, onDelete: _onDeleteImage), + ListTile( + leading: Icon(Icons.add), + title: Text("Add image"), + onTap: () async { + final imagePicker = ImagePicker(); + final pickedFile = await imagePicker.getImage( + source: ImageSource.gallery, + ); + if (pickedFile != null) { + setState(() { + imagePaths.add(pickedFile.path); + }); + } + }, ), - maxLines: 2, - onChanged: (String value) => setState(() { - text = value; - }), - ), - TextField( - decoration: const InputDecoration( - labelText: 'Share subject:', - hintText: 'Enter subject to share (optional)', + const Padding(padding: EdgeInsets.only(top: 12.0)), + Builder( + builder: (BuildContext context) { + return ElevatedButton( + child: const Text('Share'), + onPressed: text.isEmpty && imagePaths.isEmpty + ? null + : () => _onShare(context), + ); + }, ), - maxLines: 2, - onChanged: (String value) => setState(() { - subject = value; - }), - ), - const Padding(padding: EdgeInsets.only(top: 24.0)), - Builder( - builder: (BuildContext context) { - return RaisedButton( - child: const Text('Share'), - onPressed: text.isEmpty - ? null - : () { - // A builder is used to retrieve the context immediately - // surrounding the RaisedButton. - // - // The context's `findRenderObject` returns the first - // RenderObject in its descendent tree when it's not - // a RenderObjectWidget. The RaisedButton's RenderObject - // has its position and size after it's built. - final RenderBox box = context.findRenderObject(); - Share.share(text, - subject: subject, - sharePositionOrigin: - box.localToGlobal(Offset.zero) & - box.size); - }, - ); - }, - ), - ], + const Padding(padding: EdgeInsets.only(top: 12.0)), + Builder( + builder: (BuildContext context) { + return ElevatedButton( + child: const Text('Share With Empty Origin'), + onPressed: () => _onShareWithEmptyOrigin(context), + ); + }, + ), + ], + ), ), )), ); } + + _onDeleteImage(int position) { + setState(() { + imagePaths.removeAt(position); + }); + } + + _onShare(BuildContext context) async { + // A builder is used to retrieve the context immediately + // surrounding the ElevatedButton. + // + // The context's `findRenderObject` returns the first + // RenderObject in its descendent tree when it's not + // a RenderObjectWidget. The ElevatedButton's RenderObject + // has its position and size after it's built. + final RenderBox box = context.findRenderObject() as RenderBox; + + if (imagePaths.isNotEmpty) { + await Share.shareFiles(imagePaths, + text: text, + subject: subject, + sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size); + } else { + await Share.share(text, + subject: subject, + sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size); + } + } + + _onShareWithEmptyOrigin(BuildContext context) async { + await Share.share("text"); + } } diff --git a/packages/share/example/pubspec.yaml b/packages/share/example/pubspec.yaml index f69de96283b3..8a28b43d46e4 100644 --- a/packages/share/example/pubspec.yaml +++ b/packages/share/example/pubspec.yaml @@ -1,22 +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.7.0 dev_dependencies: flutter_driver: sdk: flutter - e2e: ^0.2.0 - pedantic: ^1.8.0 + integration_test: + 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/share_e2e_test.dart b/packages/share/example/test_driver/test/share_e2e_test.dart deleted file mode 100644 index f3aa9e218d82..000000000000 --- a/packages/share/example/test_driver/test/share_e2e_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 'dart:async'; -import 'dart:io'; -import 'package:flutter_driver/flutter_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); -} 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 335ba5b819e5..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. @@ -10,8 +10,12 @@ @interface ShareData : NSObject @property(readonly, nonatomic, copy) NSString *subject; @property(readonly, nonatomic, copy) NSString *text; +@property(readonly, nonatomic, copy) NSString *path; +@property(readonly, nonatomic, copy) NSString *mimeType; - (instancetype)initWithSubject:(NSString *)subject text:(NSString *)text NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithFile:(NSString *)path + mimeType:(NSString *)mimeType NS_DESIGNATED_INITIALIZER; - (instancetype)init __attribute__((unavailable("Use initWithSubject:text: instead"))); @@ -27,24 +31,62 @@ - (instancetype)init { - (instancetype)initWithSubject:(NSString *)subject text:(NSString *)text { self = [super init]; if (self) { - _subject = subject; + _subject = [subject isKindOfClass:NSNull.class] ? @"" : subject; _text = text; } return self; } +- (instancetype)initWithFile:(NSString *)path mimeType:(NSString *)mimeType { + self = [super init]; + if (self) { + _path = path; + _mimeType = mimeType; + } + return self; +} + - (id)activityViewControllerPlaceholderItem:(UIActivityViewController *)activityViewController { return @""; } - (id)activityViewController:(UIActivityViewController *)activityViewController itemForActivityType:(UIActivityType)activityType { - return _text; + if (!_path || !_mimeType) { + return _text; + } + + if ([_mimeType hasPrefix:@"image/"]) { + UIImage *image = [UIImage imageWithContentsOfFile:_path]; + return image; + } else { + NSURL *url = [NSURL fileURLWithPath:_path]; + return url; + } } - (NSString *)activityViewController:(UIActivityViewController *)activityViewController subjectForActivityType:(UIActivityType)activityType { - return [_subject isKindOfClass:NSNull.class] ? @"" : _subject; + return _subject; +} + +- (UIImage *)activityViewController:(UIActivityViewController *)activityViewController + thumbnailImageForActivityType:(UIActivityType)activityType + suggestedSize:(CGSize)suggestedSize { + if (!_path || !_mimeType || ![_mimeType hasPrefix:@"image/"]) { + return nil; + } + + UIImage *image = [UIImage imageWithContentsOfFile:_path]; + return [self imageWithImage:image scaledToSize:suggestedSize]; +} + +- (UIImage *)imageWithImage:(UIImage *)image scaledToSize:(CGSize)newSize { + UIGraphicsBeginImageContext(newSize); + [image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)]; + UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return newImage; } @end @@ -57,8 +99,19 @@ + (void)registerWithRegistrar:(NSObject *)registrar { binaryMessenger:registrar.messenger]; [shareChannel setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) { + NSDictionary *arguments = [call arguments]; + NSNumber *originX = arguments[@"originX"]; + NSNumber *originY = arguments[@"originY"]; + NSNumber *originWidth = arguments[@"originWidth"]; + NSNumber *originHeight = arguments[@"originHeight"]; + + CGRect originRect = CGRectZero; + if (originX && originY && originWidth && originHeight) { + originRect = CGRectMake([originX doubleValue], [originY doubleValue], + [originWidth doubleValue], [originHeight doubleValue]); + } + if ([@"share" isEqualToString:call.method]) { - NSDictionary *arguments = [call arguments]; NSString *shareText = arguments[@"text"]; NSString *shareSubject = arguments[@"subject"]; @@ -69,19 +122,37 @@ + (void)registerWithRegistrar:(NSObject *)registrar { return; } - NSNumber *originX = arguments[@"originX"]; - NSNumber *originY = arguments[@"originY"]; - NSNumber *originWidth = arguments[@"originWidth"]; - NSNumber *originHeight = arguments[@"originHeight"]; + [self shareText:shareText + subject:shareSubject + withController:[UIApplication sharedApplication].keyWindow.rootViewController + atSource:originRect]; + result(nil); + } else if ([@"shareFiles" isEqualToString:call.method]) { + NSArray *paths = arguments[@"paths"]; + NSArray *mimeTypes = arguments[@"mimeTypes"]; + NSString *subject = arguments[@"subject"]; + NSString *text = arguments[@"text"]; + + if (paths.count == 0) { + result([FlutterError errorWithCode:@"error" + message:@"Non-empty paths expected" + details:nil]); + return; + } - CGRect originRect = CGRectZero; - if (originX != nil && originY != nil && originWidth != nil && originHeight != nil) { - originRect = CGRectMake([originX doubleValue], [originY doubleValue], - [originWidth doubleValue], [originHeight doubleValue]); + for (NSString *path in paths) { + if (path.length == 0) { + result([FlutterError errorWithCode:@"error" + message:@"Each path must not be empty" + details:nil]); + return; + } } - [self share:shareText - subject:shareSubject + [self shareFiles:paths + withMimeType:mimeTypes + withSubject:subject + withText:text withController:[UIApplication sharedApplication].keyWindow.rootViewController atSource:originRect]; result(nil); @@ -91,18 +162,55 @@ + (void)registerWithRegistrar:(NSObject *)registrar { }]; } -+ (void)share:(NSString *)shareText - subject:(NSString *)subject ++ (void)share:(NSArray *)shareItems withController:(UIViewController *)controller atSource:(CGRect)origin { - ShareData *data = [[ShareData alloc] initWithSubject:subject text:shareText]; UIActivityViewController *activityViewController = - [[UIActivityViewController alloc] initWithActivityItems:@[ data ] applicationActivities:nil]; + [[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]; } ++ (void)shareText:(NSString *)shareText + subject:(NSString *)subject + withController:(UIViewController *)controller + atSource:(CGRect)origin { + ShareData *data = [[ShareData alloc] initWithSubject:subject text:shareText]; + [self share:@[ data ] withController:controller atSource:origin]; +} + ++ (void)shareFiles:(NSArray *)paths + withMimeType:(NSArray *)mimeTypes + withSubject:(NSString *)subject + withText:(NSString *)text + withController:(UIViewController *)controller + atSource:(CGRect)origin { + NSMutableArray *items = [[NSMutableArray alloc] init]; + + if (text || subject) { + [items addObject:[[ShareData alloc] initWithSubject:subject text:text]]; + } + + for (int i = 0; i < [paths count]; i++) { + NSString *path = paths[i]; + NSString *pathExtension = [path pathExtension]; + NSString *mimeType = mimeTypes[i]; + if ([pathExtension.lowercaseString isEqualToString:@"jpg"] || + [pathExtension.lowercaseString isEqualToString:@"jpeg"] || + [pathExtension.lowercaseString isEqualToString:@"png"] || + [mimeType.lowercaseString isEqualToString:@"image/jpg"] || + [mimeType.lowercaseString isEqualToString:@"image/jpeg"] || + [mimeType.lowercaseString isEqualToString:@"image/png"]) { + UIImage *image = [UIImage imageWithContentsOfFile:path]; + [items addObject:image]; + } else { + [items addObject:[[ShareData alloc] initWithFile:path mimeType:mimeType]]; + } + } + + [self share:items withController:controller atSource:origin]; +} + @end diff --git a/packages/share/ios/share.podspec b/packages/share/ios/share.podspec 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 ff20d194f9e5..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. @@ -7,6 +7,7 @@ import 'dart:ui'; import 'package:flutter/services.dart'; import 'package:meta/meta.dart' show visibleForTesting; +import 'package:mime/mime.dart' show lookupMimeType; /// Plugin for summoning a platform share sheet. class Share { @@ -32,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); @@ -51,4 +52,50 @@ class Share { return channel.invokeMethod('share', params); } + + /// Summons the platform's share sheet to share multiple files. + /// + /// Wraps the platform's native share dialog. Can share a file. + /// It uses the `ACTION_SEND` Intent on Android and `UIActivityViewController` + /// on iOS. + /// + /// The optional `sharePositionOrigin` parameter can be used to specify a global + /// origin rect for the share sheet to popover from on iPads. It has no effect + /// on non-iPads. + /// + /// May throw [PlatformException] or [FormatException] + /// from [MethodChannel]. + static Future shareFiles( + List paths, { + List? mimeTypes, + String? subject, + String? text, + Rect? sharePositionOrigin, + }) { + assert(paths != null); + assert(paths.isNotEmpty); + assert(paths.every((element) => element != null && element.isNotEmpty)); + final Map params = { + 'paths': paths, + 'mimeTypes': mimeTypes ?? + paths.map((String path) => _mimeTypeForPath(path)).toList(), + }; + + if (subject != null) params['subject'] = subject; + if (text != null) params['text'] = text; + + if (sharePositionOrigin != null) { + params['originX'] = sharePositionOrigin.left; + params['originY'] = sharePositionOrigin.top; + params['originWidth'] = sharePositionOrigin.width; + params['originHeight'] = sharePositionOrigin.height; + } + + return channel.invokeMethod('shareFiles', params); + } + + static String _mimeTypeForPath(String path) { + assert(path != null); + return lookupMimeType(path) ?? 'application/octet-stream'; + } } diff --git a/packages/share/pubspec.yaml b/packages/share/pubspec.yaml index 9a944e3ec4c7..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.4+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,18 +19,14 @@ flutter: pluginClass: FLTSharePlugin dependencies: - meta: ^1.0.5 + 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 - e2e: ^0.2.0 - 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/share/test/share_e2e.dart b/packages/share/test/share_e2e.dart deleted file mode 100644 index eb990222b009..000000000000 --- a/packages/share/test/share_e2e.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:e2e/e2e.dart'; - -void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('Can launch share', (WidgetTester tester) async { - expect(Share.share('message', subject: 'title'), completes); - }); -} diff --git a/packages/share/test/share_test.dart b/packages/share/test/share_test.dart index c03f8fb439df..d0049cef94ab 100644 --- a/packages/share/test/share_test.dart +++ b/packages/share/test/share_test.dart @@ -1,44 +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 { @@ -47,15 +37,81 @@ 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, - })); + + expect( + fakeChannel.invocation, + equals({ + 'share': { + 'text': 'some text to share', + 'subject': 'some subject to share', + 'originX': 1.0, + 'originY': 2.0, + 'originWidth': 3.0, + 'originHeight': 4.0, + } + }), + ); + }); + + test('sharing empty file fails', () { + expect( + () => Share.shareFiles(['']), + throwsA(isA()), + ); + expect(fakeChannel.invocation, isNull); + }); + + test('sharing file sets correct mimeType', () async { + final String path = 'tempfile-83649a.png'; + final File file = File(path); + try { + file.createSync(); + + await Share.shareFiles([path]); + + expect( + fakeChannel.invocation, + equals({ + 'shareFiles': { + 'paths': [path], + 'mimeTypes': ['image/png'], + } + }), + ); + } finally { + file.deleteSync(); + } + }); + + test('sharing file sets passed mimeType', () async { + final String path = 'tempfile-83649a.png'; + final File file = File(path); + try { + file.createSync(); + + await Share.shareFiles([path], mimeTypes: ['*/*']); + + expect( + fakeChannel.invocation, + equals({ + 'shareFiles': { + 'paths': [file.path], + 'mimeTypes': ['*/*'], + } + }), + ); + } finally { + file.deleteSync(); + } }); } -class 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 e8c1d1d1cb44..db12fd1829aa 100644 --- a/packages/shared_preferences/shared_preferences/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences/CHANGELOG.md @@ -1,3 +1,98 @@ +## 2.0.8 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. + +## 2.0.7 + +* Add iOS unit test target. +* Updated Android lint settings. +* Fix string clash with double entries on Android + +## 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. + +## 0.5.11 + +* Support Windows by default. + +## 0.5.10 + +* Update package:e2e -> package:integration_test + +## 0.5.9 + +* Update package:e2e reference to use the local version in the flutter/plugins + repository. + +## 0.5.8 + +* Support Linux by default. + +## 0.5.7+3 + +* Post-v2 Android embedding cleanup. + ## 0.5.7+2 * Update lower bound of dart dependency to 2.1.0. diff --git a/packages/shared_preferences/shared_preferences/LICENSE b/packages/shared_preferences/shared_preferences/LICENSE index 000b4618d2bd..c6823b81eb84 100644 --- a/packages/shared_preferences/shared_preferences/LICENSE +++ b/packages/shared_preferences/shared_preferences/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. \ No newline at end of file +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/README.md b/packages/shared_preferences/shared_preferences/README.md index b5bf4050a8fd..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 NSUserDefaults (on iOS) and SharedPreferences (on Android), providing -a persistent store for simple data. Data is persisted to disk asynchronously. -Neither platform can guarantee that writes will be persisted to disk after -returning and 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 +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. ## 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..9284f1c36143 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 @@ -38,5 +38,24 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' + baseline file("lint-baseline.xml") + } + dependencies { + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-inline:3.9.0' + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } } } diff --git a/packages/shared_preferences/shared_preferences/android/gradle.properties b/packages/shared_preferences/shared_preferences/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/shared_preferences/shared_preferences/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M 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/lint-baseline.xml b/packages/shared_preferences/shared_preferences/android/lint-baseline.xml new file mode 100644 index 000000000000..6b2f35f5a151 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/android/lint-baseline.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..cea3f34b9b96 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 @@ -75,7 +86,9 @@ public void onMethodCall(MethodCall call, MethodChannel.Result result) { break; case "setString": String value = (String) call.argument("value"); - if (value.startsWith(LIST_IDENTIFIER) || value.startsWith(BIG_INTEGER_PREFIX)) { + if (value.startsWith(LIST_IDENTIFIER) + || value.startsWith(BIG_INTEGER_PREFIX) + || value.startsWith(DOUBLE_PREFIX)) { result.error( "StorageError", "This string cannot be stored as it clashes with special identifier prefixes.", @@ -116,19 +129,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 859610e3d66a..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. @@ -8,14 +8,15 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.PluginRegistry; /** SharedPreferencesPlugin */ public class SharedPreferencesPlugin implements FlutterPlugin { private static final String CHANNEL_NAME = "plugins.flutter.io/shared_preferences"; private MethodChannel channel; + private MethodCallHandlerImpl handler; - public static void registerWith(PluginRegistry.Registrar registrar) { + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { final SharedPreferencesPlugin plugin = new SharedPreferencesPlugin(); plugin.setupChannel(registrar.messenger(), registrar.context()); } @@ -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/android/src/test/java/io/flutter/plugins/sharedpreferences/SharedPreferencesTest.java b/packages/shared_preferences/shared_preferences/android/src/test/java/io/flutter/plugins/sharedpreferences/SharedPreferencesTest.java new file mode 100644 index 000000000000..13d0ff8b40c1 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/android/src/test/java/io/flutter/plugins/sharedpreferences/SharedPreferencesTest.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.sharedpreferences; + +import org.junit.Test; + +public class SharedPreferencesTest { + // This is only a placeholder test and doesn't actually initialize the plugin. + @Test + public void initPluginDoesNotThrow() { + final SharedPreferencesPlugin plugin = new SharedPreferencesPlugin(); + } +} 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 abb4a67db50c..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,25 +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 68bb56444b65..000000000000 --- a/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/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.sharedpreferencesexample; - -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; - -public class EmbeddingV1Activity extends FlutterActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); - } -} 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 3eb677b21163..000000000000 --- a/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,14 +0,0 @@ - -package io.flutter.plugins.sharedpreferencesexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -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/MainActivity.java b/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/MainActivity.java deleted file mode 100644 index f40d0035c135..000000000000 --- a/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/MainActivity.java +++ /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. - -package io.flutter.plugins.sharedpreferencesexample; - -import io.flutter.embedding.android.FlutterActivity; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin; - -public class MainActivity extends FlutterActivity { - // TODO(cyanglaz): Remove this once v2 of GeneratedPluginRegistrant rolls to stable. - // https://github.com/flutter/flutter/issues/42694 - @Override - public void configureFlutterEngine(FlutterEngine flutterEngine) { - super.configureFlutterEngine(flutterEngine); - flutterEngine.getPlugins().add(new SharedPreferencesPlugin()); - } -} diff --git a/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/MainActivityTest.java b/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/MainActivityTest.java deleted file mode 100644 index dc6c826bca19..000000000000 --- a/packages/shared_preferences/shared_preferences/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/MainActivityTest.java +++ /dev/null @@ -1,15 +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.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class MainActivityTest { - @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.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..50cad6f36e24 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/android/app/src/main/kotlin/io/flutter/plugins/example/MainActivity.kt @@ -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. + +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/ios_platform_images/example/android/app/bin/src/main/res/drawable/launch_background.xml b/packages/shared_preferences/shared_preferences/example/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from packages/ios_platform_images/example/android/app/bin/src/main/res/drawable/launch_background.xml rename to packages/shared_preferences/shared_preferences/example/android/app/src/main/res/drawable/launch_background.xml 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 new file mode 100644 index 000000000000..e8498f473a2c --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/integration_test/shared_preferences_test.dart @@ -0,0 +1,142 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('$SharedPreferences', () { + const Map kTestValues = { + 'flutter.String': 'hello world', + 'flutter.bool': true, + 'flutter.int': 42, + 'flutter.double': 3.14159, + 'flutter.List': ['foo', 'bar'], + }; + + const Map kTestValues2 = { + 'flutter.String': 'goodbye world', + 'flutter.bool': false, + 'flutter.int': 1337, + 'flutter.double': 2.71828, + 'flutter.List': ['baz', 'quox'], + }; + + late SharedPreferences preferences; + + setUp(() async { + preferences = await SharedPreferences.getInstance(); + }); + + tearDown(() { + preferences.clear(); + }); + + testWidgets('reading', (WidgetTester _) 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('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']) + ]); + 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']); + }); + + testWidgets('removing', (WidgetTester _) async { + const String key = 'testKey'; + await preferences.setString(key, kTestValues['flutter.String']); + await preferences.setBool(key, kTestValues['flutter.bool']); + await preferences.setInt(key, kTestValues['flutter.int']); + await preferences.setDouble(key, kTestValues['flutter.double']); + await preferences.setStringList(key, kTestValues['flutter.List']); + await preferences.remove(key); + expect(preferences.get('testKey'), isNull); + }); + + testWidgets('clearing', (WidgetTester _) async { + await preferences.setString('String', kTestValues['flutter.String']); + await preferences.setBool('bool', kTestValues['flutter.bool']); + await preferences.setInt('int', kTestValues['flutter.int']); + await preferences.setDouble('double', kTestValues['flutter.double']); + await preferences.setStringList('List', kTestValues['flutter.List']); + await preferences.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); + }); + + 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); + }); + + testWidgets( + 'string clash with lists, big integers and doubles (Android only)', + (WidgetTester _) async { + await preferences.clear(); + // special prefixes plus a string value + expect( + // prefix for lists + preferences.setString( + 'String', + 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu' + + kTestValues2['flutter.String']), + throwsA(isA())); + await preferences.reload(); + expect(preferences.getString('String'), null); + expect( + // prefix for big integers + preferences.setString( + 'String', + 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy' + + kTestValues2['flutter.String']), + throwsA(isA())); + await preferences.reload(); + expect(preferences.getString('String'), null); + expect( + // prefix for doubles + preferences.setString( + 'String', + 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu' + + kTestValues2['flutter.String']), + throwsA(isA())); + await preferences.reload(); + expect(preferences.getString('String'), null); + }, skip: !Platform.isAndroid); + }); +} diff --git a/packages/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/AppFrameworkInfo.plist b/packages/shared_preferences/shared_preferences/example/ios/Flutter/AppFrameworkInfo.plist index 6c2de8086bcd..3a9c234f96d4 100644 --- a/packages/shared_preferences/shared_preferences/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/shared_preferences/shared_preferences/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 8.0 + 9.0 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..c7567b312596 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; @@ -362,7 +443,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -372,7 +453,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -413,7 +493,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -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/e2e/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/e2e/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/espresso/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/espresso/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/espresso/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/espresso/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/espresso/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/espresso/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/espresso/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/espresso/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/espresso/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/espresso/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 1e6df16e581b..1cb0f185baf4 100644 --- a/packages/shared_preferences/shared_preferences/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences/example/pubspec.yaml @@ -1,23 +1,28 @@ name: shared_preferences_example description: Demonstrates how to use the shared_preferences plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" 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 - e2e: ^0.2.0 - pedantic: ^1.8.0 + integration_test: + 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 new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/shared_preferences/shared_preferences/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/shared_preferences/shared_preferences/example/test_driver/shared_preferences_e2e.dart b/packages/shared_preferences/shared_preferences/example/test_driver/shared_preferences_e2e.dart deleted file mode 100644 index b693df2131ed..000000000000 --- a/packages/shared_preferences/shared_preferences/example/test_driver/shared_preferences_e2e.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'dart:async'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:e2e/e2e.dart'; - -void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); - - group('$SharedPreferences', () { - const Map kTestValues = { - 'flutter.String': 'hello world', - 'flutter.bool': true, - 'flutter.int': 42, - 'flutter.double': 3.14159, - 'flutter.List': ['foo', 'bar'], - }; - - const Map kTestValues2 = { - 'flutter.String': 'goodbye world', - 'flutter.bool': false, - 'flutter.int': 1337, - 'flutter.double': 2.71828, - 'flutter.List': ['baz', 'quox'], - }; - - SharedPreferences preferences; - - setUp(() async { - preferences = await SharedPreferences.getInstance(); - }); - - 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); - }); - - test('writing', () async { - await Future.wait(>[ - preferences.setString('String', kTestValues2['flutter.String']), - preferences.setBool('bool', kTestValues2['flutter.bool']), - preferences.setInt('int', kTestValues2['flutter.int']), - preferences.setDouble('double', kTestValues2['flutter.double']), - preferences.setStringList('List', kTestValues2['flutter.List']) - ]); - 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']); - }); - - 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']); - await preferences.remove(key); - expect(preferences.get('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']); - 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); - }); - }); -} diff --git a/packages/shared_preferences/shared_preferences/example/test_driver/shared_preferences_e2e_test.dart b/packages/shared_preferences/shared_preferences/example/test_driver/shared_preferences_e2e_test.dart deleted file mode 100644 index f3aa9e218d82..000000000000 --- a/packages/shared_preferences/shared_preferences/example/test_driver/shared_preferences_e2e_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 'dart:async'; -import 'dart:io'; -import 'package:flutter_driver/flutter_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); -} 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/ios/shared_preferences.podspec b/packages/shared_preferences/shared_preferences/ios/shared_preferences.podspec index bd239ec5a632..0cb5d35e1dd0 100644 --- a/packages/shared_preferences/shared_preferences/ios/shared_preferences.podspec +++ b/packages/shared_preferences/shared_preferences/ios/shared_preferences.podspec @@ -17,7 +17,7 @@ Wraps NSUserDefaults, providing a persistent store for simple key-value pairs. 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.platform = :ios, '9.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart index 62160dee20fd..841d615262de 100644 --- a/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart +++ b/packages/shared_preferences/shared_preferences/lib/shared_preferences.dart @@ -1,12 +1,16 @@ -// 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' show Platform; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:meta/meta.dart'; - +import 'package:shared_preferences_linux/shared_preferences_linux.dart'; +import 'package:shared_preferences_platform_interface/method_channel_shared_preferences.dart'; import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; +import 'package:shared_preferences_windows/shared_preferences_windows.dart'; /// Wraps NSUserDefaults (on iOS) and SharedPreferences (on Android), providing /// a persistent store for simple data. @@ -16,10 +20,29 @@ class SharedPreferences { SharedPreferences._(this._preferenceCache); static const String _prefix = 'flutter.'; - static Completer _completer; + static Completer? _completer; + static bool _manualDartRegistrationNeeded = true; + + static SharedPreferencesStorePlatform get _store { + // TODO(egarciad): Remove once auto registration lands on Flutter stable. + // https://github.com/flutter/flutter/issues/81421. + if (_manualDartRegistrationNeeded) { + // Only do the initial registration if it hasn't already been overridden + // with a non-default instance. + if (!kIsWeb && + SharedPreferencesStorePlatform.instance + is MethodChannelSharedPreferencesStore) { + if (Platform.isLinux) { + SharedPreferencesStorePlatform.instance = SharedPreferencesLinux(); + } else if (Platform.isWindows) { + SharedPreferencesStorePlatform.instance = SharedPreferencesWindows(); + } + } + _manualDartRegistrationNeeded = false; + } - static SharedPreferencesStorePlatform get _store => - SharedPreferencesStorePlatform.instance; + return SharedPreferencesStorePlatform.instance; + } /// Loads and parses the [SharedPreferences] for this app from disk. /// @@ -27,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. @@ -58,86 +82,83 @@ 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]. + /// Note: Due to limitations in Android's SharedPreferences, + /// values cannot start with any one of the following: + /// + /// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBhIGxpc3Qu' + /// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBCaWdJbnRlZ2Vy' + /// - 'VGhpcyBpcyB0aGUgcHJlZml4IGZvciBEb3VibGUu' 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. @@ -169,7 +190,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; } @@ -178,14 +199,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 3cab4b056d5e..1e59edf1e12e 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.7+2 +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.8 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" flutter: plugin: @@ -15,33 +17,35 @@ flutter: pluginClass: SharedPreferencesPlugin ios: pluginClass: FLTSharedPreferencesPlugin + linux: + default_package: shared_preferences_linux macos: 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_macos: ^0.0.1 - shared_preferences_web: ^0.1.2 + 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_driver: + sdk: flutter flutter_test: sdk: flutter - flutter_driver: + integration_test: sdk: flutter - test: any - e2e: ^0.2.0 - 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/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/.gitignore b/packages/shared_preferences/shared_preferences_linux/.gitignore new file mode 100644 index 000000000000..e9dc58d3d6e2 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_linux/.gitignore @@ -0,0 +1,7 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ diff --git a/packages/shared_preferences/shared_preferences_linux/.metadata b/packages/shared_preferences/shared_preferences_linux/.metadata new file mode 100644 index 000000000000..9615744e96d1 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_linux/.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: e491544588e8d34fdf31d5f840b4649850ef167a + channel: master + +project_type: plugin 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 new file mode 100644 index 000000000000..fc09bec23591 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md @@ -0,0 +1,42 @@ +## 2.0.2 + +* Updated installation instructions in README. + +## 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`. + +## 0.0.2+1 +* Replace path_provider dependency with path_provider_linux. + +## 0.0.2 +* Add iOS stub. + +## 0.0.1 +* Initial release to support shared_preferences on Linux. diff --git a/packages/shared_preferences/shared_preferences_linux/LICENSE b/packages/shared_preferences/shared_preferences_linux/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_linux/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/shared_preferences/shared_preferences_linux/README.md b/packages/shared_preferences/shared_preferences_linux/README.md new file mode 100644 index 000000000000..1a4ef3781b7e --- /dev/null +++ b/packages/shared_preferences/shared_preferences_linux/README.md @@ -0,0 +1,11 @@ +# shared\_preferences\_linux + +The Linux implementation of [`shared_preferences`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `shared_preferences` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/shared_preferences +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/shared_preferences/shared_preferences_linux/example/.gitignore b/packages/shared_preferences/shared_preferences_linux/example/.gitignore new file mode 100644 index 000000000000..1ba9c339effb --- /dev/null +++ b/packages/shared_preferences/shared_preferences_linux/example/.gitignore @@ -0,0 +1,43 @@ +# 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/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Exceptions to above rules. +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/packages/shared_preferences/shared_preferences_linux/example/.metadata b/packages/shared_preferences/shared_preferences_linux/example/.metadata new file mode 100644 index 000000000000..c0bc9a90268a --- /dev/null +++ b/packages/shared_preferences/shared_preferences_linux/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: e491544588e8d34fdf31d5f840b4649850ef167a + channel: master + +project_type: app diff --git a/packages/shared_preferences/shared_preferences_linux/example/README.md b/packages/shared_preferences/shared_preferences_linux/example/README.md new file mode 100644 index 000000000000..7dd9e9c4aa42 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_linux/example/README.md @@ -0,0 +1,8 @@ +# shared_preferences_example + +Demonstrates how to use the shared_preferences plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.dev/). diff --git a/packages/shared_preferences/shared_preferences_linux/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_linux/example/integration_test/shared_preferences_test.dart new file mode 100644 index 000000000000..5dba3def31d0 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_linux/example/integration_test/shared_preferences_test.dart @@ -0,0 +1,100 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:shared_preferences_linux/shared_preferences_linux.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('SharedPreferencesLinux', () { + const Map kTestValues = { + 'flutter.String': 'hello world', + 'flutter.bool': true, + 'flutter.int': 42, + 'flutter.double': 3.14159, + 'flutter.List': ['foo', 'bar'], + }; + + const Map kTestValues2 = { + 'flutter.String': 'goodbye world', + 'flutter.bool': false, + 'flutter.int': 1337, + 'flutter.double': 2.71828, + 'flutter.List': ['baz', 'quox'], + }; + + late SharedPreferencesLinux preferences; + + setUp(() async { + preferences = SharedPreferencesLinux.instance; + }); + + tearDown(() { + preferences.clear(); + }); + + 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); + }); + + testWidgets('writing', (WidgetTester _) async { + await Future.wait(>[ + preferences.setValue( + 'String', 'String', kTestValues2['flutter.String']), + preferences.setValue('Bool', 'bool', kTestValues2['flutter.bool']), + preferences.setValue('Int', 'int', kTestValues2['flutter.int']), + preferences.setValue( + 'Double', 'double', kTestValues2['flutter.double']), + preferences.setValue('StringList', 'List', kTestValues2['flutter.List']) + ]); + final 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']); + }); + + testWidgets('removing', (WidgetTester _) async { + const String key = 'testKey'; + + await Future.wait([ + preferences.setValue('String', key, kTestValues['flutter.String']), + preferences.setValue('Bool', key, kTestValues['flutter.bool']), + preferences.setValue('Int', key, kTestValues['flutter.int']), + preferences.setValue('Double', key, kTestValues['flutter.double']), + preferences.setValue('StringList', key, kTestValues['flutter.List']) + ]); + await preferences.remove(key); + final all = await preferences.getAll(); + expect(all['testKey'], isNull); + }); + + testWidgets('clearing', (WidgetTester _) async { + await Future.wait(>[ + preferences.setValue('String', 'String', kTestValues['flutter.String']), + preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']), + preferences.setValue('Int', 'int', kTestValues['flutter.int']), + preferences.setValue('Double', 'double', kTestValues['flutter.double']), + preferences.setValue('StringList', 'List', kTestValues['flutter.List']) + ]); + await preferences.clear(); + 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 new file mode 100644 index 000000000000..aee71d00d44d --- /dev/null +++ b/packages/shared_preferences/shared_preferences_linux/example/lib/main.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. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:shared_preferences_linux/shared_preferences_linux.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'SharedPreferences Demo', + home: SharedPreferencesDemo(), + ); + } +} + +class SharedPreferencesDemo extends StatefulWidget { + SharedPreferencesDemo({Key? key}) : super(key: key); + + @override + SharedPreferencesDemoState createState() => SharedPreferencesDemoState(); +} + +class SharedPreferencesDemoState extends State { + final prefs = SharedPreferencesLinux.instance; + late Future _counter; + + Future _incrementCounter() async { + final values = await prefs.getAll(); + final int counter = (values['counter'] as int? ?? 0) + 1; + + setState(() { + _counter = prefs.setValue('Int', 'counter', counter).then((bool success) { + return counter; + }); + }); + } + + @override + void initState() { + super.initState(); + _counter = prefs.getAll().then((Map values) { + return (values['counter'] as int? ?? 0); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("SharedPreferences Demo"), + ), + body: Center( + child: FutureBuilder( + future: _counter, + builder: (BuildContext context, AsyncSnapshot snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.waiting: + return const CircularProgressIndicator(); + default: + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } else { + return Text( + 'Button tapped ${snapshot.data} time${snapshot.data == 1 ? '' : 's'}.\n\n' + 'This should persist across restarts.', + ); + } + } + })), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/packages/shared_preferences/shared_preferences_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/CMakeLists.txt b/packages/shared_preferences/shared_preferences_linux/example/linux/CMakeLists.txt new file mode 100644 index 000000000000..0236a8806654 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_linux/example/linux/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +set(BINARY_NAME "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) + +# 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) + +# 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_linux/example/linux/flutter/CMakeLists.txt b/packages/shared_preferences/shared_preferences_linux/example/linux/flutter/CMakeLists.txt new file mode 100644 index 000000000000..94f43ff7fa6a --- /dev/null +++ b/packages/shared_preferences/shared_preferences_linux/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,86 @@ +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + linux-x64 ${CMAKE_BUILD_TYPE} +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) 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 new file mode 100644 index 000000000000..e71a16d23d05 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/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 new file mode 100644 index 000000000000..e0f0a47bc08f --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_linux/example/linux/flutter/generated_plugins.cmake b/packages/shared_preferences/shared_preferences_linux/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..51436ae8c982 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_linux/example/linux/main.cc b/packages/shared_preferences/shared_preferences_linux/example/linux/main.cc new file mode 100644 index 000000000000..1507d02825e7 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_linux/example/linux/main.cc @@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/packages/shared_preferences/shared_preferences_linux/example/linux/my_application.cc b/packages/shared_preferences/shared_preferences_linux/example/linux/my_application.cc new file mode 100644 index 000000000000..878cd973d997 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_linux/example/linux/my_application.cc @@ -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. + +#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(), nullptr)); +} 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 new file mode 100644 index 000000000000..6e9f0c3ff665 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_linux/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml new file mode 100644 index 000000000000..d34973b9dde6 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_linux/example/pubspec.yaml @@ -0,0 +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_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: ../ + +dev_dependencies: + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + pedantic: ^1.10.0 + +flutter: + uses-material-design: true diff --git a/packages/shared_preferences/shared_preferences_linux/example/test_driver/integration_test.dart b/packages/shared_preferences/shared_preferences_linux/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_linux/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart b/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.dart new file mode 100644 index 000000000000..5ec988216074 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_linux/lib/shared_preferences_linux.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 'dart:async'; +import 'dart:convert' show json; + +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider_linux/path_provider_linux.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; + +/// The Linux implementation of [SharedPreferencesStorePlatform]. +/// +/// 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; + + /// 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 { + final pathProvider = PathProviderLinux(); + final directory = await pathProvider.getApplicationSupportPath(); + if (directory == null) return null; + return 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) { + 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(); + } + } + _cachedPreferences = preferences; + return preferences; + } + + /// Writes the cached preferences to disk. Returns [true] if the operation + /// succeeded. + 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); + } + var stringMap = json.encode(preferences); + localDataFile.writeAsStringSync(stringMap); + } catch (e) { + print("Error saving preferences to disk: $e"); + return false; + } + return true; + } + + @override + Future clear() async { + var preferences = await _readPreferences(); + preferences.clear(); + return _writePreferences(preferences); + } + + @override + Future> getAll() async { + return _readPreferences(); + } + + @override + Future remove(String key) async { + var preferences = await _readPreferences(); + preferences.remove(key); + return _writePreferences(preferences); + } + + @override + Future setValue(String valueType, String key, Object value) async { + var preferences = await _readPreferences(); + preferences[key] = value; + return _writePreferences(preferences); + } +} diff --git a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml new file mode 100644 index 000000000000..c03e49e042e2 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml @@ -0,0 +1,31 @@ +name: shared_preferences_linux +description: Linux implementation of the shared_preferences plugin +repository: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_linux +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+shared_preferences%22 +version: 2.0.2 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +flutter: + plugin: + implements: shared_preferences + platforms: + linux: + dartPluginClass: SharedPreferencesLinux + pluginClass: none + +dependencies: + file: ^6.0.0 + meta: ^1.3.0 + flutter: + sdk: flutter + path: ^1.8.0 + path_provider_linux: ^2.0.0 + shared_preferences_platform_interface: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + pedantic: ^1.10.0 diff --git a/packages/shared_preferences/shared_preferences_linux/test/shared_preferences_linux_test.dart b/packages/shared_preferences/shared_preferences_linux/test/shared_preferences_linux_test.dart new file mode 100644 index 000000000000..62ec2b66c07a --- /dev/null +++ b/packages/shared_preferences/shared_preferences_linux/test/shared_preferences_linux_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:file/memory.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider_linux/path_provider_linux.dart'; +import 'package:shared_preferences_linux/shared_preferences_linux.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; + +void main() { + late MemoryFileSystem fs; + + SharedPreferencesLinux.registerWith(); + + setUp(() { + fs = MemoryFileSystem.test(); + }); + + Future _getFilePath() async { + final pathProvider = PathProviderLinux(); + final directory = await pathProvider.getApplicationSupportPath(); + return path.join(directory!, 'shared_preferences.json'); + } + + _writeTestFile(String value) async { + fs.file(await _getFilePath()) + ..createSync(recursive: true) + ..writeAsStringSync(value); + } + + Future _readTestFile() async { + return fs.file(await _getFilePath()).readAsStringSync(); + } + + SharedPreferencesLinux _getPreferences() { + var prefs = SharedPreferencesLinux(); + prefs.fs = fs; + return prefs; + } + + test('registered instance', () { + expect( + SharedPreferencesStorePlatform.instance, isA()); + }); + + test('getAll', () async { + await _writeTestFile('{"key1": "one", "key2": 2}'); + var prefs = _getPreferences(); + + var values = await prefs.getAll(); + expect(values, hasLength(2)); + expect(values['key1'], 'one'); + expect(values['key2'], 2); + }); + + test('remove', () async { + await _writeTestFile('{"key1":"one","key2":2}'); + var prefs = _getPreferences(); + + await prefs.remove('key2'); + + expect(await _readTestFile(), '{"key1":"one"}'); + }); + + test('setValue', () async { + await _writeTestFile('{}'); + var prefs = _getPreferences(); + + await prefs.setValue('', 'key1', 'one'); + await prefs.setValue('', 'key2', 2); + + expect(await _readTestFile(), '{"key1":"one","key2":2}'); + }); + + test('clear', () async { + await _writeTestFile('{"key1":"one","key2":2}'); + var prefs = _getPreferences(); + + await prefs.clear(); + expect(await _readTestFile(), '{}'); + }); +} 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 fb190899d375..2f7e0edf9a51 100644 --- a/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_macos/CHANGELOG.md @@ -1,3 +1,33 @@ +## 2.0.2 + +* Add native unit tests. +* Updated installation instructions in README. + +## 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. + +## 0.0.1+9 + +* Remove Android folder from `shared_preferences_macos`. + ## 0.0.1+8 * Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). diff --git a/packages/shared_preferences/shared_preferences_macos/LICENSE b/packages/shared_preferences/shared_preferences_macos/LICENSE index 566f5b5e7c78..c6823b81eb84 100644 --- a/packages/shared_preferences/shared_preferences_macos/LICENSE +++ b/packages/shared_preferences/shared_preferences_macos/LICENSE @@ -1,7 +1,7 @@ -Copyright 2017, the Flutter project 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: +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. @@ -13,14 +13,13 @@ met: contributors may be used to endorse or promote products derived from this software without specific prior written permission. -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file +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_macos/README.md b/packages/shared_preferences/shared_preferences_macos/README.md index c2949c9f5d33..e9cd7f25be03 100644 --- a/packages/shared_preferences/shared_preferences_macos/README.md +++ b/packages/shared_preferences/shared_preferences_macos/README.md @@ -1,41 +1,11 @@ -# shared_preferences_macos +# shared\_preferences\_macos 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 - -This package has been endorsed, meaning that you only need to add `shared_preferences` -as a dependency in your `pubspec.yaml`. It will be automatically included in your app -when you depend on `package:shared_preferences`. - -This is what the above means to your `pubspec.yaml`: - -```yaml -... -dependencies: - ... - shared_preferences: ^0.5.6 - ... -``` - -If you wish to use the macos package only, you can add `shared_preferences_macos` as a -dependency: - -```yaml -... -dependencies: - ... - shared_preferences_macos: ^0.0.1 - ... -``` +This package is [endorsed][2], which means you can simply use `shared_preferences` +normally. This package will be automatically included in your app when you do. -[1]: ../ +[1]: https://pub.dev/packages/shared_preferences +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/shared_preferences/shared_preferences_macos/android/.gitignore b/packages/shared_preferences/shared_preferences_macos/android/.gitignore deleted file mode 100644 index c6cbe562a427..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/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/shared_preferences/shared_preferences_macos/android/build.gradle b/packages/shared_preferences/shared_preferences_macos/android/build.gradle deleted file mode 100644 index 7895628fc9ba..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/android/build.gradle +++ /dev/null @@ -1,33 +0,0 @@ -group 'io.flutter.plugins.shared_preferences_macos' -version '1.0' - -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - } -} - -rootProject.allprojects { - repositories { - google() - jcenter() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 28 - - defaultConfig { - minSdkVersion 16 - } - lintOptions { - disable 'InvalidPackage' - } -} diff --git a/packages/shared_preferences/shared_preferences_macos/android/gradle.properties b/packages/shared_preferences/shared_preferences_macos/android/gradle.properties deleted file mode 100644 index 7be3d8b46841..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/android/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true diff --git a/packages/shared_preferences/shared_preferences_macos/android/gradle/wrapper/gradle-wrapper.properties b/packages/shared_preferences/shared_preferences_macos/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index caf54fa2801c..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip diff --git a/packages/shared_preferences/shared_preferences_macos/android/settings.gradle b/packages/shared_preferences/shared_preferences_macos/android/settings.gradle deleted file mode 100644 index 0a474afd7d49..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'shared_preferences_macos' diff --git a/packages/shared_preferences/shared_preferences_macos/android/src/main/AndroidManifest.xml b/packages/shared_preferences/shared_preferences_macos/android/src/main/AndroidManifest.xml deleted file mode 100644 index 46f5304a3084..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/packages/shared_preferences/shared_preferences_macos/android/src/main/java/io/flutter/plugins/shared_preferences_macos/SharedPreferencesMacosPlugin.java b/packages/shared_preferences/shared_preferences_macos/android/src/main/java/io/flutter/plugins/shared_preferences_macos/SharedPreferencesMacosPlugin.java deleted file mode 100644 index 04d5598eeb06..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/android/src/main/java/io/flutter/plugins/shared_preferences_macos/SharedPreferencesMacosPlugin.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.flutter.plugins.shared_preferences_macos; - -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.PluginRegistry.Registrar; - -/** SharedPreferencesMacosPlugin */ -public class SharedPreferencesMacosPlugin implements FlutterPlugin { - @Override - public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) {} - - public static void registerWith(Registrar registrar) {} - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) {} -} 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/android/app/build.gradle b/packages/shared_preferences/shared_preferences_macos/example/android/app/build.gradle deleted file mode 100644 index 7a285ba704ab..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/example/android/app/build.gradle +++ /dev/null @@ -1,60 +0,0 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -apply plugin: 'com.android.application' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -android { - compileSdkVersion 28 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - applicationId "io.flutter.plugins.sharedpreferencesexample" - minSdkVersion 16 - targetSdkVersion 28 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} - -dependencies { - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' -} diff --git a/packages/shared_preferences/shared_preferences_macos/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/shared_preferences/shared_preferences_macos/example/android/app/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index ee69dd68d1a6..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/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_macos/example/android/app/src/main/AndroidManifest.xml b/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index abb4a67db50c..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1Activity.java b/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1Activity.java deleted file mode 100644 index 68bb56444b65..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/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.sharedpreferencesexample; - -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; - -public class EmbeddingV1Activity extends FlutterActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); - } -} diff --git a/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1ActivityTest.java b/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1ActivityTest.java deleted file mode 100644 index 3eb677b21163..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,14 +0,0 @@ - -package io.flutter.plugins.sharedpreferencesexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/MainActivity.java b/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/MainActivity.java deleted file mode 100644 index f40d0035c135..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/MainActivity.java +++ /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. - -package io.flutter.plugins.sharedpreferencesexample; - -import io.flutter.embedding.android.FlutterActivity; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin; - -public class MainActivity extends FlutterActivity { - // TODO(cyanglaz): Remove this once v2 of GeneratedPluginRegistrant rolls to stable. - // https://github.com/flutter/flutter/issues/42694 - @Override - public void configureFlutterEngine(FlutterEngine flutterEngine) { - super.configureFlutterEngine(flutterEngine); - flutterEngine.getPlugins().add(new SharedPreferencesPlugin()); - } -} diff --git a/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/MainActivityTest.java b/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/MainActivityTest.java deleted file mode 100644 index dc6c826bca19..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/java/io/flutter/plugins/sharedpreferencesexample/MainActivityTest.java +++ /dev/null @@ -1,15 +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.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class MainActivityTest { - @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); -} diff --git a/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4b7b09..000000000000 Binary files a/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17987b79bb8a..000000000000 Binary files a/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 09d4391482be..000000000000 Binary files a/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d34e7a..000000000000 Binary files a/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372eebdb2..000000000000 Binary files a/packages/shared_preferences/shared_preferences_macos/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/shared_preferences/shared_preferences_macos/example/android/build.gradle b/packages/shared_preferences/shared_preferences_macos/example/android/build.gradle deleted file mode 100644 index 54cc96612793..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/example/android/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.4.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/shared_preferences/shared_preferences_macos/example/android/gradle.properties b/packages/shared_preferences/shared_preferences_macos/example/android/gradle.properties deleted file mode 100644 index 7be3d8b46841..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/example/android/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true diff --git a/packages/shared_preferences/shared_preferences_macos/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/shared_preferences/shared_preferences_macos/example/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index d757f3d33fcc..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/example/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/packages/shared_preferences/shared_preferences_macos/example/android/settings.gradle b/packages/shared_preferences/shared_preferences_macos/example/android/settings.gradle deleted file mode 100644 index 115da6cb4f4d..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/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/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 new file mode 100644 index 000000000000..e7a267f7e51a --- /dev/null +++ b/packages/shared_preferences/shared_preferences_macos/example/integration_test/shared_preferences_test.dart @@ -0,0 +1,107 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('SharedPreferencesMacOS', () { + const Map kTestValues = { + 'flutter.String': 'hello world', + 'flutter.bool': true, + 'flutter.int': 42, + 'flutter.double': 3.14159, + 'flutter.List': ['foo', 'bar'], + }; + + const Map kTestValues2 = { + 'flutter.String': 'goodbye world', + 'flutter.bool': false, + 'flutter.int': 1337, + 'flutter.double': 2.71828, + 'flutter.List': ['baz', 'quox'], + }; + + late SharedPreferencesStorePlatform preferences; + + setUp(() async { + preferences = SharedPreferencesStorePlatform.instance; + }); + + tearDown(() { + preferences.clear(); + }); + + // Normally the app-facing package adds the prefix, but since this test + // bypasses the app-facing package it needs to be manually added. + String _prefixedKey(String key) { + return 'flutter.$key'; + } + + testWidgets('reading', (WidgetTester _) async { + final Map values = await preferences.getAll(); + expect(values[_prefixedKey('String')], isNull); + expect(values[_prefixedKey('bool')], isNull); + expect(values[_prefixedKey('int')], isNull); + expect(values[_prefixedKey('double')], isNull); + expect(values[_prefixedKey('List')], isNull); + }); + + testWidgets('writing', (WidgetTester _) async { + await Future.wait(>[ + preferences.setValue( + 'String', _prefixedKey('String'), kTestValues2['flutter.String']), + preferences.setValue( + 'Bool', _prefixedKey('bool'), kTestValues2['flutter.bool']), + preferences.setValue( + 'Int', _prefixedKey('int'), kTestValues2['flutter.int']), + preferences.setValue( + 'Double', _prefixedKey('double'), kTestValues2['flutter.double']), + preferences.setValue( + 'StringList', _prefixedKey('List'), kTestValues2['flutter.List']) + ]); + final Map values = await preferences.getAll(); + expect(values[_prefixedKey('String')], kTestValues2['flutter.String']); + expect(values[_prefixedKey('bool')], kTestValues2['flutter.bool']); + expect(values[_prefixedKey('int')], kTestValues2['flutter.int']); + expect(values[_prefixedKey('double')], kTestValues2['flutter.double']); + expect(values[_prefixedKey('List')], kTestValues2['flutter.List']); + }); + + testWidgets('removing', (WidgetTester _) async { + final String key = _prefixedKey('testKey'); + await preferences.setValue('String', key, kTestValues['flutter.String']); + await preferences.setValue('Bool', key, kTestValues['flutter.bool']); + await preferences.setValue('Int', key, kTestValues['flutter.int']); + await preferences.setValue('Double', key, kTestValues['flutter.double']); + await preferences.setValue( + 'StringList', key, kTestValues['flutter.List']); + await preferences.remove(key); + final Map values = await preferences.getAll(); + expect(values[key], isNull); + }); + + testWidgets('clearing', (WidgetTester _) async { + await preferences.setValue( + 'String', 'String', kTestValues['flutter.String']); + await preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']); + await preferences.setValue('Int', 'int', kTestValues['flutter.int']); + await preferences.setValue( + 'Double', 'double', kTestValues['flutter.double']); + await preferences.setValue( + 'StringList', 'List', kTestValues['flutter.List']); + await preferences.clear(); + final Map values = await preferences.getAll(); + expect(values['String'], null); + expect(values['bool'], null); + expect(values['int'], null); + expect(values['double'], null); + expect(values['List'], null); + }); + }); +} diff --git a/packages/shared_preferences/shared_preferences_macos/example/ios/Flutter/AppFrameworkInfo.plist b/packages/shared_preferences/shared_preferences_macos/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 6c2de8086bcd..000000000000 --- a/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_macos/example/ios/Flutter/Debug.xcconfig b/packages/shared_preferences/shared_preferences_macos/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index 9803018ca79d..000000000000 --- a/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_macos/example/ios/Flutter/Release.xcconfig b/packages/shared_preferences/shared_preferences_macos/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index a4a8c604e13d..000000000000 --- a/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_macos/example/ios/Runner.xcodeproj/project.pbxproj b/packages/shared_preferences/shared_preferences_macos/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 2adc7021c6bf..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,490 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 46; - objects = { - -/* Begin PBXBuildFile section */ - 2D92224B1EC342E7007564B0 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D92224A1EC342E7007564B0 /* GeneratedPluginRegistrant.m */; }; - 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 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, ); }; }; - 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 */; }; -/* 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 */ - 081A3238A89B77A99B096D83 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 12E03CD14DABAA3AD3923183 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 2D9222491EC342E7007564B0 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 2D92224A1EC342E7007564B0 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 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 = ""; }; - 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 = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, - 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, - 66F8BCECCEFF62F4071D2DFC /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 840012C8B5EDBCF56B0E4AC1 /* Pods */ = { - isa = PBXGroup; - children = ( - 942E815CEF30E101E045B849 /* Pods-Runner.debug.xcconfig */, - 081A3238A89B77A99B096D83 /* Pods-Runner.release.xcconfig */, - ); - 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 = ( - 2D9222491EC342E7007564B0 /* GeneratedPluginRegistrant.h */, - 2D92224A1EC342E7007564B0 /* GeneratedPluginRegistrant.m */, - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, - ); - path = Runner; - sourceTree = ""; - }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - 97C146F21CF9000F007C117D /* main.m */, - ); - name = "Supporting Files"; - sourceTree = ""; - }; - CF3B75C9A7D2FA2A4C99F110 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 12E03CD14DABAA3AD3923183 /* libPods-Runner.a */, - ); - 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 */, - 2D92224B1EC342E7007564B0 /* 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.sharedPreferencesExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 97C147071CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = io.flutter.plugins.sharedPreferencesExample; - 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/shared_preferences/shared_preferences_macos/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/shared_preferences/shared_preferences_macos/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 3bb3697ef41c..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,87 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/AppDelegate.h b/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/AppDelegate.h deleted file mode 100644 index d9e18e990f2e..000000000000 --- a/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_macos/example/ios/Runner/AppDelegate.m b/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/AppDelegate.m deleted file mode 100644 index a4b51c88eb60..000000000000 --- a/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d22f10b2ab63..000000000000 --- a/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf03016f..000000000000 Binary files a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca8..000000000000 Binary files a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cde12118dda..000000000000 Binary files a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb8..000000000000 Binary files a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index dcdc2306c285..000000000000 Binary files a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee..000000000000 Binary files a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a9..000000000000 Binary files a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7..000000000000 Binary files a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 6a84f41e14e2..000000000000 Binary files a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_macos/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index ebf48f603974..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Base.lproj/Main.storyboard b/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516fb38..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Info.plist b/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/Info.plist deleted file mode 100644 index 22fc4c23715d..000000000000 --- a/packages/shared_preferences/shared_preferences_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 - shared_preferences_example - CFBundlePackageType - APPL - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1 - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIRequiredDeviceCapabilities - - arm64 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/main.m b/packages/shared_preferences/shared_preferences_macos/example/ios/Runner/main.m deleted file mode 100644 index bec320c0bee0..000000000000 --- a/packages/shared_preferences/shared_preferences_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/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 c31a0637253c..e6bf972cf5b4 100644 --- a/packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_macos/example/pubspec.yaml @@ -1,20 +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 - 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_macos/example/test_driver/integration_test.dart b/packages/shared_preferences/shared_preferences_macos/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_macos/example/test_driver/shared_preferences_e2e.dart b/packages/shared_preferences/shared_preferences_macos/example/test_driver/shared_preferences_e2e.dart deleted file mode 100644 index b693df2131ed..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/example/test_driver/shared_preferences_e2e.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'dart:async'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:e2e/e2e.dart'; - -void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); - - group('$SharedPreferences', () { - const Map kTestValues = { - 'flutter.String': 'hello world', - 'flutter.bool': true, - 'flutter.int': 42, - 'flutter.double': 3.14159, - 'flutter.List': ['foo', 'bar'], - }; - - const Map kTestValues2 = { - 'flutter.String': 'goodbye world', - 'flutter.bool': false, - 'flutter.int': 1337, - 'flutter.double': 2.71828, - 'flutter.List': ['baz', 'quox'], - }; - - SharedPreferences preferences; - - setUp(() async { - preferences = await SharedPreferences.getInstance(); - }); - - 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); - }); - - test('writing', () async { - await Future.wait(>[ - preferences.setString('String', kTestValues2['flutter.String']), - preferences.setBool('bool', kTestValues2['flutter.bool']), - preferences.setInt('int', kTestValues2['flutter.int']), - preferences.setDouble('double', kTestValues2['flutter.double']), - preferences.setStringList('List', kTestValues2['flutter.List']) - ]); - 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']); - }); - - 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']); - await preferences.remove(key); - expect(preferences.get('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']); - 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); - }); - }); -} diff --git a/packages/shared_preferences/shared_preferences_macos/example/test_driver/shared_preferences_e2e_test.dart b/packages/shared_preferences/shared_preferences_macos/example/test_driver/shared_preferences_e2e_test.dart deleted file mode 100644 index f3aa9e218d82..000000000000 --- a/packages/shared_preferences/shared_preferences_macos/example/test_driver/shared_preferences_e2e_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 'dart:async'; -import 'dart:io'; -import 'package:flutter_driver/flutter_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); -} 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 95a1383920e2..6e351e86fb1a 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+8 -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.2 + +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 000b4618d2bd..c6823b81eb84 100644 --- a/packages/shared_preferences/shared_preferences_platform_interface/LICENSE +++ b/packages/shared_preferences/shared_preferences_platform_interface/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. \ No newline at end of file +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_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 71e14ffb19d2..dd68f5321541 100644 --- a/packages/shared_preferences/shared_preferences_web/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_web/CHANGELOG.md @@ -1,3 +1,24 @@ +## 2.0.2 + +* Add `implements` to pubspec. + +## 2.0.1 + +* Updated installation instructions in README. +* Move tests to `example` directory, so they run as integration_tests with `flutter drive`. + +## 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`. + ## 0.1.2+6 * Update lower bound of dart dependency to 2.1.0. diff --git a/packages/shared_preferences/shared_preferences_web/LICENSE b/packages/shared_preferences/shared_preferences_web/LICENSE index 0c382ce171cc..c6823b81eb84 100644 --- a/packages/shared_preferences/shared_preferences_web/LICENSE +++ b/packages/shared_preferences/shared_preferences_web/LICENSE @@ -1,27 +1,25 @@ -// 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. +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_web/README.md b/packages/shared_preferences/shared_preferences_web/README.md index 2bb8a9316d86..5c3a51a3d9dc 100644 --- a/packages/shared_preferences/shared_preferences_web/README.md +++ b/packages/shared_preferences/shared_preferences_web/README.md @@ -1,39 +1,11 @@ -# shared_preferences_web +# shared\_preferences\_web 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 - -To use this plugin in your Flutter Web app, simply add it as a dependency in -your `pubspec.yaml` alongside the base `shared_preferences` plugin. - -_(This is only temporary: in the future we hope to make this package an -"endorsed" implementation of `shared_preferences`, so that it is automatically -included in your Flutter Web app when you depend on `package:shared_preferences`.)_ - -This is what the above means to your `pubspec.yaml`: - -```yaml -... -dependencies: - ... - shared_preferences: ^0.5.4+8 - shared_preferences_web: ^0.1.0 - ... -``` - -### Use the plugin - -Once you have the `shared_preferences_web` dependency in your pubspec, you should -be able to use `package:shared_preferences` as normal. +This package is [endorsed][2], which means you can simply use `shared_preferences` +normally. This package will be automatically included in your app when you do. -[1]: ../shared_preferences/shared_preferences +[1]: https://pub.dev/packages/shared_preferences +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/shared_preferences/shared_preferences_web/android/.gitignore b/packages/shared_preferences/shared_preferences_web/android/.gitignore deleted file mode 100644 index c6cbe562a427..000000000000 --- a/packages/shared_preferences/shared_preferences_web/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/shared_preferences/shared_preferences_web/android/build.gradle b/packages/shared_preferences/shared_preferences_web/android/build.gradle deleted file mode 100644 index c4ebf6bf891d..000000000000 --- a/packages/shared_preferences/shared_preferences_web/android/build.gradle +++ /dev/null @@ -1,33 +0,0 @@ -group 'io.flutter.plugins.shared_preferences_web' -version '1.0' - -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - } -} - -rootProject.allprojects { - repositories { - google() - jcenter() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 28 - - defaultConfig { - minSdkVersion 16 - } - lintOptions { - disable 'InvalidPackage' - } -} diff --git a/packages/shared_preferences/shared_preferences_web/android/gradle.properties b/packages/shared_preferences/shared_preferences_web/android/gradle.properties deleted file mode 100644 index 7be3d8b46841..000000000000 --- a/packages/shared_preferences/shared_preferences_web/android/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true diff --git a/packages/shared_preferences/shared_preferences_web/android/gradle/wrapper/gradle-wrapper.properties b/packages/shared_preferences/shared_preferences_web/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index caf54fa2801c..000000000000 --- a/packages/shared_preferences/shared_preferences_web/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip diff --git a/packages/shared_preferences/shared_preferences_web/android/settings.gradle b/packages/shared_preferences/shared_preferences_web/android/settings.gradle deleted file mode 100644 index 3d2f9c633e2f..000000000000 --- a/packages/shared_preferences/shared_preferences_web/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'shared_preferences_web' diff --git a/packages/shared_preferences/shared_preferences_web/android/src/main/AndroidManifest.xml b/packages/shared_preferences/shared_preferences_web/android/src/main/AndroidManifest.xml deleted file mode 100644 index 3f97ee01a9fb..000000000000 --- a/packages/shared_preferences/shared_preferences_web/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/packages/shared_preferences/shared_preferences_web/android/src/main/java/io/flutter/plugins/shared_preferences_web/SharedPreferencesWebPlugin.java b/packages/shared_preferences/shared_preferences_web/android/src/main/java/io/flutter/plugins/shared_preferences_web/SharedPreferencesWebPlugin.java deleted file mode 100644 index 74a757562cf6..000000000000 --- a/packages/shared_preferences/shared_preferences_web/android/src/main/java/io/flutter/plugins/shared_preferences_web/SharedPreferencesWebPlugin.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.flutter.plugins.shared_preferences_web; - -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.PluginRegistry.Registrar; - -/** SharedPreferencesWebPlugin */ -public class SharedPreferencesWebPlugin implements FlutterPlugin { - @Override - public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) {} - - public static void registerWith(Registrar registrar) {} - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) {} -} diff --git a/packages/shared_preferences/shared_preferences_web/example/README.md b/packages/shared_preferences/shared_preferences_web/example/README.md new file mode 100644 index 000000000000..4348451b14e2 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_web/example/README.md @@ -0,0 +1,9 @@ +# Testing + +This package uses `package:integration_test` to run its tests in a web browser. + +See [Plugin Tests > Web Tests](https://github.com/flutter/flutter/wiki/Plugin-Tests#web-tests) +in the Flutter wiki for instructions to setup and run the tests in this package. + +Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) +for more info. diff --git a/packages/shared_preferences/shared_preferences_web/test/shared_preferences_web_test.dart b/packages/shared_preferences/shared_preferences_web/example/integration_test/shared_preferences_web_test.dart similarity index 82% rename from packages/shared_preferences/shared_preferences_web/test/shared_preferences_web_test.dart rename to packages/shared_preferences/shared_preferences_web/example/integration_test/shared_preferences_web_test.dart index 951f04cbce5a..d95a0512615e 100644 --- a/packages/shared_preferences/shared_preferences_web/test/shared_preferences_web_test.dart +++ b/packages/shared_preferences/shared_preferences_web/example/integration_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 - import 'dart:convert' show json; import 'dart:html' as html; import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_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'; @@ -20,12 +20,16 @@ const Map kTestValues = { }; void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + group('SharedPreferencesPlugin', () { setUp(() { html.window.localStorage.clear(); }); - test('registers itself', () { + testWidgets('registers itself', (WidgetTester tester) async { + SharedPreferencesStorePlatform.instance = + MethodChannelSharedPreferencesStore(); expect(SharedPreferencesStorePlatform.instance, isNot(isA())); SharedPreferencesPlugin.registerWith(null); @@ -33,7 +37,7 @@ void main() { isA()); }); - test('getAll', () async { + testWidgets('getAll', (WidgetTester tester) async { final SharedPreferencesPlugin store = SharedPreferencesPlugin(); expect(await store.getAll(), isEmpty); @@ -44,7 +48,7 @@ void main() { expect(allData['flutter.testKey'], 'test value'); }); - test('remove', () async { + testWidgets('remove', (WidgetTester tester) async { final SharedPreferencesPlugin store = SharedPreferencesPlugin(); html.window.localStorage['flutter.testKey'] = '"test value"'; expect(html.window.localStorage['flutter.testKey'], isNotNull); @@ -56,7 +60,7 @@ void main() { ); }); - test('setValue', () async { + testWidgets('setValue', (WidgetTester tester) async { final SharedPreferencesPlugin store = SharedPreferencesPlugin(); for (String key in kTestValues.keys) { final dynamic value = kTestValues[key]; @@ -77,7 +81,7 @@ void main() { ); }); - test('clear', () async { + testWidgets('clear', (WidgetTester tester) async { final SharedPreferencesPlugin store = SharedPreferencesPlugin(); html.window.localStorage['flutter.testKey1'] = '"test value"'; html.window.localStorage['flutter.testKey2'] = '42'; diff --git a/packages/shared_preferences/shared_preferences_web/example/lib/main.dart b/packages/shared_preferences/shared_preferences_web/example/lib/main.dart new file mode 100644 index 000000000000..e1a38dcdcd46 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_web/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml new file mode 100644 index 000000000000..a83a71b40bf8 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_web/example/pubspec.yaml @@ -0,0 +1,21 @@ +name: shared_preferences_web_integration_tests +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.2.0" + +dependencies: + shared_preferences_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/shared_preferences/shared_preferences_web/example/run_test.sh b/packages/shared_preferences/shared_preferences_web/example/run_test.sh new file mode 100755 index 000000000000..aa52974f310e --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_web/example/test_driver/integration_test.dart b/packages/shared_preferences/shared_preferences_web/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_web/example/web/index.html b/packages/shared_preferences/shared_preferences_web/example/web/index.html new file mode 100644 index 000000000000..7fb138cc90fa --- /dev/null +++ b/packages/shared_preferences/shared_preferences_web/example/web/index.html @@ -0,0 +1,13 @@ + + + + + + example + + + + + 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 f50eb24ed810..c878903ac236 100644 --- a/packages/shared_preferences/shared_preferences_web/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_web/pubspec.yaml @@ -1,31 +1,30 @@ 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+6 +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.2 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: + implements: shared_preferences platforms: web: pluginClass: SharedPreferencesPlugin 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/README.md b/packages/shared_preferences/shared_preferences_web/test/README.md new file mode 100644 index 000000000000..7c5b4ad682ba --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_web/test/tests_exist_elsewhere_test.dart b/packages/shared_preferences/shared_preferences_web/test/tests_exist_elsewhere_test.dart new file mode 100644 index 000000000000..442c50144727 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_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/shared_preferences/shared_preferences_windows/.metadata b/packages/shared_preferences/shared_preferences_windows/.metadata new file mode 100644 index 000000000000..55d1df5ced9a --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: df90bb5fd64e2066594151b9e311d45cd687a80c + channel: master + +project_type: plugin 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 new file mode 100644 index 000000000000..7502ec917d80 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/CHANGELOG.md @@ -0,0 +1,44 @@ +## 2.0.2 + +* Updated installation instructions in README. + +## 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. + +## 0.0.1 + +* Initial release to support shared_preferences on Windows. diff --git a/packages/shared_preferences/shared_preferences_windows/LICENSE b/packages/shared_preferences/shared_preferences_windows/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/shared_preferences/shared_preferences_windows/README.md b/packages/shared_preferences/shared_preferences_windows/README.md new file mode 100644 index 000000000000..68341acf505e --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/README.md @@ -0,0 +1,11 @@ +# shared\_preferences\_windows + +The Windows implementation of [`shared_preferences`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `shared_preferences` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/shared_preferences +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/shared_preferences/shared_preferences_windows/example/.gitignore b/packages/shared_preferences/shared_preferences_windows/example/.gitignore new file mode 100644 index 000000000000..1ba9c339effb --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/example/.gitignore @@ -0,0 +1,43 @@ +# 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/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Exceptions to above rules. +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/packages/shared_preferences/shared_preferences_windows/example/.metadata b/packages/shared_preferences/shared_preferences_windows/example/.metadata new file mode 100644 index 000000000000..d39696748cee --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: df90bb5fd64e2066594151b9e311d45cd687a80c + channel: master + +project_type: app 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/LICENSE b/packages/shared_preferences/shared_preferences_windows/example/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/example/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/shared_preferences/shared_preferences_windows/example/README.md b/packages/shared_preferences/shared_preferences_windows/example/README.md new file mode 100644 index 000000000000..d85bb4107622 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/example/README.md @@ -0,0 +1,16 @@ +# shared_preferences_windows_example + +Demonstrates how to use the shared_preferences_windows plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/shared_preferences/shared_preferences_windows/example/integration_test/shared_preferences_test.dart b/packages/shared_preferences/shared_preferences_windows/example/integration_test/shared_preferences_test.dart new file mode 100644 index 000000000000..207d712650a7 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/example/integration_test/shared_preferences_test.dart @@ -0,0 +1,98 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences_windows/shared_preferences_windows.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('SharedPreferencesWindows', () { + const Map kTestValues = { + 'flutter.String': 'hello world', + 'flutter.bool': true, + 'flutter.int': 42, + 'flutter.double': 3.14159, + 'flutter.List': ['foo', 'bar'], + }; + + const Map kTestValues2 = { + 'flutter.String': 'goodbye world', + 'flutter.bool': false, + 'flutter.int': 1337, + 'flutter.double': 2.71828, + 'flutter.List': ['baz', 'quox'], + }; + + late SharedPreferencesWindows preferences; + + setUp(() async { + preferences = SharedPreferencesWindows.instance; + }); + + tearDown(() { + preferences.clear(); + }); + + testWidgets('reading', (WidgetTester _) async { + final Map values = await preferences.getAll(); + expect(values['String'], isNull); + expect(values['bool'], isNull); + expect(values['int'], isNull); + expect(values['double'], isNull); + expect(values['List'], isNull); + }); + + testWidgets('writing', (WidgetTester _) async { + await Future.wait(>[ + preferences.setValue( + 'String', 'String', kTestValues2['flutter.String']), + preferences.setValue('Bool', 'bool', kTestValues2['flutter.bool']), + preferences.setValue('Int', 'int', kTestValues2['flutter.int']), + preferences.setValue( + 'Double', 'double', kTestValues2['flutter.double']), + preferences.setValue('StringList', 'List', kTestValues2['flutter.List']) + ]); + final Map values = await preferences.getAll(); + expect(values['String'], kTestValues2['flutter.String']); + expect(values['bool'], kTestValues2['flutter.bool']); + expect(values['int'], kTestValues2['flutter.int']); + expect(values['double'], kTestValues2['flutter.double']); + expect(values['List'], kTestValues2['flutter.List']); + }); + + testWidgets('removing', (WidgetTester _) async { + const String key = 'testKey'; + await preferences.setValue('String', key, kTestValues['flutter.String']); + await preferences.setValue('Bool', key, kTestValues['flutter.bool']); + await preferences.setValue('Int', key, kTestValues['flutter.int']); + await preferences.setValue('Double', key, kTestValues['flutter.double']); + await preferences.setValue( + 'StringList', key, kTestValues['flutter.List']); + await preferences.remove(key); + final Map values = await preferences.getAll(); + expect(values[key], isNull); + }); + + testWidgets('clearing', (WidgetTester _) async { + await preferences.setValue( + 'String', 'String', kTestValues['flutter.String']); + await preferences.setValue('Bool', 'bool', kTestValues['flutter.bool']); + await preferences.setValue('Int', 'int', kTestValues['flutter.int']); + await preferences.setValue( + 'Double', 'double', kTestValues['flutter.double']); + await preferences.setValue( + 'StringList', 'List', kTestValues['flutter.List']); + await preferences.clear(); + final Map values = await preferences.getAll(); + expect(values['String'], null); + expect(values['bool'], null); + expect(values['int'], null); + expect(values['double'], null); + expect(values['List'], null); + }); + }); +} diff --git a/packages/shared_preferences/shared_preferences_windows/example/lib/main.dart b/packages/shared_preferences/shared_preferences_windows/example/lib/main.dart new file mode 100644 index 000000000000..0cdd37394706 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/example/lib/main.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. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:shared_preferences_windows/shared_preferences_windows.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'SharedPreferences Demo', + home: SharedPreferencesDemo(), + ); + } +} + +class SharedPreferencesDemo extends StatefulWidget { + SharedPreferencesDemo({Key? key}) : super(key: key); + + @override + SharedPreferencesDemoState createState() => SharedPreferencesDemoState(); +} + +class SharedPreferencesDemoState extends State { + final prefs = SharedPreferencesWindows.instance; + late Future _counter; + + Future _incrementCounter() async { + final values = await prefs.getAll(); + final int counter = (values['counter'] as int? ?? 0) + 1; + + setState(() { + _counter = prefs.setValue('Int', 'counter', counter).then((bool success) { + return counter; + }); + }); + } + + @override + void initState() { + super.initState(); + _counter = prefs.getAll().then((Map values) { + return (values['counter'] as int? ?? 0); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("SharedPreferences Demo"), + ), + body: Center( + child: FutureBuilder( + future: _counter, + builder: (BuildContext context, AsyncSnapshot snapshot) { + switch (snapshot.connectionState) { + case ConnectionState.waiting: + return const CircularProgressIndicator(); + default: + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } else { + return Text( + 'Button tapped ${snapshot.data} time${snapshot.data == 1 ? '' : 's'}.\n\n' + 'This should persist across restarts.', + ); + } + } + })), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml b/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml new file mode 100644 index 000000000000..96762e933a9d --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/example/pubspec.yaml @@ -0,0 +1,28 @@ +name: shared_preferences_windows_example +description: Demonstrates how to use the shared_preferences_windows plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" + +dependencies: + flutter: + sdk: flutter + 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 + 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 new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/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/lib/shared_preferences_windows.dart b/packages/shared_preferences/shared_preferences_windows/lib/shared_preferences_windows.dart new file mode 100644 index 000000000000..b8cd3702b837 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/lib/shared_preferences_windows.dart @@ -0,0 +1,119 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert' show json; +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as path; +import 'package:shared_preferences_platform_interface/shared_preferences_platform_interface.dart'; +import 'package:path_provider_windows/path_provider_windows.dart'; + +/// The Windows implementation of [SharedPreferencesStorePlatform]. +/// +/// This class implements the `package:shared_preferences` functionality for Windows. +class SharedPreferencesWindows extends SharedPreferencesStorePlatform { + /// The default instance of [SharedPreferencesWindows] to use. + /// TODO(egarciad): Remove when the Dart plugin registrant lands on Flutter stable. + /// https://github.com/flutter/flutter/issues/81421 + 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(); + + /// The path_provider_windows instance used to find the support directory. + @visibleForTesting + PathProviderWindows pathProvider = PathProviderWindows(); + + /// Local copy of preferences + Map? _cachedPreferences; + + /// Cached file for storing preferences. + File? _localDataFilePath; + + /// Gets the file where the preferences are stored. + Future _getLocalDataFile() async { + if (_localDataFilePath != null) { + return _localDataFilePath!; + } + final directory = await pathProvider.getApplicationSupportPath(); + if (directory == null) { + return null; + } + return _localDataFilePath = + fs.file(path.join(directory, 'shared_preferences.json')); + } + + /// Gets the preferences from the stored file. Once read, the preferences are + /// maintained in memory. + Future> _readPreferences() async { + if (_cachedPreferences != null) { + 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(); + } + } + _cachedPreferences = preferences; + return preferences; + } + + /// Writes the cached preferences to disk. Returns [true] if the operation + /// succeeded. + Future _writePreferences(Map preferences) async { + try { + 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); + } + String stringMap = json.encode(preferences); + localDataFile.writeAsStringSync(stringMap); + } catch (e) { + print("Error saving preferences to disk: $e"); + return false; + } + return true; + } + + @override + Future clear() async { + var preferences = await _readPreferences(); + preferences.clear(); + return _writePreferences(preferences); + } + + @override + Future> getAll() async { + return _readPreferences(); + } + + @override + Future remove(String key) async { + var preferences = await _readPreferences(); + preferences.remove(key); + return _writePreferences(preferences); + } + + @override + Future setValue(String valueType, String key, Object value) async { + var preferences = await _readPreferences(); + preferences[key] = value; + return _writePreferences(preferences); + } +} diff --git a/packages/shared_preferences/shared_preferences_windows/pubspec.yaml b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml new file mode 100644 index 000000000000..87b685f6d0bc --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/pubspec.yaml @@ -0,0 +1,32 @@ +name: shared_preferences_windows +description: Windows implementation of shared_preferences +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.2 + +environment: + sdk: '>=2.12.0 <3.0.0' + flutter: ">=2.0.0" + +flutter: + plugin: + implements: shared_preferences + platforms: + windows: + dartPluginClass: SharedPreferencesWindows + pluginClass: none + +dependencies: + flutter: + sdk: flutter + file: ^6.0.0 + meta: ^1.3.0 + path: ^1.8.0 + path_provider_platform_interface: ^2.0.0 + path_provider_windows: ^2.0.0 + shared_preferences_platform_interface: ^2.0.0 + +dev_dependencies: + flutter_test: + sdk: flutter + pedantic: ^1.10.0 diff --git a/packages/shared_preferences/shared_preferences_windows/test/shared_preferences_windows_test.dart b/packages/shared_preferences/shared_preferences_windows/test/shared_preferences_windows_test.dart new file mode 100644 index 000000000000..6bb21b814e07 --- /dev/null +++ b/packages/shared_preferences/shared_preferences_windows/test/shared_preferences_windows_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:file/memory.dart'; +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() { + late MemoryFileSystem fileSystem; + late PathProviderWindows pathProvider; + + setUp(() { + fileSystem = MemoryFileSystem.test(); + pathProvider = FakePathProviderWindows(); + }); + + Future _getFilePath() async { + final directory = await pathProvider.getApplicationSupportPath(); + return path.join(directory!, 'shared_preferences.json'); + } + + _writeTestFile(String value) async { + fileSystem.file(await _getFilePath()) + ..createSync(recursive: true) + ..writeAsStringSync(value); + } + + Future _readTestFile() async { + return fileSystem.file(await _getFilePath()).readAsStringSync(); + } + + SharedPreferencesWindows _getPreferences() { + var prefs = SharedPreferencesWindows(); + prefs.fs = fileSystem; + prefs.pathProvider = pathProvider; + return prefs; + } + + test('registered instance', () { + SharedPreferencesWindows.registerWith(); + expect(SharedPreferencesStorePlatform.instance, + isA()); + }); + + test('getAll', () async { + await _writeTestFile('{"key1": "one", "key2": 2}'); + var prefs = _getPreferences(); + + var values = await prefs.getAll(); + expect(values, hasLength(2)); + expect(values['key1'], 'one'); + expect(values['key2'], 2); + }); + + test('remove', () async { + await _writeTestFile('{"key1":"one","key2":2}'); + var prefs = _getPreferences(); + + await prefs.remove('key2'); + + expect(await _readTestFile(), '{"key1":"one"}'); + }); + + test('setValue', () async { + await _writeTestFile('{}'); + var prefs = _getPreferences(); + + await prefs.setValue('', 'key1', 'one'); + await prefs.setValue('', 'key2', 2); + + expect(await _readTestFile(), '{"key1":"one","key2":2}'); + }); + + test('clear', () async { + await _writeTestFile('{"key1":"one","key2":2}'); + var prefs = _getPreferences(); + + await prefs.clear(); + expect(await _readTestFile(), '{}'); + }); +} + +/// Fake implementation of PathProviderWindows that returns hard-coded paths, +/// allowing tests to run on any platform. +/// +/// Note that this should only be used with an in-memory filesystem, as the +/// path it returns is a root path that does not actually exist on Windows. +class FakePathProviderWindows extends PathProviderPlatform + implements PathProviderWindows { + late VersionInfoQuerier versionInfoQuerier; + + @override + Future getApplicationSupportPath() async => r'C:\appsupport'; + + @override + Future getTemporaryPath() async => null; + + @override + Future getLibraryPath() async => null; + + @override + Future getApplicationDocumentsPath() async => null; + + @override + Future getDownloadsPath() async => null; + + @override + 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 56b4bb3d0630..fff325e08915 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,3 +1,151 @@ +## 6.0.12 + +* Fixed an error where 'launch' method of url_launcher would cause an error if the provided URL was not valid by RFC 3986. + +## 6.0.11 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. +* Updated Android lint settings. + +## 6.0.10 + +* Remove references to the Android v1 embedding. + +## 6.0.9 + +* Silenced warnings that may occur during build when using a very + recent version of Flutter relating to null safety. + +## 6.0.8 + +* Adding API level 30 required package visibility configuration to the example's AndroidManifest.xml and README +* Fix test button check for iOS 15. + +## 6.0.7 + +* Update the README to describe a workaround to the `Uri` query + encoding bug. + +## 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. + +## 5.7.1 + +* Keep handling deprecated Android v1 classes for backward compatibility. + +## 5.7.0 + +* Handle WebView multi-window support. + +## 5.6.0 + +* Support Windows by default. + +## 5.5.3 + +* Suppress deprecation warning on the `shouldOverrideUrlLoading` method on Android. + +## 5.5.2 + +* Depend explicitly on the `platform_interface` package that adds the `webOnlyWindowName` parameter. + +## 5.5.1 + +* Added webOnlyWindowName parameter to launch() + +## 5.5.0 + +* Support Linux by default. + +## 5.4.11 + +* Add documentation in README suggesting how to properly encode urls with special characters. + +## 5.4.10 + +* Post-v2 Android embedding cleanups. + ## 5.4.9 * Update README. @@ -38,7 +186,7 @@ ## 5.4.0 -* Support macos by default. +* Support macOS by default. ## 5.3.0 @@ -64,7 +212,7 @@ ## 5.2.3 -Android: Use android.arch.lifecycle instead of androidx.lifecycle:lifecycle in `build.gradle` to support apps that has not been migrated to AndroidX. +* Android: Use android.arch.lifecycle instead of androidx.lifecycle:lifecycle in `build.gradle` to support apps that has not been migrated to AndroidX. ## 5.2.2 diff --git a/packages/url_launcher/url_launcher/LICENSE b/packages/url_launcher/url_launcher/LICENSE index 000b4618d2bd..c6823b81eb84 100644 --- a/packages/url_launcher/url_launcher/LICENSE +++ b/packages/url_launcher/url_launcher/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. \ No newline at end of file +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/url_launcher/url_launcher/README.md b/packages/url_launcher/url_launcher/README.md index 2dcc6359c1af..c649b5c0fe7b 100644 --- a/packages/url_launcher/url_launcher/README.md +++ b/packages/url_launcher/url_launcher/README.md @@ -1,43 +1,88 @@ # 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 iOS and Android. +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'; +``` +### Android + +Starting from API 30 Android requires package visibility configuration in your +`AndroidManifest.xml` otherwise `canLaunch` will return `false`. A `` +element must be added to your manifest as a child of the root element. + +The snippet below shows an example for an application that uses `https`, `tel`, +and `mailto` URLs with `url_launcher`. See +[the Android documentation](https://developer.android.com/training/package-visibility/use-cases) +for examples of other queries. + +``` xml + + + + + + + + + + + + + + + + + ``` ## 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. @@ -53,6 +98,41 @@ 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). +For example: +```dart +String? encodeQueryParameters(Map params) { + return params.entries + .map((e) => '${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value)}') + .join('&'); +} + +final Uri emailLaunchUri = Uri( + scheme: 'mailto', + path: 'smith@example.com', + query: encodeQueryParameters({ + 'subject': 'Example Subject & Symbols are allowed!' + }), +); + +launch(emailLaunchUri.toString()); +``` + +**Warning**: For any scheme other than `http` or `https`, you should use the +`query` parameter and the `encodeQueryParameters` function shown above rather +than `Uri`'s `queryParameters` constructor argument, due to +[a bug](https://github.com/dart-lang/sdk/issues/43838) in the way `Uri` +encodes query parameters. Using `queryParameters` will result in spaces being +converted to `+` in many cases. + ## Handling missing URL receivers A particular mobile device may not be able to receive all supported URL schemes. @@ -61,7 +141,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 @@ -70,8 +150,8 @@ web page using a URL following the `http` scheme. ## Browser vs In-app Handling By default, Android opens up a browser when handling URLs. You can pass -`forceWebView: true` parameter to tell the plugin to open a WebView instead. -If you do this for a URL of a page containing JavaScript, make sure to pass in +`forceWebView: true` parameter to tell the plugin to open a WebView instead. +If you do this for a URL of a page containing JavaScript, make sure to pass in `enableJavaScript: true`, or else the launch method will not work properly. On iOS, the default behavior is to open all web URLs within the app. Everything else is redirected to the app handler. diff --git a/packages/url_launcher/url_launcher/android/build.gradle b/packages/url_launcher/url_launcher/android/build.gradle index c02b29a5814f..d374d40534c3 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 @@ -30,9 +30,20 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } + + testOptions { unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } } } diff --git a/packages/url_launcher/url_launcher/android/gradle.properties b/packages/url_launcher/url_launcher/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/url_launcher/url_launcher/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M 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 0ff2fd4aac3b..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; @@ -6,7 +10,6 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import io.flutter.plugin.common.PluginRegistry.Registrar; /** * Plugin implementation that uses the new {@code io.flutter.embedding} package. @@ -25,7 +28,8 @@ public final class UrlLauncherPlugin implements FlutterPlugin, ActivityAware { *

      Calling this automatically initializes the plugin. However plugins initialized this way * won't react to changes in activity or context, unlike {@link UrlLauncherPlugin}. */ - public static void registerWith(Registrar registrar) { + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { MethodCallHandlerImpl handler = new MethodCallHandlerImpl(new UrlLauncher(registrar.context(), registrar.activity())); handler.startListening(registrar.messenger()); 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 52714790a25c..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,5 +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. + package io.flutter.plugins.urllauncher; +import android.annotation.TargetApi; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; @@ -7,11 +12,15 @@ import android.content.IntentFilter; import android.os.Build; import android.os.Bundle; +import android.os.Message; import android.provider.Browser; import android.view.KeyEvent; +import android.webkit.WebChromeClient; import android.webkit.WebResourceRequest; import android.webkit.WebView; import android.webkit.WebViewClient; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; import java.util.HashMap; import java.util.Map; @@ -38,6 +47,11 @@ public void onReceive(Context context, Intent intent) { private final WebViewClient webViewClient = new WebViewClient() { + /* + * This method is deprecated in API 24. Still overridden to support + * earlier Android versions. + */ + @SuppressWarnings("deprecation") @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { @@ -47,6 +61,7 @@ public boolean shouldOverrideUrlLoading(WebView view, String url) { return super.shouldOverrideUrlLoading(view, url); } + @RequiresApi(Build.VERSION_CODES.N) @Override public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { @@ -60,6 +75,44 @@ public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request private IntentFilter closeIntentFilter = new IntentFilter(ACTION_CLOSE); + // Verifies that a url opened by `Window.open` has a secure url. + private class FlutterWebChromeClient extends WebChromeClient { + @Override + public boolean onCreateWindow( + final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { + final WebViewClient webViewClient = + new WebViewClient() { + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading( + @NonNull WebView view, @NonNull WebResourceRequest request) { + webview.loadUrl(request.getUrl().toString()); + 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); + return true; + } + }; + + final WebView newWebView = new WebView(webview.getContext()); + newWebView.setWebViewClient(webViewClient); + + final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; + transport.setWebView(newWebView); + resultMsg.sendToTarget(); + + return true; + } + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -81,6 +134,10 @@ public void onCreate(Bundle savedInstanceState) { // Open new urls inside the webview itself. webview.setWebViewClient(webViewClient); + // Multi windows is set with FlutterWebChromeClient by default to handle internal bug: b/159892679. + webview.getSettings().setSupportMultipleWindows(true); + webview.setWebChromeClient(new FlutterWebChromeClient()); + // Register receiver that may finish this Activity. registerReceiver(broadcastReceiver, closeIntentFilter); } 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..8280da86f124 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 30 lintOptions { disable 'InvalidPackage' @@ -34,7 +34,7 @@ android { defaultConfig { applicationId "io.flutter.plugins.urllauncherexample" minSdkVersion 16 - targetSdkVersion 28 + targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" diff --git a/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java b/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/EmbeddingV1ActivityTest.java b/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/EmbeddingV1ActivityTest.java deleted file mode 100644 index bae2957ab81e..000000000000 --- a/packages/url_launcher/url_launcher/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.flutter.plugins.urllauncherexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} 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 4dda10f32621..67f15efb10aa 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,11 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + package io.flutter.plugins.urllauncherexample; import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; import org.junit.Rule; import org.junit.runner.RunWith; -@RunWith(FlutterRunner.class) -public class MainActivityTest { - @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class FlutterActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); } diff --git a/packages/url_launcher/url_launcher/example/android/app/src/main/AndroidManifest.xml b/packages/url_launcher/url_launcher/example/android/app/src/main/AndroidManifest.xml index d30c15fd9ea8..918c29ee2dca 100644 --- a/packages/url_launcher/url_launcher/example/android/app/src/main/AndroidManifest.xml +++ b/packages/url_launcher/url_launcher/example/android/app/src/main/AndroidManifest.xml @@ -1,27 +1,31 @@ - + + + + + + + + + + + + + + + - - + android:label="url_launcher_example"> @@ -29,11 +33,7 @@ + - - 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 deleted file mode 100644 index e52ccfc18b47..000000000000 --- a/packages/url_launcher/url_launcher/example/android/app/src/main/java/io/flutter/plugins/urllauncherexample/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.urllauncherexample; - -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; - -public class EmbeddingV1Activity extends FlutterActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); - } -} diff --git a/packages/url_launcher/url_launcher/example/android/app/src/main/java/io/flutter/plugins/urllauncherexample/MainActivity.java b/packages/url_launcher/url_launcher/example/android/app/src/main/java/io/flutter/plugins/urllauncherexample/MainActivity.java deleted file mode 100644 index 76f7df752842..000000000000 --- a/packages/url_launcher/url_launcher/example/android/app/src/main/java/io/flutter/plugins/urllauncherexample/MainActivity.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.flutter.plugins.urllauncherexample; - -import io.flutter.embedding.android.FlutterActivity; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.plugins.urllauncher.UrlLauncherPlugin; - -public class MainActivity extends FlutterActivity { - @Override - public void configureFlutterEngine(FlutterEngine flutterEngine) { - flutterEngine.getPlugins().add(new UrlLauncherPlugin()); - } -} diff --git a/packages/url_launcher/url_launcher/example/android/build.gradle b/packages/url_launcher/url_launcher/example/android/build.gradle index 6b1a639efd76..328175bb6ac5 100644 --- a/packages/url_launcher/url_launcher/example/android/build.gradle +++ b/packages/url_launcher/url_launcher/example/android/build.gradle @@ -1,18 +1,18 @@ buildscript { repositories { google() - jcenter() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.2' + classpath 'com.android.tools.build:gradle:4.2.1' } } allprojects { repositories { google() - jcenter() + mavenCentral() } } diff --git a/packages/url_launcher/url_launcher/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/url_launcher/url_launcher/example/android/gradle/wrapper/gradle-wrapper.properties index 1cedb28ea41f..4ae10e927b38 100644 --- a/packages/url_launcher/url_launcher/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/url_launcher/url_launcher/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip 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 new file mode 100644 index 000000000000..14136996b692 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/integration_test/url_launcher_test.dart @@ -0,0 +1,29 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' show Platform; + +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:url_launcher/url_launcher.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('canLaunch', (WidgetTester _) async { + expect(await canLaunch('randomstring'), false); + + // Generally all devices should have some default browser. + expect(await canLaunch('http://flutter.dev'), true); + + // SMS handling is available by default on most platforms. + if (kIsWeb || !(Platform.isLinux || Platform.isWindows)) { + 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. + }); +} diff --git a/packages/url_launcher/url_launcher/example/ios/Flutter/AppFrameworkInfo.plist b/packages/url_launcher/url_launcher/example/ios/Flutter/AppFrameworkInfo.plist index 6c2de8086bcd..3a9c234f96d4 100644 --- a/packages/url_launcher/url_launcher/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/url_launcher/url_launcher/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 8.0 + 9.0 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..595f85d9a75b 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; @@ -362,7 +517,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -372,7 +527,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -413,7 +567,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -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..18af3be9a1e5 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/ios/RunnerUITests/URLLauncherUITests.m @@ -0,0 +1,42 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import 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]; + XCTAssertTrue([button waitForExistenceWithTimeout:30.0]); + XCTAssertEqual(app.webViews.count, 0); + [button tap]; + XCUIElement* webView = app.webViews.firstMatch; + XCTAssertTrue([webView waitForExistenceWithTimeout:30.0]); + XCTAssertTrue([app.buttons[@"ForwardButton"] waitForExistenceWithTimeout:30.0]); + XCTAssertTrue(app.buttons[@"Share"].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/CMakeLists.txt b/packages/url_launcher/url_launcher/example/linux/CMakeLists.txt new file mode 100644 index 000000000000..0236a8806654 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/linux/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +set(BINARY_NAME "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) + +# 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) + +# 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/url_launcher/url_launcher/example/linux/flutter/CMakeLists.txt b/packages/url_launcher/url_launcher/example/linux/flutter/CMakeLists.txt new file mode 100644 index 000000000000..94f43ff7fa6a --- /dev/null +++ b/packages/url_launcher/url_launcher/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,86 @@ +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + linux-x64 ${CMAKE_BUILD_TYPE} +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) 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 new file mode 100644 index 000000000000..f6f23bfe970f --- /dev/null +++ b/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugin_registrant.h b/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000000..e0f0a47bc08f --- /dev/null +++ b/packages/url_launcher/url_launcher/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/url_launcher/url_launcher/example/linux/flutter/generated_plugins.cmake b/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..1fc8ed344297 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,16 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux +) + +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/url_launcher/url_launcher/example/linux/main.cc b/packages/url_launcher/url_launcher/example/linux/main.cc new file mode 100644 index 000000000000..1507d02825e7 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/linux/main.cc @@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/packages/url_launcher/url_launcher/example/linux/my_application.cc b/packages/url_launcher/url_launcher/example/linux/my_application.cc new file mode 100644 index 000000000000..878cd973d997 --- /dev/null +++ b/packages/url_launcher/url_launcher/example/linux/my_application.cc @@ -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. + +#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(), nullptr)); +} diff --git a/packages/url_launcher/url_launcher/example/linux/my_application.h b/packages/url_launcher/url_launcher/example/linux/my_application.h new file mode 100644 index 000000000000..6e9f0c3ff665 --- /dev/null +++ b/packages/url_launcher/url_launcher/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/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 1eb3e603696f..db1d548695dc 100644 --- a/packages/url_launcher/url_launcher/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher/example/pubspec.yaml @@ -1,19 +1,30 @@ name: url_launcher_example description: Demonstrates how to use the url_launcher plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" 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: - e2e: "^0.2.0" + 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 new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/url_launcher/url_launcher/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/example/test_driver/url_launcher_e2e.dart b/packages/url_launcher/url_launcher/example/test_driver/url_launcher_e2e.dart deleted file mode 100644 index e1d75f93b326..000000000000 --- a/packages/url_launcher/url_launcher/example/test_driver/url_launcher_e2e.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:e2e/e2e.dart'; -import 'package:url_launcher/url_launcher.dart'; - -void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); - - test('canLaunch', () async { - expect(await canLaunch('randomstring'), false); - - // Generally all devices should have some default browser. - expect(await 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. - }); -} diff --git a/packages/url_launcher/url_launcher/example/test_driver/url_launcher_e2e_test.dart b/packages/url_launcher/url_launcher/example/test_driver/url_launcher_e2e_test.dart deleted file mode 100644 index 1bcd0d37f450..000000000000 --- a/packages/url_launcher/url_launcher/example/test_driver/url_launcher_e2e_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 'dart:io'; - -import 'package:flutter_driver/flutter_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); -} 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..9ba9b1331728 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. @@ -23,10 +23,8 @@ - (instancetype)initWithUrl:url withFlutterResult:result { if (self) { self.url = url; self.flutterResult = result; - if (@available(iOS 9.0, *)) { - self.safari = [[SFSafariViewController alloc] initWithURL:url]; - self.safari.delegate = self; - } + self.safari = [[SFSafariViewController alloc] initWithURL:url]; + self.safari.delegate = self; } return self; } @@ -34,7 +32,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" @@ -78,23 +76,12 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result } else if ([@"launch" isEqualToString:call.method]) { NSNumber *useSafariVC = call.arguments[@"useSafariVC"]; if (useSafariVC.boolValue) { - if (@available(iOS 9.0, *)) { - [self launchURLInVC:url result:result]; - } else { - [self launchURL:url call:call result:result]; - } + [self launchURLInVC:url result:result]; } else { [self launchURL:url call:call result:result]; } } else if ([@"closeWebView" isEqualToString:call.method]) { - if (@available(iOS 9.0, *)) { - [self closeWebViewWithResult:result]; - } else { - result([FlutterError - errorWithCode:@"API_NOT_AVAILABLE" - message:@"SafariViewController related api is not availabe for version <= IOS9" - details:nil]); - } + [self closeWebViewWithResult:result]; } else { result(FlutterMethodNotImplemented); } diff --git a/packages/url_launcher/url_launcher/ios/url_launcher.podspec b/packages/url_launcher/url_launcher/ios/url_launcher.podspec index c8bba7e02c3e..6af64b66bee4 100644 --- a/packages/url_launcher/url_launcher/ios/url_launcher.podspec +++ b/packages/url_launcher/url_launcher/ios/url_launcher.podspec @@ -17,7 +17,7 @@ A Flutter plugin for making the underlying platform (Android or iOS) launch a UR 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.platform = :ios, '9.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/packages/url_launcher/url_launcher/lib/link.dart b/packages/url_launcher/url_launcher/lib/link.dart 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 2ce725da8642..300f96f4a179 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. @@ -16,7 +16,7 @@ import 'package:url_launcher_platform_interface/url_launcher_platform_interface. /// schemes which cannot be handled, that is when [canLaunch] would complete /// with false. /// -/// [forceSafariVC] is only used in iOS with iOS version >= 9.0. By default (when unset), the launcher +/// By default when [forceSafariVC] is unset, the launcher /// opens web URLs in the Safari View Controller, anything else is opened /// using the default handler on the platform. If set to true, it opens the /// URL in the Safari View Controller. If false, the URL is opened in the @@ -44,6 +44,12 @@ 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. /// /// Note that if any of the above are set to true but the URL is not a web URL, /// this will throw a [PlatformException]. @@ -56,17 +62,19 @@ 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, + 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'; + final Uri? url = Uri.tryParse(urlString.trimLeft()); + final bool isWebURL = + url != null && (url.scheme == 'http' || url.scheme == 'https'); + if ((forceSafariVC == true || forceWebView == true) && !isWebURL) { throw PlatformException( code: 'NOT_A_WEB_SCHEME', @@ -77,37 +85,50 @@ Future launch( /// [true] so that ui is automatically computed if [statusBarBrightness] is set. bool previousAutomaticSystemUiAdjustment = true; if (statusBarBrightness != null && - defaultTargetPlatform == TargetPlatform.iOS) { - previousAutomaticSystemUiAdjustment = - WidgetsBinding.instance.renderView.automaticSystemUiAdjustment; - WidgetsBinding.instance.renderView.automaticSystemUiAdjustment = false; + defaultTargetPlatform == TargetPlatform.iOS && + _ambiguate(WidgetsBinding.instance) != null) { + previousAutomaticSystemUiAdjustment = _ambiguate(WidgetsBinding.instance)! + .renderView + .automaticSystemUiAdjustment; + _ambiguate(WidgetsBinding.instance)! + .renderView + .automaticSystemUiAdjustment = false; SystemChrome.setSystemUIOverlayStyle(statusBarBrightness == Brightness.light ? SystemUiOverlayStyle.dark : SystemUiOverlayStyle.light); } + final bool result = await UrlLauncherPlatform.instance.launch( urlString, useSafariVC: forceSafariVC ?? isWebURL, - useWebView: forceWebView ?? 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 = - previousAutomaticSystemUiAdjustment; + + if (statusBarBrightness != null && + _ambiguate(WidgetsBinding.instance) != null) { + _ambiguate(WidgetsBinding.instance)! + .renderView + .automaticSystemUiAdjustment = previousAutomaticSystemUiAdjustment; } + return result; } /// Checks whether the specified URL can be handled by some app installed on the /// device. +/// +/// On Android (from API 30), [canLaunch] will return `false` when the required +/// visibility configuration is not provided in the AndroidManifest.xml file. +/// For more information see the +/// [Package visibility filtering on Android](https://developer.android.com/training/basics/intents/package-visibility) +/// article in the Android documentation or the url_launcher example app's +/// [AndroidManifest.xml's queries element](https://github.com/flutter/plugins/blob/master/packages/url_launcher/url_launcher/example/android/app/src/main/AndroidManifest.xml). Future canLaunch(String urlString) async { - if (urlString == null) { - return false; - } return await UrlLauncherPlatform.instance.canLaunch(urlString); } @@ -119,9 +140,13 @@ Future canLaunch(String urlString) async { /// Or on IOS systems, if [launch] was called without `forceSafariVC` being set to `true`, /// this call will not do anything either, simply because there is no /// WebView/SafariViewController available to be closed. -/// -/// SafariViewController is only available on IOS version >= 9.0, this method does not do anything -/// on IOS version below 9.0 Future closeWebView() async { return await UrlLauncherPlatform.instance.closeWebView(); } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +// TODO(ianh): Remove this once we roll stable in late 2021. +T? _ambiguate(T? value) => value; diff --git a/packages/url_launcher/url_launcher/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 a01985909b87..8edb9e21c535 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.4.9 +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.12 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" flutter: plugin: @@ -12,31 +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.4 + 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.0+1 - url_launcher_macos: ^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..a038746d6bec 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(); + _anonymize(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(); + _anonymize(TestWidgetsFlutterBinding.ensureInitialized()) + as TestWidgetsFlutterBinding; debugDefaultTargetPlatformOverride = TargetPlatform.android; expect(binding.renderView.automaticSystemUiAdjustment, true); final Future launchResult = @@ -220,9 +281,49 @@ void main() { await launchResult; expect(binding.renderView.automaticSystemUiAdjustment, true); }); + + test('open non-parseable url', () async { + mock + ..setLaunchExpectations( + url: + 'rdp://full%20address=s:mypc:3389&audiomode=i:2&disable%20themes=i:1', + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, + webOnlyWindowName: null, + ) + ..setResponse(true); + expect( + await launch( + 'rdp://full%20address=s:mypc:3389&audiomode=i:2&disable%20themes=i:1'), + isTrue); + }); + + test('cannot open non-parseable url with forceSafariVC: true', () async { + expect( + () async => await launch( + 'rdp://full%20address=s:mypc:3389&audiomode=i:2&disable%20themes=i:1', + forceSafariVC: true), + throwsA(isA())); + }); + + test('cannot open non-parseable url with forceWebView: true', () async { + expect( + () async => await launch( + 'rdp://full%20address=s:mypc:3389&audiomode=i:2&disable%20themes=i:1', + forceWebView: true), + throwsA(isA())); + }); }); } -class MockUrlLauncher extends Mock - with MockPlatformInterfaceMixin - implements UrlLauncherPlatform {} +/// This removes the type information from a value so that it can be cast +/// to another type even if that cast is redundant. +/// +/// We use this so that APIs whose type have become more descriptive can still +/// be used on the stable branch where they require a cast. +// TODO(ianh): Remove this once we roll stable in late 2021. +Object? _anonymize(T? value) => value; diff --git a/packages/url_launcher/url_launcher_linux/.gitignore b/packages/url_launcher/url_launcher_linux/.gitignore new file mode 100644 index 000000000000..53e92cc4181f --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/.gitignore @@ -0,0 +1,3 @@ +.packages +.flutter-plugins +pubspec.lock diff --git a/packages/url_launcher/url_launcher_linux/.metadata b/packages/url_launcher/url_launcher_linux/.metadata new file mode 100644 index 000000000000..457a92ae1645 --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/.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: 4b12050112afd581ddf53df848275fa681f908f3 + channel: master + +project_type: plugin 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 new file mode 100644 index 000000000000..147d0f312c7e --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/CHANGELOG.md @@ -0,0 +1,40 @@ +## 2.0.2 + +* Replaced reference to `shared_preferences` plugin with the `url_launcher` in the README. + +## 2.0.1 + +* Updated installation instructions in README. + +## 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. + +# 0.0.1 +* The initial implementation of url_launcher for Linux diff --git a/packages/url_launcher/url_launcher_linux/LICENSE b/packages/url_launcher/url_launcher_linux/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/url_launcher/url_launcher_linux/README.md b/packages/url_launcher/url_launcher_linux/README.md new file mode 100644 index 000000000000..1d0667860030 --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/README.md @@ -0,0 +1,11 @@ +# url\_launcher\_linux + +The Linux implementation of [`url_launcher`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `url_launcher` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/url_launcher +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/url_launcher/url_launcher_linux/example/.gitignore b/packages/url_launcher/url_launcher_linux/example/.gitignore new file mode 100644 index 000000000000..f3c205341e7d --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/example/.gitignore @@ -0,0 +1,44 @@ +# 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 + +# Exceptions to above rules. +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages diff --git a/packages/url_launcher/url_launcher_linux/example/.metadata b/packages/url_launcher/url_launcher_linux/example/.metadata new file mode 100644 index 000000000000..99b1a7456d66 --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 4b12050112afd581ddf53df848275fa681f908f3 + channel: master + +project_type: app diff --git a/packages/url_launcher/url_launcher_linux/example/README.md b/packages/url_launcher/url_launcher_linux/example/README.md new file mode 100644 index 000000000000..c200da8974d1 --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/example/README.md @@ -0,0 +1,8 @@ +# url_launcher_example + +Demonstrates how to use the url_launcher plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.dev/). diff --git a/packages/url_launcher/url_launcher_linux/example/integration_test/url_launcher_test.dart b/packages/url_launcher/url_launcher_linux/example/integration_test/url_launcher_test.dart new file mode 100644 index 000000000000..ae9a9148f9d7 --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/example/integration_test/url_launcher_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:integration_test/integration_test.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('canLaunch', (WidgetTester _) async { + UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; + + expect(await launcher.canLaunch('randomstring'), false); + + // Generally all devices should have some default browser. + expect(await launcher.canLaunch('http://flutter.dev'), true); + }); +} diff --git a/packages/url_launcher/url_launcher_linux/example/lib/main.dart b/packages/url_launcher/url_launcher_linux/example/lib/main.dart new file mode 100644 index 000000000000..86e06f3fafed --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/example/lib/main.dart @@ -0,0 +1,93 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'URL Launcher', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: MyHomePage(title: 'URL Launcher'), + ); + } +} + +class MyHomePage extends StatefulWidget { + MyHomePage({Key? key, required this.title}) : super(key: key); + final String title; + + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + Future? _launched; + + Future _launchInBrowser(String url) async { + if (await UrlLauncherPlatform.instance.canLaunch(url)) { + await UrlLauncherPlatform.instance.launch( + url, + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, + ); + } else { + throw 'Could not launch $url'; + } + } + + Widget _launchStatus(BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } else { + return const Text(''); + } + } + + @override + Widget build(BuildContext context) { + const String toLaunch = 'https://www.cylog.org/headers/'; + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: ListView( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Padding( + padding: EdgeInsets.all(16.0), + child: Text(toLaunch), + ), + ElevatedButton( + onPressed: () => setState(() { + _launched = _launchInBrowser(toLaunch); + }), + child: const Text('Launch in browser'), + ), + const Padding(padding: EdgeInsets.all(16.0)), + FutureBuilder(future: _launched, builder: _launchStatus), + ], + ), + ], + ), + ); + } +} diff --git a/packages/url_launcher/url_launcher_linux/example/linux/.gitignore b/packages/url_launcher/url_launcher_linux/example/linux/.gitignore new file mode 100644 index 000000000000..d3896c98444f --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/example/linux/.gitignore @@ -0,0 +1 @@ +flutter/ephemeral diff --git a/packages/url_launcher/url_launcher_linux/example/linux/CMakeLists.txt b/packages/url_launcher/url_launcher_linux/example/linux/CMakeLists.txt new file mode 100644 index 000000000000..1758aac03b0d --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/example/linux/CMakeLists.txt @@ -0,0 +1,98 @@ +cmake_minimum_required(VERSION 3.10) +project(runner LANGUAGES CXX) + +set(BINARY_NAME "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) + +# 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) + +# Enable the test target. +set(include_url_launcher_linux_tests TRUE) + +# 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/url_launcher/url_launcher_linux/example/linux/flutter/CMakeLists.txt b/packages/url_launcher/url_launcher_linux/example/linux/flutter/CMakeLists.txt new file mode 100644 index 000000000000..33fd5801e713 --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/example/linux/flutter/CMakeLists.txt @@ -0,0 +1,87 @@ +cmake_minimum_required(VERSION 3.10) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. + +# Serves the same purpose as list(TRANSFORM ... PREPEND ...), +# which isn't available in 3.10. +function(list_prepend LIST_NAME PREFIX) + set(NEW_LIST "") + foreach(element ${${LIST_NAME}}) + list(APPEND NEW_LIST "${PREFIX}${element}") + endforeach(element) + set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) +endfunction() + +# === Flutter Library === +# System-level dependencies. +find_package(PkgConfig REQUIRED) +pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) +pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) +pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) + +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "fl_basic_message_channel.h" + "fl_binary_codec.h" + "fl_binary_messenger.h" + "fl_dart_project.h" + "fl_engine.h" + "fl_json_message_codec.h" + "fl_json_method_codec.h" + "fl_message_codec.h" + "fl_method_call.h" + "fl_method_channel.h" + "fl_method_codec.h" + "fl_method_response.h" + "fl_plugin_registrar.h" + "fl_plugin_registry.h" + "fl_standard_message_codec.h" + "fl_standard_method_codec.h" + "fl_string_codec.h" + "fl_value.h" + "fl_view.h" + "flutter_linux.h" +) +list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") +target_link_libraries(flutter INTERFACE + PkgConfig::GTK + PkgConfig::GLIB + PkgConfig::GIO +) +add_dependencies(flutter flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CMAKE_CURRENT_BINARY_DIR}/_phony_ + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" + ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} +) diff --git a/packages/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 new file mode 100644 index 000000000000..f6f23bfe970f --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugin_registrant.h b/packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 000000000000..e0f0a47bc08f --- /dev/null +++ b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugins.cmake b/packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugins.cmake new file mode 100644 index 000000000000..1fc8ed344297 --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/example/linux/flutter/generated_plugins.cmake @@ -0,0 +1,16 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + url_launcher_linux +) + +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/url_launcher/url_launcher_linux/example/linux/main.cc b/packages/url_launcher/url_launcher_linux/example/linux/main.cc new file mode 100644 index 000000000000..1507d02825e7 --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/example/linux/main.cc @@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "my_application.h" + +int main(int argc, char** argv) { + g_autoptr(MyApplication) app = my_application_new(); + return g_application_run(G_APPLICATION(app), argc, argv); +} diff --git a/packages/url_launcher/url_launcher_linux/example/linux/my_application.cc b/packages/url_launcher/url_launcher_linux/example/linux/my_application.cc new file mode 100644 index 000000000000..878cd973d997 --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/example/linux/my_application.cc @@ -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. + +#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(), nullptr)); +} 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 new file mode 100644 index 000000000000..6e9f0c3ff665 --- /dev/null +++ b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_linux/example/pubspec.yaml b/packages/url_launcher/url_launcher_linux/example/pubspec.yaml new file mode 100644 index 000000000000..b0ef2e6eddbf --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/example/pubspec.yaml @@ -0,0 +1,29 @@ +name: url_launcher_example +description: Demonstrates how to use the url_launcher plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=1.20.0" + +dependencies: + flutter: + sdk: flutter + 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: + sdk: flutter + flutter_driver: + sdk: flutter + 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 new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/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_linux/linux/CMakeLists.txt b/packages/url_launcher/url_launcher_linux/linux/CMakeLists.txt new file mode 100644 index 000000000000..b3f4a22b053d --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/linux/CMakeLists.txt @@ -0,0 +1,62 @@ +cmake_minimum_required(VERSION 3.10) +set(PROJECT_NAME "url_launcher_linux") +project(${PROJECT_NAME} LANGUAGES CXX) + +set(PLUGIN_NAME "${PROJECT_NAME}_plugin") + +list(APPEND PLUGIN_SOURCES + "url_launcher_plugin.cc" +) + +add_library(${PLUGIN_NAME} SHARED + ${PLUGIN_SOURCES} +) +apply_standard_settings(${PLUGIN_NAME}) +set_target_properties(${PLUGIN_NAME} PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) +target_include_directories(${PLUGIN_NAME} INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter) +target_link_libraries(${PLUGIN_NAME} PRIVATE PkgConfig::GTK) + + +# === Tests === + +if (${include_${PROJECT_NAME}_tests}) +if(${CMAKE_VERSION} VERSION_LESS "3.11.0") +message("Unit tests require CMake 3.11.0 or later") +else() +set(TEST_RUNNER "${PROJECT_NAME}_test") +enable_testing() +# TODO(stuartmorgan): Consider using a single shared, pre-checked-in googletest +# instance rather than downloading for each plugin. This approach makes sense +# for a template, but not for a monorepo with many plugins. +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/release-1.11.0.zip +) +# Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +# Disable install commands for gtest so it doesn't end up in the bundle. +set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of googletest" FORCE) + +FetchContent_MakeAvailable(googletest) + +# The plugin's exported API is not very useful for unit testing, so build the +# sources directly into the test binary rather than using the shared library. +add_executable(${TEST_RUNNER} + test/url_launcher_linux_test.cc + ${PLUGIN_SOURCES} +) +apply_standard_settings(${TEST_RUNNER}) +target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(${TEST_RUNNER} PRIVATE flutter) +target_link_libraries(${TEST_RUNNER} PRIVATE PkgConfig::GTK) +target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock) + +include(GoogleTest) +gtest_discover_tests(${TEST_RUNNER}) +endif() # CMake version check +endif() # include_${PROJECT_NAME}_tests 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 new file mode 100644 index 000000000000..f4d19395e37f --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/linux/include/url_launcher_linux/url_launcher_plugin.h @@ -0,0 +1,31 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PACKAGES_URL_LAUNCHER_URL_LAUNCHER_LINUX_LINUX_INCLUDE_URL_LAUNCHER_URL_LAUNCHER_PLUGIN_H_ +#define PACKAGES_URL_LAUNCHER_URL_LAUNCHER_LINUX_LINUX_INCLUDE_URL_LAUNCHER_URL_LAUNCHER_PLUGIN_H_ + +// A plugin to launch URLs. + +#include + +G_BEGIN_DECLS + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default"))) +#else +#define FLUTTER_PLUGIN_EXPORT +#endif + +G_DECLARE_FINAL_TYPE(FlUrlLauncherPlugin, fl_url_launcher_plugin, FL, + URL_LAUNCHER_PLUGIN, GObject) + +FLUTTER_PLUGIN_EXPORT FlUrlLauncherPlugin* fl_url_launcher_plugin_new( + FlPluginRegistrar* registrar); + +FLUTTER_PLUGIN_EXPORT void url_launcher_plugin_register_with_registrar( + FlPluginRegistrar* registrar); + +G_END_DECLS + +#endif // PACKAGES_URL_LAUNCHER_URL_LAUNCHER_LINUX_LINUX_INCLUDE_URL_LAUNCHER_URL_LAUNCHER_PLUGIN_H_ diff --git a/packages/url_launcher/url_launcher_linux/linux/test/url_launcher_linux_test.cc b/packages/url_launcher/url_launcher_linux/linux/test/url_launcher_linux_test.cc new file mode 100644 index 000000000000..e655638c4ed7 --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/linux/test/url_launcher_linux_test.cc @@ -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. +#include +#include +#include + +#include +#include + +#include "include/url_launcher_linux/url_launcher_plugin.h" +#include "url_launcher_plugin_private.h" + +namespace url_launcher_plugin { +namespace test { + +TEST(UrlLauncherPlugin, CanLaunchSuccess) { + g_autoptr(FlValue) args = fl_value_new_map(); + fl_value_set_string_take(args, "url", + fl_value_new_string("https://flutter.dev")); + FlMethodResponse* response = can_launch(nullptr, args); + ASSERT_NE(response, nullptr); + ASSERT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response)); + g_autoptr(FlValue) expected = fl_value_new_bool(true); + EXPECT_TRUE(fl_value_equal(fl_method_success_response_get_result( + FL_METHOD_SUCCESS_RESPONSE(response)), + expected)); +} + +TEST(UrlLauncherPlugin, CanLaunchFailureUnhandled) { + g_autoptr(FlValue) args = fl_value_new_map(); + fl_value_set_string_take(args, "url", fl_value_new_string("madeup:scheme")); + FlMethodResponse* response = can_launch(nullptr, args); + ASSERT_NE(response, nullptr); + ASSERT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response)); + g_autoptr(FlValue) expected = fl_value_new_bool(false); + EXPECT_TRUE(fl_value_equal(fl_method_success_response_get_result( + FL_METHOD_SUCCESS_RESPONSE(response)), + expected)); +} + +// For consistency with the established mobile implementations, +// an invalid URL should return false, not an error. +TEST(UrlLauncherPlugin, CanLaunchFailureInvalidUrl) { + g_autoptr(FlValue) args = fl_value_new_map(); + fl_value_set_string_take(args, "url", fl_value_new_string("")); + FlMethodResponse* response = can_launch(nullptr, args); + ASSERT_NE(response, nullptr); + ASSERT_TRUE(FL_IS_METHOD_SUCCESS_RESPONSE(response)); + g_autoptr(FlValue) expected = fl_value_new_bool(false); + EXPECT_TRUE(fl_value_equal(fl_method_success_response_get_result( + FL_METHOD_SUCCESS_RESPONSE(response)), + expected)); +} + +} // namespace test +} // namespace url_launcher_plugin 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 new file mode 100644 index 000000000000..d3f454ee7198 --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/linux/url_launcher_plugin.cc @@ -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. + +#include "include/url_launcher_linux/url_launcher_plugin.h" + +#include +#include + +#include + +#include "url_launcher_plugin_private.h" + +// See url_launcher_channel.dart for documentation. +const char kChannelName[] = "plugins.flutter.io/url_launcher"; +const char kBadArgumentsError[] = "Bad Arguments"; +const char kLaunchError[] = "Launch Error"; +const char kCanLaunchMethod[] = "canLaunch"; +const char kLaunchMethod[] = "launch"; +const char kUrlKey[] = "url"; + +struct _FlUrlLauncherPlugin { + GObject parent_instance; + + FlPluginRegistrar* registrar; + + // Connection to Flutter engine. + FlMethodChannel* channel; +}; + +G_DEFINE_TYPE(FlUrlLauncherPlugin, fl_url_launcher_plugin, g_object_get_type()) + +// Gets the URL from the arguments or generates an error. +static gchar* get_url(FlValue* args, GError** error) { + if (fl_value_get_type(args) != FL_VALUE_TYPE_MAP) { + g_set_error(error, 0, 0, "Argument map missing or malformed"); + return nullptr; + } + FlValue* url_value = fl_value_lookup_string(args, kUrlKey); + if (url_value == nullptr) { + g_set_error(error, 0, 0, "Missing URL"); + return nullptr; + } + + return g_strdup(fl_value_get_string(url_value)); +} + +// Called to check if a URL can be launched. +FlMethodResponse* can_launch(FlUrlLauncherPlugin* self, FlValue* args) { + g_autoptr(GError) error = nullptr; + g_autofree gchar* url = get_url(args, &error); + if (url == nullptr) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + kBadArgumentsError, error->message, nullptr)); + } + + gboolean is_launchable = FALSE; + g_autofree gchar* scheme = g_uri_parse_scheme(url); + if (scheme != nullptr) { + g_autoptr(GAppInfo) app_info = + g_app_info_get_default_for_uri_scheme(scheme); + is_launchable = app_info != nullptr; + } + + g_autoptr(FlValue) result = fl_value_new_bool(is_launchable); + return FL_METHOD_RESPONSE(fl_method_success_response_new(result)); +} + +// Called when a URL should launch. +static FlMethodResponse* launch(FlUrlLauncherPlugin* self, FlValue* args) { + g_autoptr(GError) error = nullptr; + g_autofree gchar* url = get_url(args, &error); + if (url == nullptr) { + return FL_METHOD_RESPONSE(fl_method_error_response_new( + kBadArgumentsError, error->message, nullptr)); + } + + FlView* view = fl_plugin_registrar_get_view(self->registrar); + gboolean launched; + if (view != nullptr) { + GtkWindow* window = GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(view))); + launched = gtk_show_uri_on_window(window, url, GDK_CURRENT_TIME, &error); + } else { + launched = g_app_info_launch_default_for_uri(url, nullptr, &error); + } + if (!launched) { + g_autofree gchar* message = + g_strdup_printf("Failed to launch URL: %s", error->message); + return FL_METHOD_RESPONSE( + fl_method_error_response_new(kLaunchError, message, nullptr)); + } + + g_autoptr(FlValue) result = fl_value_new_bool(TRUE); + return FL_METHOD_RESPONSE(fl_method_success_response_new(result)); +} + +// Called when a method call is received from Flutter. +static void method_call_cb(FlMethodChannel* channel, FlMethodCall* method_call, + gpointer user_data) { + FlUrlLauncherPlugin* self = FL_URL_LAUNCHER_PLUGIN(user_data); + + const gchar* method = fl_method_call_get_name(method_call); + FlValue* args = fl_method_call_get_args(method_call); + + g_autoptr(FlMethodResponse) response = nullptr; + if (strcmp(method, kCanLaunchMethod) == 0) + response = can_launch(self, args); + else if (strcmp(method, kLaunchMethod) == 0) + response = launch(self, args); + else + response = FL_METHOD_RESPONSE(fl_method_not_implemented_response_new()); + + g_autoptr(GError) error = nullptr; + if (!fl_method_call_respond(method_call, response, &error)) + g_warning("Failed to send method call response: %s", error->message); +} + +static void fl_url_launcher_plugin_dispose(GObject* object) { + FlUrlLauncherPlugin* self = FL_URL_LAUNCHER_PLUGIN(object); + + g_clear_object(&self->registrar); + g_clear_object(&self->channel); + + G_OBJECT_CLASS(fl_url_launcher_plugin_parent_class)->dispose(object); +} + +static void fl_url_launcher_plugin_class_init(FlUrlLauncherPluginClass* klass) { + G_OBJECT_CLASS(klass)->dispose = fl_url_launcher_plugin_dispose; +} + +FlUrlLauncherPlugin* fl_url_launcher_plugin_new(FlPluginRegistrar* registrar) { + FlUrlLauncherPlugin* self = FL_URL_LAUNCHER_PLUGIN( + g_object_new(fl_url_launcher_plugin_get_type(), nullptr)); + + self->registrar = FL_PLUGIN_REGISTRAR(g_object_ref(registrar)); + + g_autoptr(FlStandardMethodCodec) codec = fl_standard_method_codec_new(); + self->channel = + fl_method_channel_new(fl_plugin_registrar_get_messenger(registrar), + kChannelName, FL_METHOD_CODEC(codec)); + fl_method_channel_set_method_call_handler(self->channel, method_call_cb, + g_object_ref(self), g_object_unref); + + return self; +} + +static void fl_url_launcher_plugin_init(FlUrlLauncherPlugin* self) {} + +void url_launcher_plugin_register_with_registrar(FlPluginRegistrar* registrar) { + FlUrlLauncherPlugin* plugin = fl_url_launcher_plugin_new(registrar); + g_object_unref(plugin); +} diff --git a/packages/url_launcher/url_launcher_linux/linux/url_launcher_plugin_private.h b/packages/url_launcher/url_launcher_linux/linux/url_launcher_plugin_private.h new file mode 100644 index 000000000000..cde5242a8f47 --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/linux/url_launcher_plugin_private.h @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include + +#include "include/url_launcher_linux/url_launcher_plugin.h" + +// TODO(stuartmorgan): Remove this private header and change the below back to +// a static function once https://github.com/flutter/flutter/issues/88724 +// is fixed, and test through the public API instead. + +// Handles the canLaunch method call. +FlMethodResponse* can_launch(FlUrlLauncherPlugin* self, FlValue* args); diff --git a/packages/url_launcher/url_launcher_linux/pubspec.yaml b/packages/url_launcher/url_launcher_linux/pubspec.yaml new file mode 100644 index 000000000000..960216851e5d --- /dev/null +++ b/packages/url_launcher/url_launcher_linux/pubspec.yaml @@ -0,0 +1,20 @@ +name: url_launcher_linux +description: Linux implementation of the url_launcher plugin. +repository: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_linux +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 +version: 2.0.2 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +flutter: + plugin: + implements: url_launcher + platforms: + linux: + pluginClass: UrlLauncherPlugin + +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 31ac1f8bb45d..96d2fd49c7e7 100644 --- a/packages/url_launcher/url_launcher_macos/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_macos/CHANGELOG.md @@ -1,3 +1,38 @@ +## 2.0.2 + +* Replaced reference to `shared_preferences` plugin with the `url_launcher` in the README. + +## 2.0.1 + +* Add native unit tests. +* Updated installation instructions in README. + +## 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. + +# 0.0.1+7 + +* Remove Android folder from url_launcher_web and url_launcher_macos. + # 0.0.1+6 * Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). @@ -26,4 +61,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 0c382ce171cc..c6823b81eb84 100644 --- a/packages/url_launcher/url_launcher_macos/LICENSE +++ b/packages/url_launcher/url_launcher_macos/LICENSE @@ -1,27 +1,25 @@ -// 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. +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/url_launcher/url_launcher_macos/README.md b/packages/url_launcher/url_launcher_macos/README.md index e0c326ba86bf..0869f0ce9940 100644 --- a/packages/url_launcher/url_launcher_macos/README.md +++ b/packages/url_launcher/url_launcher_macos/README.md @@ -1,41 +1,11 @@ -# url_launcher_macos +# url\_launcher\_macos 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 - -This package has been endorsed, meaning that you only need to add `url_launcher` -as a dependency in your `pubspec.yaml`. It will be automatically included in your app -when you depend on `package:url_launcher`. - -This is what the above means to your `pubspec.yaml`: - -```yaml -... -dependencies: - ... - url_launcher: ^5.4.1 - ... -``` - -If you wish to use the macos package only, you can add `url_launcher_macos` as a -dependency: - -```yaml -... -dependencies: - ... - url_launcher_macos: ^0.0.1 - ... -``` +This package is [endorsed][2], which means you can simply use `url_launcher` +normally. This package will be automatically included in your app when you do. -[1]: ../url_launcher/url_launcher +[1]: https://pub.dev/packages/url_launcher +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/url_launcher/url_launcher_macos/android/.gitignore b/packages/url_launcher/url_launcher_macos/android/.gitignore deleted file mode 100644 index c6cbe562a427..000000000000 --- a/packages/url_launcher/url_launcher_macos/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/url_launcher/url_launcher_macos/android/build.gradle b/packages/url_launcher/url_launcher_macos/android/build.gradle deleted file mode 100644 index 2012041d8964..000000000000 --- a/packages/url_launcher/url_launcher_macos/android/build.gradle +++ /dev/null @@ -1,33 +0,0 @@ -group 'io.flutter.plugins.url_launcher_macos' -version '1.0' - -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - } -} - -rootProject.allprojects { - repositories { - google() - jcenter() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 28 - - defaultConfig { - minSdkVersion 16 - } - lintOptions { - disable 'InvalidPackage' - } -} diff --git a/packages/url_launcher/url_launcher_macos/android/gradle.properties b/packages/url_launcher/url_launcher_macos/android/gradle.properties deleted file mode 100644 index 7be3d8b46841..000000000000 --- a/packages/url_launcher/url_launcher_macos/android/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true diff --git a/packages/url_launcher/url_launcher_macos/android/gradle/wrapper/gradle-wrapper.properties b/packages/url_launcher/url_launcher_macos/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index d757f3d33fcc..000000000000 --- a/packages/url_launcher/url_launcher_macos/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/packages/url_launcher/url_launcher_macos/android/settings.gradle b/packages/url_launcher/url_launcher_macos/android/settings.gradle deleted file mode 100644 index 92bf3ef92267..000000000000 --- a/packages/url_launcher/url_launcher_macos/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'url_launcher_macos' diff --git a/packages/url_launcher/url_launcher_macos/android/src/main/AndroidManifest.xml b/packages/url_launcher/url_launcher_macos/android/src/main/AndroidManifest.xml deleted file mode 100644 index 16cdb8184a7d..000000000000 --- a/packages/url_launcher/url_launcher_macos/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/packages/url_launcher/url_launcher_macos/android/src/main/java/io/flutter/plugins/url_launcher_macos/UrlLauncherMacosPlugin.java b/packages/url_launcher/url_launcher_macos/android/src/main/java/io/flutter/plugins/url_launcher_macos/UrlLauncherMacosPlugin.java deleted file mode 100644 index ec0a5c88f2a7..000000000000 --- a/packages/url_launcher/url_launcher_macos/android/src/main/java/io/flutter/plugins/url_launcher_macos/UrlLauncherMacosPlugin.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.flutter.plugins.url_launcher_macos; - -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.PluginRegistry.Registrar; - -public class UrlLauncherMacosPlugin implements FlutterPlugin { - @Override - public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) {} - - public static void registerWith(Registrar registrar) {} - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) {} -} 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/android/app/build.gradle b/packages/url_launcher/url_launcher_macos/example/android/app/build.gradle deleted file mode 100644 index 7a6cf5df0d33..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/android/app/build.gradle +++ /dev/null @@ -1,60 +0,0 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -apply plugin: 'com.android.application' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -android { - compileSdkVersion 28 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - applicationId "io.flutter.plugins.urllauncherexample" - 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/url_launcher/url_launcher_macos/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/EmbeddingV1ActivityTest.java b/packages/url_launcher/url_launcher_macos/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/EmbeddingV1ActivityTest.java deleted file mode 100644 index bae2957ab81e..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.flutter.plugins.urllauncherexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} diff --git a/packages/url_launcher/url_launcher_macos/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/MainActivityTest.java b/packages/url_launcher/url_launcher_macos/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/MainActivityTest.java deleted file mode 100644 index 4dda10f32621..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/android/app/src/androidTestDebug/java/io/flutter/plugins/urllauncherexample/MainActivityTest.java +++ /dev/null @@ -1,11 +0,0 @@ -package io.flutter.plugins.urllauncherexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class MainActivityTest { - @Rule public ActivityTestRule rule = new ActivityTestRule<>(MainActivity.class); -} diff --git a/packages/url_launcher/url_launcher_macos/example/android/app/src/main/AndroidManifest.xml b/packages/url_launcher/url_launcher_macos/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index d30c15fd9ea8..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/packages/url_launcher/url_launcher_macos/example/android/app/src/main/java/io/flutter/plugins/urllauncherexample/EmbeddingV1Activity.java b/packages/url_launcher/url_launcher_macos/example/android/app/src/main/java/io/flutter/plugins/urllauncherexample/EmbeddingV1Activity.java deleted file mode 100644 index e52ccfc18b47..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/android/app/src/main/java/io/flutter/plugins/urllauncherexample/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.urllauncherexample; - -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; - -public class EmbeddingV1Activity extends FlutterActivity { - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); - } -} diff --git a/packages/url_launcher/url_launcher_macos/example/android/app/src/main/java/io/flutter/plugins/urllauncherexample/MainActivity.java b/packages/url_launcher/url_launcher_macos/example/android/app/src/main/java/io/flutter/plugins/urllauncherexample/MainActivity.java deleted file mode 100644 index 76f7df752842..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/android/app/src/main/java/io/flutter/plugins/urllauncherexample/MainActivity.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.flutter.plugins.urllauncherexample; - -import io.flutter.embedding.android.FlutterActivity; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.plugins.urllauncher.UrlLauncherPlugin; - -public class MainActivity extends FlutterActivity { - @Override - public void configureFlutterEngine(FlutterEngine flutterEngine) { - flutterEngine.getPlugins().add(new UrlLauncherPlugin()); - } -} diff --git a/packages/url_launcher/url_launcher_macos/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/url_launcher/url_launcher_macos/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4b7b09..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/url_launcher/url_launcher_macos/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17987b79bb8a..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/url_launcher/url_launcher_macos/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 09d4391482be..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/url_launcher/url_launcher_macos/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d34e7a..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/url_launcher/url_launcher_macos/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372eebdb2..000000000000 Binary files a/packages/url_launcher/url_launcher_macos/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/url_launcher/url_launcher_macos/example/android/build.gradle b/packages/url_launcher/url_launcher_macos/example/android/build.gradle deleted file mode 100644 index 6b1a639efd76..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/android/build.gradle +++ /dev/null @@ -1,29 +0,0 @@ -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.4.2' - } -} - -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/url_launcher/url_launcher_macos/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/url_launcher/url_launcher_macos/example/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 1cedb28ea41f..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Wed Jul 31 20:16:04 BRT 2019 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip diff --git a/packages/url_launcher/url_launcher_macos/example/android/settings.gradle b/packages/url_launcher/url_launcher_macos/example/android/settings.gradle deleted file mode 100644 index 115da6cb4f4d..000000000000 --- a/packages/url_launcher/url_launcher_macos/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/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 new file mode 100644 index 000000000000..897b22f89392 --- /dev/null +++ b/packages/url_launcher/url_launcher_macos/example/integration_test/url_launcher_test.dart @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('canLaunch', (WidgetTester _) async { + UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; + + expect(await launcher.canLaunch('randomstring'), false); + + // Generally all devices should have some default browser. + expect(await launcher.canLaunch('http://flutter.dev'), true); + + // Generally all devices should have some default SMS app. + 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 1125b6da73e9..6d12b75819b0 100644 --- a/packages/url_launcher/url_launcher_macos/example/pubspec.yaml +++ b/packages/url_launcher/url_launcher_macos/example/pubspec.yaml @@ -1,18 +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: - e2e: "^0.2.0" + 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 new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/url_launcher/url_launcher_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/url_launcher/url_launcher_macos/example/test_driver/url_launcher_e2e.dart b/packages/url_launcher/url_launcher_macos/example/test_driver/url_launcher_e2e.dart deleted file mode 100644 index e1d75f93b326..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/test_driver/url_launcher_e2e.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:e2e/e2e.dart'; -import 'package:url_launcher/url_launcher.dart'; - -void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); - - test('canLaunch', () async { - expect(await canLaunch('randomstring'), false); - - // Generally all devices should have some default browser. - expect(await 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. - }); -} diff --git a/packages/url_launcher/url_launcher_macos/example/test_driver/url_launcher_e2e_test.dart b/packages/url_launcher/url_launcher_macos/example/test_driver/url_launcher_e2e_test.dart deleted file mode 100644 index 1bcd0d37f450..000000000000 --- a/packages/url_launcher/url_launcher_macos/example/test_driver/url_launcher_e2e_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 'dart:io'; - -import 'package:flutter_driver/flutter_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); -} 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 9b4ffa6266ca..534830000626 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+6 -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.2 + +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 8766d7a3f239..fc56473533f2 100644 --- a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md @@ -1,3 +1,36 @@ +## 2.0.4 + +* Silenced warnings that may occur during build when using a very + recent version of Flutter relating to null safety. + +## 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 + ## 1.0.7 * Update lower bound of dart dependency to 2.1.0. diff --git a/packages/url_launcher/url_launcher_platform_interface/LICENSE b/packages/url_launcher/url_launcher_platform_interface/LICENSE index c89293372cf3..c6823b81eb84 100644 --- a/packages/url_launcher/url_launcher_platform_interface/LICENSE +++ b/packages/url_launcher/url_launcher_platform_interface/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/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..ffff14feb8d7 --- /dev/null +++ b/packages/url_launcher/url_launcher_platform_interface/lib/link.dart @@ -0,0 +1,112 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '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 = _ambiguate(WidgetsBinding.instance) + ?.platformDispatcher + .onPlatformMessage ?? + ui.channelBuffers.push; + sendMessage( + 'flutter/navigation', + _codec.encodeMethodCall( + MethodCall('pushRouteInformation', { + 'location': routeName, + 'state': null, + }), + ), + completer.complete, + ); + return completer.future; +} + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +// TODO(ianh): Remove this once we roll stable in late 2021. +T? _ambiguate(T? value) => value; diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart b/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart index 3fbd2ee01843..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,12 +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, + required bool useSafariVC, + required bool useWebView, + required bool enableJavaScript, + required bool enableDomStorage, + required bool universalLinksOnly, + required Map headers, + String? webOnlyWindowName, }) { return _channel.invokeMethod( 'launch', @@ -47,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 164555d63e0c..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,12 +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, + 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 4486134310c2..074e95b08c2c 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.7 +version: 2.0.4 + +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..23d9a4534f7b 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,16 +7,19 @@ 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'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); + // Store the initial instance before any tests change it. + final UrlLauncherPlatform initialInstance = UrlLauncherPlatform.instance; + group('$UrlLauncherPlatform', () { test('$MethodChannelUrlLauncher() is the default instance', () { - expect(UrlLauncherPlatform.instance, - isInstanceOf()); + expect(initialInstance, isInstanceOf()); }); test('Cannot be implemented with `implements`', () { @@ -41,6 +44,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 +68,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 +282,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 +313,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 df1b2c97b744..f5338e62a775 100644 --- a/packages/url_launcher/url_launcher_web/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_web/CHANGELOG.md @@ -1,3 +1,72 @@ +## 2.0.4 + +* Add `implements` to pubspec. + +## 2.0.3 + +- Replaced reference to `shared_preferences` plugin with the `url_launcher` in the README. + +## 2.0.2 + +- Updated installation instructions in README. + +# 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`. + +# 0.1.4+1 + +- Add a more correct attribution to `package:platform_detect` code. + +# 0.1.4 + +- (Null safety) Remove dependency on `package:platform_detect` +- Port unit tests to run with `flutter drive` + +# 0.1.3+2 + +- Fix a typo in a test name and fix some style inconsistencies. + +# 0.1.3+1 + +- Depend explicitly on the `platform_interface` package that adds the `webOnlyWindowName` parameter. + +# 0.1.3 + +- Added webOnlyWindowName parameter to launch() + +# 0.1.2+1 + +- Update docs + +# 0.1.2 + +- Adds "tel" and "sms" support + # 0.1.1+6 - Open "mailto" urls with target set as "\_top" on Safari browsers. diff --git a/packages/url_launcher/url_launcher_web/LICENSE b/packages/url_launcher/url_launcher_web/LICENSE index 0c382ce171cc..dd4ac737fc37 100644 --- a/packages/url_launcher/url_launcher_web/LICENSE +++ b/packages/url_launcher/url_launcher_web/LICENSE @@ -1,27 +1,231 @@ -// 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. +url_launcher_web + +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. +-------------------------------------------------------------------------------- +platform_detect + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2017 Workiva Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/url_launcher/url_launcher_web/README.md b/packages/url_launcher/url_launcher_web/README.md index 374301032778..8043c9fa07ff 100644 --- a/packages/url_launcher/url_launcher_web/README.md +++ b/packages/url_launcher/url_launcher_web/README.md @@ -1,37 +1,11 @@ -# url_launcher_web +# url\_launcher\_web The web implementation of [`url_launcher`][1]. -**Please set your constraint to `url_launcher_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 `url_launcher_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 -To use this plugin in your Flutter Web app, simply add it as a dependency in -your pubspec alongside the base `url_launcher` plugin. - -_(This is only temporary: in the future we hope to make this package an -"endorsed" implementation of `url_launcher`, so that it is automatically -included in your Flutter Web app when you depend on `package:url_launcher`.)_ - -This is what the above means to your `pubspec.yaml`: - -```yaml -... -dependencies: - ... - url_launcher: ^5.1.4 - url_launcher_web: ^0.1.0 - ... -``` - -### Use the plugin -Once you have the `url_launcher_web` dependency in your pubspec, you should -be able to use `package:url_launcher` as normal. +This package is [endorsed][2], which means you can simply use `url_launcher` +normally. This package will be automatically included in your app when you do. -[1]: ../url_launcher/url_launcher +[1]: https://pub.dev/packages/url_launcher +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/url_launcher/url_launcher_web/android/.gitignore b/packages/url_launcher/url_launcher_web/android/.gitignore deleted file mode 100644 index c6cbe562a427..000000000000 --- a/packages/url_launcher/url_launcher_web/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/url_launcher/url_launcher_web/android/build.gradle b/packages/url_launcher/url_launcher_web/android/build.gradle deleted file mode 100644 index e58b8f81aae4..000000000000 --- a/packages/url_launcher/url_launcher_web/android/build.gradle +++ /dev/null @@ -1,33 +0,0 @@ -group 'io.flutter.url_launcher_web' -version '1.0' - -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - } -} - -rootProject.allprojects { - repositories { - google() - jcenter() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 28 - - defaultConfig { - minSdkVersion 16 - } - lintOptions { - disable 'InvalidPackage' - } -} diff --git a/packages/url_launcher/url_launcher_web/android/gradle.properties b/packages/url_launcher/url_launcher_web/android/gradle.properties deleted file mode 100644 index 7be3d8b46841..000000000000 --- a/packages/url_launcher/url_launcher_web/android/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true diff --git a/packages/url_launcher/url_launcher_web/android/gradle/wrapper/gradle-wrapper.properties b/packages/url_launcher/url_launcher_web/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 019065d1d650..000000000000 --- a/packages/url_launcher/url_launcher_web/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/url_launcher/url_launcher_web/android/settings.gradle b/packages/url_launcher/url_launcher_web/android/settings.gradle deleted file mode 100644 index e632d77bba29..000000000000 --- a/packages/url_launcher/url_launcher_web/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'url_launcher_web' diff --git a/packages/url_launcher/url_launcher_web/android/src/main/AndroidManifest.xml b/packages/url_launcher/url_launcher_web/android/src/main/AndroidManifest.xml deleted file mode 100644 index cef3aa48f1b5..000000000000 --- a/packages/url_launcher/url_launcher_web/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/packages/url_launcher/url_launcher_web/android/src/main/java/io/flutter/url_launcher_web/UrlLauncherWebPlugin.java b/packages/url_launcher/url_launcher_web/android/src/main/java/io/flutter/url_launcher_web/UrlLauncherWebPlugin.java deleted file mode 100644 index 8682ce2587a6..000000000000 --- a/packages/url_launcher/url_launcher_web/android/src/main/java/io/flutter/url_launcher_web/UrlLauncherWebPlugin.java +++ /dev/null @@ -1,28 +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.url_launcher_web; - -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.PluginRegistry.Registrar; - -/** UrlLauncherWebPlugin */ -public class UrlLauncherWebPlugin implements FlutterPlugin { - @Override - public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) {} - - // This static function is optional and equivalent to onAttachedToEngine. It supports the old - // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting - // plugin registration via this function while apps migrate to use the new Android APIs - // post-flutter-1.12 via https://flutter.dev/go/android-project-migration. - // - // It is encouraged to share logic between onAttachedToEngine and registerWith to keep - // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called - // depending on the user's project. onAttachedToEngine or registerWith must both be defined - // in the same class. - public static void registerWith(Registrar registrar) {} - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) {} -} 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..3cdecfab2ab9 --- /dev/null +++ b/packages/url_launcher/url_launcher_web/example/README.md @@ -0,0 +1,12 @@ +# Testing + +This package uses `package:integration_test` to run its tests in a web browser. + +See [Plugin Tests > Web Tests](https://github.com/flutter/flutter/wiki/Plugin-Tests#web-tests) +in the Flutter wiki for instructions to setup and run the tests in this package. + +Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) +for more info. + +See [Plugin Tests > Web Tests > Mocks](https://github.com/flutter/flutter/wiki/Plugin-Tests#mocks) +in the Flutter wiki for more information about the `.mocks.dart` files in this package. \ No newline at end of file diff --git a/packages/url_launcher/url_launcher_web/example/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/src/third_party/platform_detect/LICENSE b/packages/url_launcher/url_launcher_web/lib/src/third_party/platform_detect/LICENSE new file mode 100644 index 000000000000..26b05d9b94c9 --- /dev/null +++ b/packages/url_launcher/url_launcher_web/lib/src/third_party/platform_detect/LICENSE @@ -0,0 +1,13 @@ +Copyright 2017 Workiva Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/url_launcher/url_launcher_web/lib/src/third_party/platform_detect/README.md b/packages/url_launcher/url_launcher_web/lib/src/third_party/platform_detect/README.md new file mode 100644 index 000000000000..7d6cfdfd994c --- /dev/null +++ b/packages/url_launcher/url_launcher_web/lib/src/third_party/platform_detect/README.md @@ -0,0 +1,5 @@ +The code in this directory is a stripped down, and modified version of `package:platform_detect`. + +You can find the original file in Workiva's repository, here: + +* https://github.com/Workiva/platform_detect/blob/77d160f1c3be4e20dc085a094209e8cab4aec135/lib/src/browser.dart diff --git a/packages/url_launcher/url_launcher_web/lib/src/third_party/platform_detect/browser.dart b/packages/url_launcher/url_launcher_web/lib/src/third_party/platform_detect/browser.dart new file mode 100644 index 000000000000..9e83c391de0b --- /dev/null +++ b/packages/url_launcher/url_launcher_web/lib/src/third_party/platform_detect/browser.dart @@ -0,0 +1,35 @@ +// Copyright 2017 Workiva Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// ////////////////////////////////////////////////////////// +// +// This file is a stripped down, and slightly modified version of +// package:platform_detect's. +// +// Original version here: https://github.com/Workiva/platform_detect +// +// ////////////////////////////////////////////////////////// + +import 'dart:html' as html show Navigator; + +/// Determines if the `navigator` is Safari. +bool navigatorIsSafari(html.Navigator navigator) { + // An web view running in an iOS app does not have a 'Version/X.X.X' string in the appVersion + final vendor = navigator.vendor; + final appVersion = navigator.appVersion; + return vendor != null && + vendor.contains('Apple') && + appVersion != null && + appVersion.contains('Version'); +} 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 e55ceb2269bc..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,44 +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 '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 'package:platform_detect/platform_detect.dart' show browser; +import 'src/link.dart'; +import 'src/third_party/platform_detect/browser.dart'; + +const _safariTargetTopSchemes = { + 'mailto', + 'tel', + 'sms', +}; +String? _getUrlScheme(String url) => Uri.tryParse(url)?.scheme; -const _mailtoScheme = 'mailto'; +bool _isSafariTargetTopScheme(String url) => + _safariTargetTopSchemes.contains(_getUrlScheme(url)); /// The web implementation of [UrlLauncherPlatform]. /// /// This class implements the `package:url_launcher` functionality for the web. class UrlLauncherPlugin extends UrlLauncherPlatform { html.Window _window; + bool _isSafari = false; // The set of schemes that can be handled by the plugin - static final _supportedSchemes = {'http', 'https', _mailtoScheme}; + static final _supportedSchemes = { + 'http', + 'https', + }.union(_safariTargetTopSchemes); /// A constructor that allows tests to override the window object used by the plugin. - UrlLauncherPlugin({@visibleForTesting html.Window window}) - : _window = window ?? html.window; + UrlLauncherPlugin({@visibleForTesting html.Window? debugWindow}) + : _window = debugWindow ?? html.window { + _isSafari = navigatorIsSafari(_window.navigator); + } /// Registers this class as the default instance of [UrlLauncherPlatform]. static void registerWith(Registrar registrar) { UrlLauncherPlatform.instance = UrlLauncherPlugin(); + ui.platformViewRegistry.registerViewFactory(linkViewType, linkViewFactory); } - String _getUrlScheme(String url) => Uri.tryParse(url)?.scheme; - - bool _isMailtoScheme(String url) => _getUrlScheme(url) == _mailtoScheme; + @override + LinkDelegate get linkDelegate { + return (LinkInfo linkInfo) => WebLinkDelegate(linkInfo); + } - /// Opens the given [url] in a new window. + /// Opens the given [url] in the specified [webOnlyWindowName]. /// /// Returns the newly created window. @visibleForTesting - html.WindowBase openNewWindow(String url) { - // We need to open mailto urls on the _top window context on safari browsers. + 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 = browser.isSafari && _isMailtoScheme(url) ? '_top' : ''; + final target = webOnlyWindowName ?? + ((_isSafari && _isSafariTargetTopScheme(url)) ? '_top' : ''); return _window.open(url, target); } @@ -50,13 +74,15 @@ 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, + 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) != null); + 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 207f2dc15424..77e8068f1396 100644 --- a/packages/url_launcher/url_launcher_web/pubspec.yaml +++ b/packages/url_launcher/url_launcher_web/pubspec.yaml @@ -1,34 +1,30 @@ 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.1+6 +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.4 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: + implements: url_launcher platforms: web: pluginClass: UrlLauncherPlugin fileName: url_launcher_web.dart dependencies: - url_launcher_platform_interface: ^1.0.1 - platform_detect: ^1.4.0 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 - -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 new file mode 100644 index 000000000000..7c5b4ad682ba --- /dev/null +++ b/packages/url_launcher/url_launcher_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/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/url_launcher_web_test.dart b/packages/url_launcher/url_launcher_web/test/url_launcher_web_test.dart deleted file mode 100644 index 4cf92062e10f..000000000000 --- a/packages/url_launcher/url_launcher_web/test/url_launcher_web_test.dart +++ /dev/null @@ -1,126 +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('chrome') // Uses web-only Flutter SDK - -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:platform_detect/test_utils.dart' as platform; - -class MockWindow extends Mock implements html.Window {} - -void main() { - group('$UrlLauncherPlugin', () { - MockWindow mockWindow = MockWindow(); - UrlLauncherPlugin plugin = UrlLauncherPlugin(window: mockWindow); - - setUp(() { - platform.configurePlatformForTesting(browser: platform.chrome); - }); - - group('canLaunch', () { - test('"http" URLs -> true', () { - expect(plugin.canLaunch('http://google.com'), completion(isTrue)); - }); - - test('"https" URLs -> true', () { - expect(plugin.canLaunch('https://google.com'), completion(isTrue)); - }); - - test('"mailto" URLs -> true', () { - expect( - plugin.canLaunch('mailto:name@mydomain.com'), completion(isTrue)); - }); - - test('"tel" URLs -> false', () { - expect(plugin.canLaunch('tel:5551234567'), completion(isFalse)); - }); - }); - - 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()); - }); - - test('launching a URL returns true', () { - expect( - plugin.launch( - 'https://www.google.com', - useSafariVC: null, - useWebView: null, - universalLinksOnly: null, - enableDomStorage: null, - enableJavaScript: null, - headers: null, - ), - completion(isTrue)); - }); - - test('launching a "mailto" returns true', () { - expect( - plugin.launch( - 'mailto:name@mydomain.com', - useSafariVC: null, - useWebView: null, - universalLinksOnly: null, - enableDomStorage: null, - enableJavaScript: null, - headers: null, - ), - completion(isTrue)); - }); - }); - - group('openNewWindow', () { - test('http urls should be launched in a new window', () { - plugin.openNewWindow('http://www.google.com'); - - verify(mockWindow.open('http://www.google.com', '')); - }); - - test('https urls should be launched in a new window', () { - plugin.openNewWindow('https://www.google.com'); - - verify(mockWindow.open('https://www.google.com', '')); - }); - - test('mailto urls should be launched on a new window', () { - plugin.openNewWindow('mailto:name@mydomain.com'); - - verify(mockWindow.open('mailto:name@mydomain.com', '')); - }); - - group('Safari', () { - setUp(() { - platform.configurePlatformForTesting(browser: platform.safari); - }); - - test('http urls should be launched in a new window', () { - plugin.openNewWindow('http://www.google.com'); - - verify(mockWindow.open('http://www.google.com', '')); - }); - - test('https urls should be launched in a new window', () { - plugin.openNewWindow('https://www.google.com'); - - verify(mockWindow.open('https://www.google.com', '')); - }); - - test('mailto urls should be launched on the same window', () { - plugin.openNewWindow('mailto:name@mydomain.com'); - - verify(mockWindow.open('mailto:name@mydomain.com', '_top')); - }); - }); - }); - }); -} diff --git a/packages/url_launcher/url_launcher_windows/.gitignore b/packages/url_launcher/url_launcher_windows/.gitignore new file mode 100644 index 000000000000..53e92cc4181f --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/.gitignore @@ -0,0 +1,3 @@ +.packages +.flutter-plugins +pubspec.lock diff --git a/packages/url_launcher/url_launcher_windows/.metadata b/packages/url_launcher/url_launcher_windows/.metadata new file mode 100644 index 000000000000..720a4596c087 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 6d1c244b79f3a2747281f718297ce248bd5ad099 + channel: master + +project_type: plugin diff --git a/packages/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 new file mode 100644 index 000000000000..d095a52341b5 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/CHANGELOG.md @@ -0,0 +1,41 @@ +## NEXT + +* Added unit tests. + +## 2.0.2 + +* Replaced reference to `shared_preferences` plugin with the `url_launcher` in the README. + +## 2.0.1 + +* Updated installation instructions in README. + +## 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. + +## 0.0.1 + +* Initial Windows implementation of `url_launcher`. diff --git a/packages/url_launcher/url_launcher_windows/LICENSE b/packages/url_launcher/url_launcher_windows/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/LICENSE @@ -0,0 +1,25 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/packages/url_launcher/url_launcher_windows/README.md b/packages/url_launcher/url_launcher_windows/README.md new file mode 100644 index 000000000000..cd7b6d47eeb2 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/README.md @@ -0,0 +1,11 @@ +# url\_launcher\_windows + +The Windows implementation of [`url_launcher`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `url_launcher` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/url_launcher +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/url_launcher/url_launcher_windows/example/.gitignore b/packages/url_launcher/url_launcher_windows/example/.gitignore new file mode 100644 index 000000000000..9d532b18a01f --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/example/.gitignore @@ -0,0 +1,41 @@ +# 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 diff --git a/packages/url_launcher/url_launcher_windows/example/.metadata b/packages/url_launcher/url_launcher_windows/example/.metadata new file mode 100644 index 000000000000..82cce8b18642 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/example/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 6d1c244b79f3a2747281f718297ce248bd5ad099 + channel: master + +project_type: app diff --git a/packages/url_launcher/url_launcher_windows/example/README.md b/packages/url_launcher/url_launcher_windows/example/README.md new file mode 100644 index 000000000000..e444852697b9 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/example/README.md @@ -0,0 +1,3 @@ +# url_launcher_windows_example + +Demonstrates the url_launcher_windows plugin. 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 new file mode 100644 index 000000000000..ae9a9148f9d7 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/example/integration_test/url_launcher_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:integration_test/integration_test.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('canLaunch', (WidgetTester _) async { + UrlLauncherPlatform launcher = UrlLauncherPlatform.instance; + + expect(await launcher.canLaunch('randomstring'), false); + + // Generally all devices should have some default browser. + expect(await launcher.canLaunch('http://flutter.dev'), true); + }); +} diff --git a/packages/url_launcher/url_launcher_windows/example/lib/main.dart b/packages/url_launcher/url_launcher_windows/example/lib/main.dart new file mode 100644 index 000000000000..86e06f3fafed --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/example/lib/main.dart @@ -0,0 +1,93 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'URL Launcher', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: MyHomePage(title: 'URL Launcher'), + ); + } +} + +class MyHomePage extends StatefulWidget { + MyHomePage({Key? key, required this.title}) : super(key: key); + final String title; + + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + Future? _launched; + + Future _launchInBrowser(String url) async { + if (await UrlLauncherPlatform.instance.canLaunch(url)) { + await UrlLauncherPlatform.instance.launch( + url, + useSafariVC: false, + useWebView: false, + enableJavaScript: false, + enableDomStorage: false, + universalLinksOnly: false, + headers: {}, + ); + } else { + throw 'Could not launch $url'; + } + } + + Widget _launchStatus(BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } else { + return const Text(''); + } + } + + @override + Widget build(BuildContext context) { + const String toLaunch = 'https://www.cylog.org/headers/'; + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: ListView( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Padding( + padding: EdgeInsets.all(16.0), + child: Text(toLaunch), + ), + ElevatedButton( + onPressed: () => setState(() { + _launched = _launchInBrowser(toLaunch); + }), + child: const Text('Launch in browser'), + ), + const Padding(padding: EdgeInsets.all(16.0)), + FutureBuilder(future: _launched, builder: _launchStatus), + ], + ), + ], + ), + ); + } +} diff --git a/packages/url_launcher/url_launcher_windows/example/pubspec.yaml b/packages/url_launcher/url_launcher_windows/example/pubspec.yaml new file mode 100644 index 000000000000..11be3e84f03b --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/example/pubspec.yaml @@ -0,0 +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: ^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: + sdk: flutter + flutter_driver: + sdk: flutter + 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 new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/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..5b1622bcb333 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/example/windows/CMakeLists.txt @@ -0,0 +1,98 @@ +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") + +# Enable the test target. +set(include_url_launcher_windows_tests TRUE) + +# 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..744f08a9389b --- /dev/null +++ b/packages/url_launcher/url_launcher_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/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..4f7884874da7 --- /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) { + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); +} diff --git a/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.h b/packages/url_launcher/url_launcher_windows/example/windows/flutter/generated_plugin_registrant.h 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/pubspec.yaml b/packages/url_launcher/url_launcher_windows/pubspec.yaml new file mode 100644 index 000000000000..a92e91ee4568 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/pubspec.yaml @@ -0,0 +1,20 @@ +name: url_launcher_windows +description: Windows implementation of the url_launcher plugin. +repository: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_windows +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+url_launcher%22 +version: 2.0.2 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" + +flutter: + plugin: + implements: url_launcher + platforms: + windows: + pluginClass: UrlLauncherWindows + +dependencies: + flutter: + sdk: flutter diff --git a/packages/url_launcher/url_launcher_windows/windows/.gitignore b/packages/url_launcher/url_launcher_windows/windows/.gitignore new file mode 100644 index 000000000000..b3eb2be169a5 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt b/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt new file mode 100644 index 000000000000..a4185acff6a1 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/windows/CMakeLists.txt @@ -0,0 +1,71 @@ +cmake_minimum_required(VERSION 3.10) +set(PROJECT_NAME "url_launcher_windows") +project(${PROJECT_NAME} LANGUAGES CXX) + +set(PLUGIN_NAME "${PROJECT_NAME}_plugin") + +list(APPEND PLUGIN_SOURCES + "system_apis.cpp" + "system_apis.h" + "url_launcher_plugin.cpp" + "url_launcher_plugin.h" +) + +add_library(${PLUGIN_NAME} SHARED + "include/url_launcher_windows/url_launcher_windows.h" + "url_launcher_windows.cpp" + ${PLUGIN_SOURCES} +) +apply_standard_settings(${PLUGIN_NAME}) +set_target_properties(${PLUGIN_NAME} PROPERTIES CXX_VISIBILITY_PRESET hidden) +target_compile_definitions(${PLUGIN_NAME} PRIVATE FLUTTER_PLUGIN_IMPL) +target_include_directories(${PLUGIN_NAME} INTERFACE + "${CMAKE_CURRENT_SOURCE_DIR}/include") +target_link_libraries(${PLUGIN_NAME} PRIVATE flutter flutter_wrapper_plugin) + +# List of absolute paths to libraries that should be bundled with the plugin +set(file_chooser_bundled_libraries + "" + PARENT_SCOPE +) + + +# === Tests === + +if (${include_${PROJECT_NAME}_tests}) +set(TEST_RUNNER "${PROJECT_NAME}_test") +enable_testing() +# TODO(stuartmorgan): Consider using a single shared, pre-checked-in googletest +# instance rather than downloading for each plugin. This approach makes sense +# for a template, but not for a monorepo with many plugins. +include(FetchContent) +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/release-1.11.0.zip +) +# Prevent overriding the parent project's compiler/linker settings +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +# Disable install commands for gtest so it doesn't end up in the bundle. +set(INSTALL_GTEST OFF CACHE BOOL "Disable installation of googletest" FORCE) + +FetchContent_MakeAvailable(googletest) + +# The plugin's C API is not very useful for unit testing, so build the sources +# directly into the test binary rather than using the DLL. +add_executable(${TEST_RUNNER} + test/url_launcher_windows_test.cpp + ${PLUGIN_SOURCES} +) +apply_standard_settings(${TEST_RUNNER}) +target_include_directories(${TEST_RUNNER} PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}") +target_link_libraries(${TEST_RUNNER} PRIVATE flutter_wrapper_plugin) +target_link_libraries(${TEST_RUNNER} PRIVATE gtest_main gmock) +# flutter_wrapper_plugin has link dependencies on the Flutter DLL. +add_custom_command(TARGET ${TEST_RUNNER} POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${FLUTTER_LIBRARY}" $ +) + +include(GoogleTest) +gtest_discover_tests(${TEST_RUNNER}) +endif() diff --git a/packages/url_launcher/url_launcher_windows/windows/include/url_launcher_windows/url_launcher_windows.h b/packages/url_launcher/url_launcher_windows/windows/include/url_launcher_windows/url_launcher_windows.h new file mode 100644 index 000000000000..251471c9fe56 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/windows/include/url_launcher_windows/url_launcher_windows.h @@ -0,0 +1,26 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#ifndef PACKAGES_URL_LAUNCHER_URL_LAUNCHER_WINDOWS_WINDOWS_INCLUDE_URL_LAUNCHER_WINDOWS_URL_LAUNCHER_PLUGIN_H_ +#define PACKAGES_URL_LAUNCHER_URL_LAUNCHER_WINDOWS_WINDOWS_INCLUDE_URL_LAUNCHER_WINDOWS_URL_LAUNCHER_PLUGIN_H_ + +#include + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __declspec(dllexport) +#else +#define FLUTTER_PLUGIN_EXPORT __declspec(dllimport) +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FLUTTER_PLUGIN_EXPORT void UrlLauncherWindowsRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // 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/system_apis.cpp b/packages/url_launcher/url_launcher_windows/windows/system_apis.cpp new file mode 100644 index 000000000000..abd690b6e47f --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/windows/system_apis.cpp @@ -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. +#include "system_apis.h" + +#include + +namespace url_launcher_plugin { + +SystemApis::SystemApis() {} + +SystemApis::~SystemApis() {} + +SystemApisImpl::SystemApisImpl() {} + +SystemApisImpl::~SystemApisImpl() {} + +LSTATUS SystemApisImpl::RegCloseKey(HKEY key) { return ::RegCloseKey(key); } + +LSTATUS SystemApisImpl::RegOpenKeyExW(HKEY key, LPCWSTR sub_key, DWORD options, + REGSAM desired, PHKEY result) { + return ::RegOpenKeyExW(key, sub_key, options, desired, result); +} + +LSTATUS SystemApisImpl::RegQueryValueExW(HKEY key, LPCWSTR value_name, + LPDWORD type, LPBYTE data, + LPDWORD data_size) { + return ::RegQueryValueExW(key, value_name, nullptr, type, data, data_size); +} + +HINSTANCE SystemApisImpl::ShellExecuteW(HWND hwnd, LPCWSTR operation, + LPCWSTR file, LPCWSTR parameters, + LPCWSTR directory, int show_flags) { + return ::ShellExecuteW(hwnd, operation, file, parameters, directory, + show_flags); +} + +} // namespace url_launcher_plugin diff --git a/packages/url_launcher/url_launcher_windows/windows/system_apis.h b/packages/url_launcher/url_launcher_windows/windows/system_apis.h new file mode 100644 index 000000000000..7b56704d8e04 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/windows/system_apis.h @@ -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. +#include + +namespace url_launcher_plugin { + +// An interface wrapping system APIs used by the plugin, for mocking. +class SystemApis { + public: + SystemApis(); + virtual ~SystemApis(); + + // Disallow copy and move. + SystemApis(const SystemApis&) = delete; + SystemApis& operator=(const SystemApis&) = delete; + + // Wrapper for RegCloseKey. + virtual LSTATUS RegCloseKey(HKEY key) = 0; + + // Wrapper for RegQueryValueEx. + virtual LSTATUS RegQueryValueExW(HKEY key, LPCWSTR value_name, LPDWORD type, + LPBYTE data, LPDWORD data_size) = 0; + + // Wrapper for RegOpenKeyEx. + virtual LSTATUS RegOpenKeyExW(HKEY key, LPCWSTR sub_key, DWORD options, + REGSAM desired, PHKEY result) = 0; + + // Wrapper for ShellExecute. + virtual HINSTANCE ShellExecuteW(HWND hwnd, LPCWSTR operation, LPCWSTR file, + LPCWSTR parameters, LPCWSTR directory, + int show_flags) = 0; +}; + +// Implementation of SystemApis using the Win32 APIs. +class SystemApisImpl : public SystemApis { + public: + SystemApisImpl(); + virtual ~SystemApisImpl(); + + // Disallow copy and move. + SystemApisImpl(const SystemApisImpl&) = delete; + SystemApisImpl& operator=(const SystemApisImpl&) = delete; + + // SystemApis Implementation: + virtual LSTATUS RegCloseKey(HKEY key); + virtual LSTATUS RegOpenKeyExW(HKEY key, LPCWSTR sub_key, DWORD options, + REGSAM desired, PHKEY result); + virtual LSTATUS RegQueryValueExW(HKEY key, LPCWSTR value_name, LPDWORD type, + LPBYTE data, LPDWORD data_size); + virtual HINSTANCE ShellExecuteW(HWND hwnd, LPCWSTR operation, LPCWSTR file, + LPCWSTR parameters, LPCWSTR directory, + int show_flags); +}; + +} // namespace url_launcher_plugin diff --git a/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp b/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp new file mode 100644 index 000000000000..191d51a0caa8 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/windows/test/url_launcher_windows_test.cpp @@ -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. +#include +#include +#include +#include +#include +#include + +#include +#include + +#include "url_launcher_plugin.h" + +namespace url_launcher_plugin { +namespace test { + +namespace { + +using flutter::EncodableMap; +using flutter::EncodableValue; +using ::testing::DoAll; +using ::testing::Pointee; +using ::testing::Return; +using ::testing::SetArgPointee; + +class MockSystemApis : public SystemApis { + public: + MOCK_METHOD(LSTATUS, RegCloseKey, (HKEY key), (override)); + MOCK_METHOD(LSTATUS, RegQueryValueExW, + (HKEY key, LPCWSTR value_name, LPDWORD type, LPBYTE data, + LPDWORD data_size), + (override)); + MOCK_METHOD(LSTATUS, RegOpenKeyExW, + (HKEY key, LPCWSTR sub_key, DWORD options, REGSAM desired, + PHKEY result), + (override)); + MOCK_METHOD(HINSTANCE, ShellExecuteW, + (HWND hwnd, LPCWSTR operation, LPCWSTR file, LPCWSTR parameters, + LPCWSTR directory, int show_flags), + (override)); +}; + +class MockMethodResult : public flutter::MethodResult<> { + public: + MOCK_METHOD(void, SuccessInternal, (const EncodableValue* result), + (override)); + MOCK_METHOD(void, ErrorInternal, + (const std::string& error_code, const std::string& error_message, + const EncodableValue* details), + (override)); + MOCK_METHOD(void, NotImplementedInternal, (), (override)); +}; + +std::unique_ptr CreateArgumentsWithUrl(const std::string& url) { + EncodableMap args = { + {EncodableValue("url"), EncodableValue(url)}, + }; + return std::make_unique(args); +} + +} // namespace + +TEST(UrlLauncherPlugin, CanLaunchSuccessTrue) { + std::unique_ptr system = std::make_unique(); + std::unique_ptr result = + std::make_unique(); + + // Return success values from the registery commands. + HKEY fake_key = reinterpret_cast(1); + EXPECT_CALL(*system, RegOpenKeyExW) + .WillOnce(DoAll(SetArgPointee<4>(fake_key), Return(ERROR_SUCCESS))); + EXPECT_CALL(*system, RegQueryValueExW).WillOnce(Return(ERROR_SUCCESS)); + EXPECT_CALL(*system, RegCloseKey(fake_key)).WillOnce(Return(ERROR_SUCCESS)); + // Expect a success response. + EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(true)))); + + UrlLauncherPlugin plugin(std::move(system)); + plugin.HandleMethodCall( + flutter::MethodCall("canLaunch", + CreateArgumentsWithUrl("https://some.url.com")), + std::move(result)); +} + +TEST(UrlLauncherPlugin, CanLaunchQueryFailure) { + std::unique_ptr system = std::make_unique(); + std::unique_ptr result = + std::make_unique(); + + // Return success values from the registery commands, except for the query, + // to simulate a scheme that is in the registry, but has no URL handler. + HKEY fake_key = reinterpret_cast(1); + EXPECT_CALL(*system, RegOpenKeyExW) + .WillOnce(DoAll(SetArgPointee<4>(fake_key), Return(ERROR_SUCCESS))); + EXPECT_CALL(*system, RegQueryValueExW).WillOnce(Return(ERROR_FILE_NOT_FOUND)); + EXPECT_CALL(*system, RegCloseKey(fake_key)).WillOnce(Return(ERROR_SUCCESS)); + // Expect a success response. + EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(false)))); + + UrlLauncherPlugin plugin(std::move(system)); + plugin.HandleMethodCall( + flutter::MethodCall("canLaunch", + CreateArgumentsWithUrl("https://some.url.com")), + std::move(result)); +} + +TEST(UrlLauncherPlugin, CanLaunchHandlesOpenFailure) { + std::unique_ptr system = std::make_unique(); + std::unique_ptr result = + std::make_unique(); + + // Return failure for opening. + EXPECT_CALL(*system, RegOpenKeyExW).WillOnce(Return(ERROR_BAD_PATHNAME)); + // Expect a success response. + EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(false)))); + + UrlLauncherPlugin plugin(std::move(system)); + plugin.HandleMethodCall( + flutter::MethodCall("canLaunch", + CreateArgumentsWithUrl("https://some.url.com")), + std::move(result)); +} + +TEST(UrlLauncherPlugin, LaunchSuccess) { + std::unique_ptr system = std::make_unique(); + std::unique_ptr result = + std::make_unique(); + + // Return a success value (>32) from launching. + EXPECT_CALL(*system, ShellExecuteW) + .WillOnce(Return(reinterpret_cast(33))); + // Expect a success response. + EXPECT_CALL(*result, SuccessInternal(Pointee(EncodableValue(true)))); + + UrlLauncherPlugin plugin(std::move(system)); + plugin.HandleMethodCall( + flutter::MethodCall("launch", + CreateArgumentsWithUrl("https://some.url.com")), + std::move(result)); +} + +TEST(UrlLauncherPlugin, LaunchReportsFailure) { + std::unique_ptr system = std::make_unique(); + std::unique_ptr result = + std::make_unique(); + + // Return a faile value (<=32) from launching. + EXPECT_CALL(*system, ShellExecuteW) + .WillOnce(Return(reinterpret_cast(32))); + // Expect an error response. + EXPECT_CALL(*result, ErrorInternal); + + UrlLauncherPlugin plugin(std::move(system)); + plugin.HandleMethodCall( + flutter::MethodCall("launch", + CreateArgumentsWithUrl("https://some.url.com")), + std::move(result)); +} + +} // namespace test +} // namespace url_launcher_plugin 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 new file mode 100644 index 000000000000..748c75ddd243 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.cpp @@ -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. +#include "url_launcher_plugin.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace url_launcher_plugin { + +namespace { + +using flutter::EncodableMap; +using flutter::EncodableValue; + +// Converts the given UTF-8 string to UTF-16. +std::wstring Utf16FromUtf8(const std::string& utf8_string) { + if (utf8_string.empty()) { + return std::wstring(); + } + int target_length = + ::MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8_string.data(), + static_cast(utf8_string.length()), nullptr, 0); + if (target_length == 0) { + return std::wstring(); + } + std::wstring utf16_string; + utf16_string.resize(target_length); + int converted_length = + ::MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, utf8_string.data(), + static_cast(utf8_string.length()), + utf16_string.data(), target_length); + if (converted_length == 0) { + return std::wstring(); + } + return utf16_string; +} + +// Returns the URL argument from |method_call| if it is present, otherwise +// returns an empty string. +std::string GetUrlArgument(const flutter::MethodCall<>& method_call) { + std::string url; + const auto* arguments = std::get_if(method_call.arguments()); + if (arguments) { + auto url_it = arguments->find(EncodableValue("url")); + if (url_it != arguments->end()) { + url = std::get(url_it->second); + } + } + return url; +} + +} // namespace + +// static +void UrlLauncherPlugin::RegisterWithRegistrar( + flutter::PluginRegistrar* registrar) { + auto channel = std::make_unique>( + registrar->messenger(), "plugins.flutter.io/url_launcher", + &flutter::StandardMethodCodec::GetInstance()); + + std::unique_ptr plugin = + std::make_unique(); + + channel->SetMethodCallHandler( + [plugin_pointer = plugin.get()](const auto& call, auto result) { + plugin_pointer->HandleMethodCall(call, std::move(result)); + }); + + registrar->AddPlugin(std::move(plugin)); +} + +UrlLauncherPlugin::UrlLauncherPlugin() + : system_apis_(std::make_unique()) {} + +UrlLauncherPlugin::UrlLauncherPlugin(std::unique_ptr system_apis) + : system_apis_(std::move(system_apis)) {} + +UrlLauncherPlugin::~UrlLauncherPlugin() = default; + +void UrlLauncherPlugin::HandleMethodCall( + const flutter::MethodCall<>& method_call, + std::unique_ptr> result) { + if (method_call.method_name().compare("launch") == 0) { + std::string url = GetUrlArgument(method_call); + if (url.empty()) { + result->Error("argument_error", "No URL provided"); + return; + } + + std::optional error = LaunchUrl(url); + if (error) { + result->Error("open_error", error.value()); + return; + } + result->Success(EncodableValue(true)); + } else if (method_call.method_name().compare("canLaunch") == 0) { + std::string url = GetUrlArgument(method_call); + if (url.empty()) { + result->Error("argument_error", "No URL provided"); + return; + } + + bool can_launch = CanLaunchUrl(url); + result->Success(EncodableValue(can_launch)); + } else { + result->NotImplemented(); + } +} + +bool UrlLauncherPlugin::CanLaunchUrl(const std::string& url) { + size_t separator_location = url.find(":"); + if (separator_location == std::string::npos) { + return false; + } + std::wstring scheme = Utf16FromUtf8(url.substr(0, separator_location)); + + HKEY key = nullptr; + if (system_apis_->RegOpenKeyExW(HKEY_CLASSES_ROOT, scheme.c_str(), 0, + KEY_QUERY_VALUE, &key) != ERROR_SUCCESS) { + return false; + } + bool has_handler = + system_apis_->RegQueryValueExW(key, L"URL Protocol", nullptr, nullptr, + nullptr) == ERROR_SUCCESS; + system_apis_->RegCloseKey(key); + return has_handler; +} + +std::optional UrlLauncherPlugin::LaunchUrl( + const std::string& url) { + std::wstring url_wide = Utf16FromUtf8(url); + + int status = static_cast(reinterpret_cast( + system_apis_->ShellExecuteW(nullptr, TEXT("open"), url_wide.c_str(), + nullptr, nullptr, SW_SHOWNORMAL))); + + // Per ::ShellExecuteW documentation, anything >32 indicates success. + if (status <= 32) { + std::ostringstream error_message; + error_message << "Failed to open " << url << ": ShellExecute error code " + << status; + return std::optional(error_message.str()); + } + return std::nullopt; +} + +} // namespace url_launcher_plugin diff --git a/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.h b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.h new file mode 100644 index 000000000000..45e70e5fc067 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/windows/url_launcher_plugin.h @@ -0,0 +1,48 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#include +#include +#include + +#include +#include +#include +#include + +#include "system_apis.h" + +namespace url_launcher_plugin { + +class UrlLauncherPlugin : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrar* registrar); + + UrlLauncherPlugin(); + + // Creates a plugin instance with the given SystemApi instance. + // + // Exists for unit testing with mock implementations. + UrlLauncherPlugin(std::unique_ptr system_apis); + + virtual ~UrlLauncherPlugin(); + + // Disallow copy and move. + UrlLauncherPlugin(const UrlLauncherPlugin&) = delete; + UrlLauncherPlugin& operator=(const UrlLauncherPlugin&) = delete; + + // Called when a method is called on the plugin channel. + void HandleMethodCall(const flutter::MethodCall<>& method_call, + std::unique_ptr> result); + + private: + // Returns whether or not the given URL has a registered handler. + bool CanLaunchUrl(const std::string& url); + + // Attempts to launch the given URL. On failure, returns an error string. + std::optional LaunchUrl(const std::string& url); + + std::unique_ptr system_apis_; +}; + +} // namespace url_launcher_plugin diff --git a/packages/url_launcher/url_launcher_windows/windows/url_launcher_windows.cpp b/packages/url_launcher/url_launcher_windows/windows/url_launcher_windows.cpp new file mode 100644 index 000000000000..05de586d8fe0 --- /dev/null +++ b/packages/url_launcher/url_launcher_windows/windows/url_launcher_windows.cpp @@ -0,0 +1,15 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +#include "include/url_launcher_windows/url_launcher_windows.h" + +#include + +#include "url_launcher_plugin.h" + +void UrlLauncherWindowsRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + url_launcher_plugin::UrlLauncherPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} 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 3d22a81d20ce..539a5520e5be 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,192 @@ +## 2.2.5 + +* Support to closed caption WebVTT format added. + +## 2.2.4 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. + +## 2.2.3 + +* Fixed empty caption text still showing the caption widget. + +## 2.2.2 + +* Fix a disposed `VideoPlayerController` throwing an exception when being replaced in the `VideoPlayer`. + +## 2.2.1 + +* Specify Java 8 for Android build. + +## 2.2.0 + +* Add `contentUri` based VideoPlayerController. + +## 2.1.15 + +* Ensured seekTo isn't called before video player is initialized. Fixes [#89259](https://github.com/flutter/flutter/issues/89259). +* Updated Android lint settings. + +## 2.1.14 + +* Removed dependency on the `flutter_test` package. + +## 2.1.13 + +* Removed obsolete warning about not working in iOS simulators from README. + +## 2.1.12 + +* Update the video url in the readme code sample + +## 2.1.11 + +* Remove references to the Android V1 embedding. + +## 2.1.10 + +* Ensure video pauses correctly when it finishes. + +## 2.1.9 + +* Silenced warnings that may occur during build when using a very + recent version of Flutter relating to null safety. + +## 2.1.8 + +* Refactor `FLTCMTimeToMillis` to support indefinite streams. Fixes [#48670](https://github.com/flutter/flutter/issues/48670). + +## 2.1.7 + +* Update exoplayer to 2.14.1, removing dependency on Bintray. + +## 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. +* **Minor breaking change**: fixed `VideoPlayerValue.toString` to insert a comma after `isBuffering`. + +## 0.10.12+5 + +* Depend on `video_player_platform_interface` version that contains the new `TestHostVideoPlayerApi` + in order for tests to pass using the latest dependency. + +## 0.10.12+4 + +* Keep handling deprecated Android v1 classes for backward compatibility. + +## 0.10.12+3 + +* Avoiding uses or overrides a deprecated API in `VideoPlayerPlugin` class. + +## 0.10.12+2 + +* Fix `setMixWithOthers` test. + +## 0.10.12+1 + +* Depend on the version of `video_player_platform_interface` that contains the new `VideoPlayerOptions` class. + +## 0.10.12 + +* Introduce VideoPlayerOptions to set the audio mix mode. + +## 0.10.11+2 + +* Fix aspectRatio calculation when size.width or size.height are zero. + +## 0.10.11+1 + +* Post-v2 Android embedding cleanups. + ## 0.10.11 * iOS: Fixed crash when detaching from a dying engine. @@ -37,7 +226,7 @@ ## 0.10.7 -* `VideoPlayerController` support for reading closed caption files. +* `VideoPlayerController` support for reading closed caption files. * `VideoPlayerValue` has a `caption` field for reading the current closed caption at any given time. ## 0.10.6 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 c89293372cf3..c6823b81eb84 100644 --- a/packages/video_player/video_player/LICENSE +++ b/packages/video_player/video_player/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/video_player/video_player/README.md b/packages/video_player/video_player/README.md index ef522a455c21..d5e7528fa973 100644 --- a/packages/video_player/video_player/README.md +++ b/packages/video_player/video_player/README.md @@ -1,31 +1,18 @@ # 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 -Warning: The video player is not functional on iOS simulators. An iOS device must be used during development/testing. - -Add the following entry to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: +This plugin requires iOS 9.0 or higher. Add the following entry to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: ```xml NSAppTransportSecurity @@ -55,6 +42,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 +73,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://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4') ..initialize().then((_) { // Ensure the first frame is shown after the video is initialized, even before the play button has been pressed. setState(() {}); @@ -97,7 +86,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), @@ -127,3 +116,22 @@ class _VideoAppState extends State { } } ``` + +## Usage + +The following section contains usage information that goes beyond what is included in the +documentation in order to give a more elaborate overview of the API. + +This is not complete as of now. You can contribute to this section by [opening a pull request](https://github.com/flutter/plugins/pulls). + +### Playback speed + +You can set the playback speed on your `_controller` (instance of `VideoPlayerController`) by +calling `_controller.setPlaybackSpeed`. `setPlaybackSpeed` takes a `double` speed value indicating +the rate of playback for your video. +For example, when given a value of `2.0`, your video will play at 2x the regular playback speed +and so on. + +To learn about playback speed limitations, see the [`setPlaybackSpeed` method documentation](https://pub.dev/documentation/video_player/latest/video_player/VideoPlayerController/setPlaybackSpeed.html). + +Furthermore, see the example app for an example playback speed implementation. diff --git a/packages/video_player/video_player/android/build.gradle b/packages/video_player/video_player/android/build.gradle index edbb4c7acce4..5d6b737f47a5 100644 --- a/packages/video_player/video_player/android/build.gradle +++ b/packages/video_player/video_player/android/build.gradle @@ -1,28 +1,33 @@ group 'io.flutter.plugins.videoplayer' 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 @@ -30,18 +35,32 @@ android { } lintOptions { disable 'InvalidPackage' + disable 'GradleDependency' } - android { - compileOptions { - sourceCompatibility 1.8 - targetCompatibility 1.8 - } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 } 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.14.1' + implementation 'com.google.android.exoplayer:exoplayer-hls:2.14.1' + implementation 'com.google.android.exoplayer:exoplayer-dash:2.14.1' + implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.14.1' + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-inline:3.9.0' + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } } } diff --git a/packages/video_player/video_player/android/gradle.properties b/packages/video_player/video_player/android/gradle.properties deleted file mode 100644 index 8bd86f680510..000000000000 --- a/packages/video_player/video_player/android/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M 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 7bba51b21f98..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.0-experimental.11), 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; @@ -9,6 +13,7 @@ import java.util.HashMap; /** Generated class from Pigeon. */ +@SuppressWarnings("unused") public class Messages { /** Generated class from Pigeon that represents data sent in messages. */ @@ -24,17 +29,18 @@ public void setTextureId(Long setterArg) { } HashMap toMap() { - HashMap toMapResult = new HashMap(); + HashMap toMapResult = new HashMap<>(); toMapResult.put("textureId", textureId); return toMapResult; } static TextureMessage fromMap(HashMap map) { TextureMessage fromMapResult = new TextureMessage(); + Object textureId = map.get("textureId"); fromMapResult.textureId = - (map.get("textureId") instanceof Integer) - ? (Integer) map.get("textureId") - : (Long) map.get("textureId"); + (textureId == null) + ? null + : ((textureId instanceof Integer) ? (Integer) textureId : (Long) textureId); return fromMapResult; } } @@ -81,21 +87,38 @@ 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(); + HashMap toMapResult = new HashMap<>(); toMapResult.put("asset", asset); toMapResult.put("uri", uri); toMapResult.put("packageName", packageName); toMapResult.put("formatHint", formatHint); + toMapResult.put("httpHeaders", httpHeaders); return toMapResult; } static CreateMessage fromMap(HashMap map) { CreateMessage fromMapResult = new CreateMessage(); - fromMapResult.asset = (String) map.get("asset"); - fromMapResult.uri = (String) map.get("uri"); - fromMapResult.packageName = (String) map.get("packageName"); - fromMapResult.formatHint = (String) map.get("formatHint"); + Object asset = map.get("asset"); + fromMapResult.asset = (String) asset; + Object uri = map.get("uri"); + fromMapResult.uri = (String) uri; + Object packageName = map.get("packageName"); + fromMapResult.packageName = (String) packageName; + Object formatHint = map.get("formatHint"); + fromMapResult.formatHint = (String) formatHint; + Object httpHeaders = map.get("httpHeaders"); + fromMapResult.httpHeaders = (HashMap) httpHeaders; return fromMapResult; } } @@ -123,7 +146,7 @@ public void setIsLooping(Boolean setterArg) { } HashMap toMap() { - HashMap toMapResult = new HashMap(); + HashMap toMapResult = new HashMap<>(); toMapResult.put("textureId", textureId); toMapResult.put("isLooping", isLooping); return toMapResult; @@ -131,11 +154,13 @@ HashMap toMap() { static LoopingMessage fromMap(HashMap map) { LoopingMessage fromMapResult = new LoopingMessage(); + Object textureId = map.get("textureId"); fromMapResult.textureId = - (map.get("textureId") instanceof Integer) - ? (Integer) map.get("textureId") - : (Long) map.get("textureId"); - fromMapResult.isLooping = (Boolean) map.get("isLooping"); + (textureId == null) + ? null + : ((textureId instanceof Integer) ? (Integer) textureId : (Long) textureId); + Object isLooping = map.get("isLooping"); + fromMapResult.isLooping = (Boolean) isLooping; return fromMapResult; } } @@ -163,7 +188,7 @@ public void setVolume(Double setterArg) { } HashMap toMap() { - HashMap toMapResult = new HashMap(); + HashMap toMapResult = new HashMap<>(); toMapResult.put("textureId", textureId); toMapResult.put("volume", volume); return toMapResult; @@ -171,11 +196,55 @@ HashMap toMap() { static VolumeMessage fromMap(HashMap map) { VolumeMessage fromMapResult = new VolumeMessage(); + Object textureId = map.get("textureId"); + fromMapResult.textureId = + (textureId == null) + ? null + : ((textureId instanceof Integer) ? (Integer) textureId : (Long) textureId); + Object volume = map.get("volume"); + fromMapResult.volume = (Double) volume; + return fromMapResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class PlaybackSpeedMessage { + private Long textureId; + + public Long getTextureId() { + return textureId; + } + + public void setTextureId(Long setterArg) { + this.textureId = setterArg; + } + + private Double speed; + + public Double getSpeed() { + return speed; + } + + public void setSpeed(Double setterArg) { + this.speed = setterArg; + } + + HashMap toMap() { + HashMap toMapResult = new HashMap<>(); + toMapResult.put("textureId", textureId); + toMapResult.put("speed", speed); + return toMapResult; + } + + static PlaybackSpeedMessage fromMap(HashMap map) { + PlaybackSpeedMessage fromMapResult = new PlaybackSpeedMessage(); + Object textureId = map.get("textureId"); fromMapResult.textureId = - (map.get("textureId") instanceof Integer) - ? (Integer) map.get("textureId") - : (Long) map.get("textureId"); - fromMapResult.volume = (Double) map.get("volume"); + (textureId == null) + ? null + : ((textureId instanceof Integer) ? (Integer) textureId : (Long) textureId); + Object speed = map.get("speed"); + fromMapResult.speed = (Double) speed; return fromMapResult; } } @@ -203,7 +272,7 @@ public void setPosition(Long setterArg) { } HashMap toMap() { - HashMap toMapResult = new HashMap(); + HashMap toMapResult = new HashMap<>(); toMapResult.put("textureId", textureId); toMapResult.put("position", position); return toMapResult; @@ -211,14 +280,42 @@ HashMap toMap() { static PositionMessage fromMap(HashMap map) { PositionMessage fromMapResult = new PositionMessage(); + Object textureId = map.get("textureId"); fromMapResult.textureId = - (map.get("textureId") instanceof Integer) - ? (Integer) map.get("textureId") - : (Long) map.get("textureId"); + (textureId == null) + ? null + : ((textureId instanceof Integer) ? (Integer) textureId : (Long) textureId); + Object position = map.get("position"); fromMapResult.position = - (map.get("position") instanceof Integer) - ? (Integer) map.get("position") - : (Long) map.get("position"); + (position == null) + ? null + : ((position instanceof Integer) ? (Integer) position : (Long) position); + return fromMapResult; + } + } + + /** Generated class from Pigeon that represents data sent in messages. */ + public static class MixWithOthersMessage { + private Boolean mixWithOthers; + + public Boolean getMixWithOthers() { + return mixWithOthers; + } + + public void setMixWithOthers(Boolean setterArg) { + this.mixWithOthers = setterArg; + } + + HashMap toMap() { + HashMap toMapResult = new HashMap<>(); + toMapResult.put("mixWithOthers", mixWithOthers); + return toMapResult; + } + + static MixWithOthersMessage fromMap(HashMap map) { + MixWithOthersMessage fromMapResult = new MixWithOthersMessage(); + Object mixWithOthers = map.get("mixWithOthers"); + fromMapResult.mixWithOthers = (Boolean) mixWithOthers; return fromMapResult; } } @@ -235,6 +332,8 @@ public interface VideoPlayerApi { void setVolume(VolumeMessage arg); + void setPlaybackSpeed(PlaybackSpeedMessage arg); + void play(TextureMessage arg); PositionMessage position(TextureMessage arg); @@ -243,27 +342,27 @@ public interface VideoPlayerApi { void pause(TextureMessage arg); + void setMixWithOthers(MixWithOthersMessage arg); + /** Sets up an instance of `VideoPlayerApi` to handle messages through the `binaryMessenger` */ - public static void setup(BinaryMessenger binaryMessenger, VideoPlayerApi api) { + static void setup(BinaryMessenger binaryMessenger, VideoPlayerApi api) { { BasicMessageChannel channel = - new BasicMessageChannel( + new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.initialize", new StandardMessageCodec()); if (api != null) { channel.setMessageHandler( - new BasicMessageChannel.MessageHandler() { - public void onMessage(Object message, BasicMessageChannel.Reply reply) { - HashMap wrapped = new HashMap(); - try { - api.initialize(); - wrapped.put("result", null); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); + (message, reply) -> { + HashMap wrapped = new HashMap<>(); + try { + api.initialize(); + wrapped.put("result", null); + } catch (Exception exception) { + wrapped.put("error", wrapError(exception)); } + reply.reply(wrapped); }); } else { channel.setMessageHandler(null); @@ -271,24 +370,23 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { } { BasicMessageChannel channel = - new BasicMessageChannel( + new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.create", new StandardMessageCodec()); if (api != null) { channel.setMessageHandler( - new BasicMessageChannel.MessageHandler() { - public void onMessage(Object message, BasicMessageChannel.Reply reply) { + (message, reply) -> { + HashMap wrapped = new HashMap<>(); + try { + @SuppressWarnings("ConstantConditions") CreateMessage input = CreateMessage.fromMap((HashMap) message); - HashMap wrapped = new HashMap(); - try { - TextureMessage output = api.create(input); - wrapped.put("result", output.toMap()); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); + TextureMessage output = api.create(input); + wrapped.put("result", output.toMap()); + } catch (Exception exception) { + wrapped.put("error", wrapError(exception)); } + reply.reply(wrapped); }); } else { channel.setMessageHandler(null); @@ -296,24 +394,23 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { } { BasicMessageChannel channel = - new BasicMessageChannel( + new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.dispose", new StandardMessageCodec()); if (api != null) { channel.setMessageHandler( - new BasicMessageChannel.MessageHandler() { - public void onMessage(Object message, BasicMessageChannel.Reply reply) { + (message, reply) -> { + HashMap wrapped = new HashMap<>(); + try { + @SuppressWarnings("ConstantConditions") TextureMessage input = TextureMessage.fromMap((HashMap) message); - HashMap wrapped = new HashMap(); - try { - api.dispose(input); - wrapped.put("result", null); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); + api.dispose(input); + wrapped.put("result", null); + } catch (Exception exception) { + wrapped.put("error", wrapError(exception)); } + reply.reply(wrapped); }); } else { channel.setMessageHandler(null); @@ -321,24 +418,23 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { } { BasicMessageChannel channel = - new BasicMessageChannel( + new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.setLooping", new StandardMessageCodec()); if (api != null) { channel.setMessageHandler( - new BasicMessageChannel.MessageHandler() { - public void onMessage(Object message, BasicMessageChannel.Reply reply) { + (message, reply) -> { + HashMap wrapped = new HashMap<>(); + try { + @SuppressWarnings("ConstantConditions") LoopingMessage input = LoopingMessage.fromMap((HashMap) message); - HashMap wrapped = new HashMap(); - try { - api.setLooping(input); - wrapped.put("result", null); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); + api.setLooping(input); + wrapped.put("result", null); + } catch (Exception exception) { + wrapped.put("error", wrapError(exception)); } + reply.reply(wrapped); }); } else { channel.setMessageHandler(null); @@ -346,24 +442,47 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { } { BasicMessageChannel channel = - new BasicMessageChannel( + new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.setVolume", new StandardMessageCodec()); if (api != null) { channel.setMessageHandler( - new BasicMessageChannel.MessageHandler() { - public void onMessage(Object message, BasicMessageChannel.Reply reply) { + (message, reply) -> { + HashMap wrapped = new HashMap<>(); + try { + @SuppressWarnings("ConstantConditions") VolumeMessage input = VolumeMessage.fromMap((HashMap) message); - HashMap wrapped = new HashMap(); - try { - api.setVolume(input); - wrapped.put("result", null); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); + api.setVolume(input); + wrapped.put("result", null); + } catch (Exception exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed", + new StandardMessageCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + HashMap wrapped = new HashMap<>(); + try { + @SuppressWarnings("ConstantConditions") + PlaybackSpeedMessage input = PlaybackSpeedMessage.fromMap((HashMap) message); + api.setPlaybackSpeed(input); + wrapped.put("result", null); + } catch (Exception exception) { + wrapped.put("error", wrapError(exception)); } + reply.reply(wrapped); }); } else { channel.setMessageHandler(null); @@ -371,24 +490,23 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { } { BasicMessageChannel channel = - new BasicMessageChannel( + new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.play", new StandardMessageCodec()); if (api != null) { channel.setMessageHandler( - new BasicMessageChannel.MessageHandler() { - public void onMessage(Object message, BasicMessageChannel.Reply reply) { + (message, reply) -> { + HashMap wrapped = new HashMap<>(); + try { + @SuppressWarnings("ConstantConditions") TextureMessage input = TextureMessage.fromMap((HashMap) message); - HashMap wrapped = new HashMap(); - try { - api.play(input); - wrapped.put("result", null); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); + api.play(input); + wrapped.put("result", null); + } catch (Exception exception) { + wrapped.put("error", wrapError(exception)); } + reply.reply(wrapped); }); } else { channel.setMessageHandler(null); @@ -396,24 +514,23 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { } { BasicMessageChannel channel = - new BasicMessageChannel( + new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.position", new StandardMessageCodec()); if (api != null) { channel.setMessageHandler( - new BasicMessageChannel.MessageHandler() { - public void onMessage(Object message, BasicMessageChannel.Reply reply) { + (message, reply) -> { + HashMap wrapped = new HashMap<>(); + try { + @SuppressWarnings("ConstantConditions") TextureMessage input = TextureMessage.fromMap((HashMap) message); - HashMap wrapped = new HashMap(); - try { - PositionMessage output = api.position(input); - wrapped.put("result", output.toMap()); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); + PositionMessage output = api.position(input); + wrapped.put("result", output.toMap()); + } catch (Exception exception) { + wrapped.put("error", wrapError(exception)); } + reply.reply(wrapped); }); } else { channel.setMessageHandler(null); @@ -421,24 +538,23 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { } { BasicMessageChannel channel = - new BasicMessageChannel( + new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.seekTo", new StandardMessageCodec()); if (api != null) { channel.setMessageHandler( - new BasicMessageChannel.MessageHandler() { - public void onMessage(Object message, BasicMessageChannel.Reply reply) { + (message, reply) -> { + HashMap wrapped = new HashMap<>(); + try { + @SuppressWarnings("ConstantConditions") PositionMessage input = PositionMessage.fromMap((HashMap) message); - HashMap wrapped = new HashMap(); - try { - api.seekTo(input); - wrapped.put("result", null); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); + api.seekTo(input); + wrapped.put("result", null); + } catch (Exception exception) { + wrapped.put("error", wrapError(exception)); } + reply.reply(wrapped); }); } else { channel.setMessageHandler(null); @@ -446,24 +562,47 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { } { BasicMessageChannel channel = - new BasicMessageChannel( + new BasicMessageChannel<>( binaryMessenger, "dev.flutter.pigeon.VideoPlayerApi.pause", new StandardMessageCodec()); if (api != null) { channel.setMessageHandler( - new BasicMessageChannel.MessageHandler() { - public void onMessage(Object message, BasicMessageChannel.Reply reply) { + (message, reply) -> { + HashMap wrapped = new HashMap<>(); + try { + @SuppressWarnings("ConstantConditions") TextureMessage input = TextureMessage.fromMap((HashMap) message); - HashMap wrapped = new HashMap(); - try { - api.pause(input); - wrapped.put("result", null); - } catch (Exception exception) { - wrapped.put("error", wrapError(exception)); - } - reply.reply(wrapped); + api.pause(input); + wrapped.put("result", null); + } catch (Exception exception) { + wrapped.put("error", wrapError(exception)); + } + reply.reply(wrapped); + }); + } else { + channel.setMessageHandler(null); + } + } + { + BasicMessageChannel channel = + new BasicMessageChannel<>( + binaryMessenger, + "dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers", + new StandardMessageCodec()); + if (api != null) { + channel.setMessageHandler( + (message, reply) -> { + HashMap wrapped = new HashMap<>(); + try { + @SuppressWarnings("ConstantConditions") + MixWithOthersMessage input = MixWithOthersMessage.fromMap((HashMap) message); + api.setMixWithOthers(input); + wrapped.put("result", null); + } catch (Exception exception) { + wrapped.put("error", wrapError(exception)); } + reply.reply(wrapped); }); } else { channel.setMessageHandler(null); @@ -473,9 +612,9 @@ public void onMessage(Object message, BasicMessageChannel.Reply reply) { } private static HashMap wrapError(Exception exception) { - HashMap errorMap = new HashMap(); + 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 9db281d38b86..887d3d15f175 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,30 +9,26 @@ 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.Player.Listener; 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; -import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.util.Util; import io.flutter.plugin.common.EventChannel; import io.flutter.view.TextureRegistry; @@ -56,35 +56,42 @@ final class VideoPlayer { private boolean isInitialized = false; + private final VideoPlayerOptions options; + VideoPlayer( Context context, EventChannel eventChannel, TextureRegistry.SurfaceTextureEntry textureEntry, String dataSource, - String formatHint) { + 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 = - new DefaultHttpDataSourceFactory( - "ExoPlayer", - null, - DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, - DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, - true); + DefaultHttpDataSource.Factory httpDataSourceFactory = + new DefaultHttpDataSource.Factory() + .setUserAgent("ExoPlayer") + .setAllowCrossProtocolRedirects(true); + + if (httpHeaders != null && !httpHeaders.isEmpty()) { + httpDataSourceFactory.setDefaultRequestProperties(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); } @@ -126,18 +133,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); @@ -147,7 +154,6 @@ private MediaSource buildMediaSource( private void setupVideoPlayer( EventChannel eventChannel, TextureRegistry.SurfaceTextureEntry textureEntry) { - eventChannel.setStreamHandler( new EventChannel.StreamHandler() { @Override @@ -163,14 +169,25 @@ public void onCancel(Object o) { surface = new Surface(textureEntry.surfaceTexture()); exoPlayer.setVideoSurface(surface); - setAudioAttributes(exoPlayer); + setAudioAttributes(exoPlayer, options.mixWithOthers); exoPlayer.addListener( - new EventListener() { + new Listener() { + 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) { @@ -182,10 +199,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); } @@ -203,13 +225,9 @@ void sendBufferingUpdate() { } @SuppressWarnings("deprecation") - private static void setAudioAttributes(SimpleExoPlayer exoPlayer) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - exoPlayer.setAudioAttributes( - new AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MOVIE).build()); - } else { - exoPlayer.setAudioStreamType(C.STREAM_TYPE_MUSIC); - } + private static void setAudioAttributes(SimpleExoPlayer exoPlayer, boolean isMixMode) { + exoPlayer.setAudioAttributes( + new AudioAttributes.Builder().setContentType(C.CONTENT_TYPE_MOVIE).build(), !isMixMode); } void play() { @@ -229,6 +247,14 @@ void setVolume(double value) { exoPlayer.setVolume(bracketedValue); } + void setPlaybackSpeed(double value) { + // We do not need to consider pitch and skipSilence for now as we do not handle them and + // therefore never diverge from the default values. + final PlaybackParameters playbackParameters = new PlaybackParameters(((float) value)); + + exoPlayer.setPlaybackParameters(playbackParameters); + } + void seekTo(int location) { exoPlayer.seekTo(location); } 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 new file mode 100644 index 000000000000..85ad892f9e19 --- /dev/null +++ b/packages/video_player/video_player/android/src/main/java/io/flutter/plugins/videoplayer/VideoPlayerOptions.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.videoplayer; + +class VideoPlayerOptions { + public boolean mixWithOthers; +} 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 3ec40e5fa2c4..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,35 +1,43 @@ -// 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.FlutterInjector; +import io.flutter.Log; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.EventChannel; -import io.flutter.plugin.common.PluginRegistry.Registrar; import io.flutter.plugins.videoplayer.Messages.CreateMessage; import io.flutter.plugins.videoplayer.Messages.LoopingMessage; +import io.flutter.plugins.videoplayer.Messages.MixWithOthersMessage; +import io.flutter.plugins.videoplayer.Messages.PlaybackSpeedMessage; import io.flutter.plugins.videoplayer.Messages.PositionMessage; import io.flutter.plugins.videoplayer.Messages.TextureMessage; import io.flutter.plugins.videoplayer.Messages.VideoPlayerApi; import io.flutter.plugins.videoplayer.Messages.VolumeMessage; -import io.flutter.view.FlutterMain; 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 { private static final String TAG = "VideoPlayerPlugin"; private final LongSparseArray videoPlayers = new LongSparseArray<>(); private FlutterState flutterState; + private VideoPlayerOptions options = new VideoPlayerOptions(); /** Register this with the v2 embedding for the plugin to respond to lifecycle callbacks. */ public VideoPlayerPlugin() {} - private VideoPlayerPlugin(Registrar registrar) { + @SuppressWarnings("deprecation") + private VideoPlayerPlugin(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { this.flutterState = new FlutterState( registrar.context(), @@ -41,7 +49,8 @@ private VideoPlayerPlugin(Registrar registrar) { } /** Registers this with the stable v1 embedding. Will not respond to lifecycle events. */ - public static void registerWith(Registrar registrar) { + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { final VideoPlayerPlugin plugin = new VideoPlayerPlugin(registrar); registrar.addViewDestroyListener( view -> { @@ -52,13 +61,28 @@ public static void registerWith(Registrar registrar) { @Override public void onAttachedToEngine(FlutterPluginBinding binding) { + + 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(), - FlutterMain::getLookupKeyForAsset, - FlutterMain::getLookupKeyForAsset, - binding.getFlutterEngine().getRenderer()); + injector.flutterLoader()::getLookupKeyForAsset, + injector.flutterLoader()::getLookupKeyForAsset, + binding.getTextureRegistry()); flutterState.startListening(this, binding.getBinaryMessenger()); } @@ -69,6 +93,7 @@ public void onDetachedFromEngine(FlutterPluginBinding binding) { } flutterState.stopListening(binding.getBinaryMessenger()); flutterState = null; + initialize(); } private void disposeAllPlayers() { @@ -113,18 +138,23 @@ public TextureMessage create(CreateMessage arg) { eventChannel, handle, "asset:///" + assetLookupKey, - null); - videoPlayers.put(handle.id(), player); + null, + null, + options); } else { + @SuppressWarnings("unchecked") + Map httpHeaders = arg.getHttpHeaders(); player = new VideoPlayer( flutterState.applicationContext, eventChannel, handle, arg.getUri(), - arg.getFormatHint()); - videoPlayers.put(handle.id(), player); + arg.getFormatHint(), + httpHeaders, + options); } + videoPlayers.put(handle.id(), player); TextureMessage result = new TextureMessage(); result.setTextureId(handle.id()); @@ -147,6 +177,11 @@ public void setVolume(VolumeMessage arg) { player.setVolume(arg.getVolume()); } + public void setPlaybackSpeed(PlaybackSpeedMessage arg) { + VideoPlayer player = videoPlayers.get(arg.getTextureId()); + player.setPlaybackSpeed(arg.getSpeed()); + } + public void play(TextureMessage arg) { VideoPlayer player = videoPlayers.get(arg.getTextureId()); player.play(); @@ -170,6 +205,11 @@ public void pause(TextureMessage arg) { player.pause(); } + @Override + public void setMixWithOthers(MixWithOthersMessage arg) { + options.mixWithOthers = arg.getMixWithOthers(); + } + private interface KeyForAssetFn { String get(String asset); } diff --git a/packages/video_player/video_player/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java b/packages/video_player/video_player/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java new file mode 100644 index 000000000000..ec960b7a4480 --- /dev/null +++ b/packages/video_player/video_player/android/src/test/java/io/flutter/plugins/videoplayer/VideoPlayerTest.java @@ -0,0 +1,15 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.videoplayer; + +import org.junit.Test; + +public class VideoPlayerTest { + // This is only a placeholder test and doesn't actually initialize the plugin. + @Test + public void initPluginDoesNotThrow() { + final VideoPlayerPlugin plugin = new VideoPlayerPlugin(); + } +} diff --git a/packages/video_player/video_player/example/README.md b/packages/video_player/video_player/example/README.md index 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..0d1d5031ef4f 100644 --- a/packages/video_player/video_player/example/android/app/build.gradle +++ b/packages/video_player/video_player/example/android/app/build.gradle @@ -25,28 +25,25 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 28 + compileSdkVersion 29 lintOptions { disable 'InvalidPackage' } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } defaultConfig { applicationId "io.flutter.plugins.videoplayerexample" minSdkVersion 16 - targetSdkVersion 28 + targetSdkVersion 29 versionCode flutterVersionCode.toInteger() versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } - android { - compileOptions { - sourceCompatibility 1.8 - targetCompatibility 1.8 - } - } - buildTypes { release { // TODO: Add your own signing config for the release build. @@ -64,4 +61,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/AndroidManifest.xml b/packages/video_player/video_player/example/android/app/src/main/AndroidManifest.xml index deec4b6b5b08..a2574c90d7d9 100644 --- a/packages/video_player/video_player/example/android/app/src/main/AndroidManifest.xml +++ b/packages/video_player/video_player/example/android/app/src/main/AndroidManifest.xml @@ -4,24 +4,11 @@ - - - @@ -29,6 +16,7 @@ + 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 deleted file mode 100644 index f1af8ecd74e7..000000000000 --- a/packages/video_player/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/EmbeddingV1Activity.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.videoplayerexample; - -import android.os.Bundle; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.GeneratedPluginRegistrant; - -public class EmbeddingV1Activity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - GeneratedPluginRegistrant.registerWith(this); - } -} diff --git a/packages/video_player/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/MainActivity.java b/packages/video_player/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/MainActivity.java deleted file mode 100644 index 2a0ae15e5e2f..000000000000 --- a/packages/video_player/video_player/example/android/app/src/main/java/io/flutter/plugins/videoplayerexample/MainActivity.java +++ /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. - -package io.flutter.plugins.videoplayerexample; - -import io.flutter.embedding.android.FlutterActivity; -import io.flutter.embedding.engine.FlutterEngine; -import io.flutter.plugins.videoplayer.VideoPlayerPlugin; - -public class MainActivity extends FlutterActivity { - @Override - public void configureFlutterEngine(FlutterEngine flutterEngine) { - flutterEngine.getPlugins().add(new VideoPlayerPlugin()); - } -} 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/assets/bumble_bee_captions.vtt b/packages/video_player/video_player/example/assets/bumble_bee_captions.vtt new file mode 100644 index 000000000000..1dca2c58695e --- /dev/null +++ b/packages/video_player/video_player/example/assets/bumble_bee_captions.vtt @@ -0,0 +1,7 @@ +WEBVTT + +00:00:00.200 --> 00:00:01.750 +[ Birds chirping ] + +00:00:02.300 --> 00:00:05.000 +[ Buzzing ] diff --git a/packages/video_player/video_player/example/integration_test/controller_swap_test.dart b/packages/video_player/video_player/example/integration_test/controller_swap_test.dart new file mode 100644 index 000000000000..cae51767f4aa --- /dev/null +++ b/packages/video_player/video_player/example/integration_test/controller_swap_test.dart @@ -0,0 +1,92 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart: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'; + +const Duration _playDuration = Duration(seconds: 1); + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + testWidgets( + 'can substitute one controller by another without crashing', + (WidgetTester tester) async { + VideoPlayerController controller = VideoPlayerController.network( + 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', + ); + VideoPlayerController another = VideoPlayerController.network( + 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', + ); + await controller.initialize(); + await another.initialize(); + await controller.setVolume(0); + await another.setVolume(0); + + final Completer started = Completer(); + final Completer ended = Completer(); + bool startedBuffering = false; + bool endedBuffering = false; + + another.addListener(() { + if (another.value.isBuffering && !startedBuffering) { + startedBuffering = true; + started.complete(); + } + if (startedBuffering && !another.value.isBuffering && !endedBuffering) { + endedBuffering = true; + ended.complete(); + } + }); + + // Inject a widget with `controller`... + await tester.pumpWidget(renderVideoWidget(controller)); + await controller.play(); + await tester.pumpAndSettle(_playDuration); + await controller.pause(); + + // Disposing controller causes the Widget to crash in the next line + // (Issue https://github.com/flutter/flutter/issues/90046) + await controller.dispose(); + + // Now replace it with `another` controller... + await tester.pumpWidget(renderVideoWidget(another)); + await another.play(); + await another.seekTo(const Duration(seconds: 5)); + await tester.pumpAndSettle(_playDuration); + await another.pause(); + + // Expect that `another` played. + expect(another.value.position, + (Duration position) => position > const Duration(seconds: 0)); + + await started; + expect(startedBuffering, true); + + await ended; + expect(endedBuffering, true); + }, + skip: !(kIsWeb || defaultTargetPlatform == TargetPlatform.android), + ); +} + +Widget renderVideoWidget(VideoPlayerController controller) { + return Material( + elevation: 0, + child: Directionality( + textDirection: TextDirection.ltr, + child: Center( + child: AspectRatio( + key: Key('same'), + aspectRatio: controller.value.aspectRatio, + child: VideoPlayer(controller), + ), + ), + ), + ); +} diff --git a/packages/video_player/video_player/example/integration_test/video_player_test.dart b/packages/video_player/video_player/example/integration_test/video_player_test.dart new file mode 100644 index 000000000000..373538ad365e --- /dev/null +++ b/packages/video_player/video_player/example/integration_test/video_player_test.dart @@ -0,0 +1,228 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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/material.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:video_player/video_player.dart'; + +const Duration _playDuration = Duration(seconds: 1); + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + late VideoPlayerController _controller; + tearDown(() async => _controller.dispose()); + + group('asset videos', () { + setUp(() { + _controller = VideoPlayerController.asset('assets/Butterfly-209.mp4'); + }); + + testWidgets('can be initialized', (WidgetTester tester) async { + await _controller.initialize(); + + expect(_controller.value.isInitialized, true); + expect(_controller.value.position, const Duration(seconds: 0)); + expect(_controller.value.isPlaying, false); + expect(_controller.value.duration, + const Duration(seconds: 7, milliseconds: 540)); + }); + + testWidgets( + 'reports buffering status', + (WidgetTester tester) async { + VideoPlayerController networkController = VideoPlayerController.network( + 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', + ); + await networkController.initialize(); + // Mute to allow playing without DOM interaction on Web. + // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes + await networkController.setVolume(0); + final Completer started = Completer(); + final Completer ended = Completer(); + bool startedBuffering = false; + bool endedBuffering = false; + networkController.addListener(() { + if (networkController.value.isBuffering && !startedBuffering) { + startedBuffering = true; + started.complete(); + } + if (startedBuffering && + !networkController.value.isBuffering && + !endedBuffering) { + endedBuffering = true; + ended.complete(); + } + }); + + await networkController.play(); + await networkController.seekTo(const Duration(seconds: 5)); + await tester.pumpAndSettle(_playDuration); + await networkController.pause(); + + expect(networkController.value.isPlaying, false); + expect(networkController.value.position, + (Duration position) => position > const Duration(seconds: 0)); + + await started; + expect(startedBuffering, true); + + await ended; + expect(endedBuffering, true); + }, + skip: !(kIsWeb || defaultTargetPlatform == TargetPlatform.android), + ); + + testWidgets( + 'live stream duration != 0', + (WidgetTester tester) async { + VideoPlayerController networkController = VideoPlayerController.network( + 'https://cph-p2p-msl.akamaized.net/hls/live/2000341/test/master.m3u8', + ); + await networkController.initialize(); + + expect(networkController.value.isInitialized, true); + // Live streams should have either a positive duration or C.TIME_UNSET if the duration is unknown + // See https://exoplayer.dev/doc/reference/com/google/android/exoplayer2/Player.html#getDuration-- + expect(networkController.value.duration, + (Duration duration) => duration != Duration.zero); + }, + skip: (kIsWeb), + ); + + testWidgets( + 'can be played', + (WidgetTester tester) async { + await _controller.initialize(); + // Mute to allow playing without DOM interaction on Web. + // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes + await _controller.setVolume(0); + + await _controller.play(); + await tester.pumpAndSettle(_playDuration); + + expect(_controller.value.isPlaying, true); + expect(_controller.value.position, + (Duration position) => position > const Duration(seconds: 0)); + }, + ); + + testWidgets( + 'can seek', + (WidgetTester tester) async { + await _controller.initialize(); + + await _controller.seekTo(const Duration(seconds: 3)); + + expect(_controller.value.position, const Duration(seconds: 3)); + }, + ); + + testWidgets( + 'can be paused', + (WidgetTester tester) async { + await _controller.initialize(); + // Mute to allow playing without DOM interaction on Web. + // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes + await _controller.setVolume(0); + + // Play for a second, then pause, and then wait a second. + await _controller.play(); + await tester.pumpAndSettle(_playDuration); + await _controller.pause(); + final Duration pausedPosition = _controller.value.position; + await tester.pumpAndSettle(_playDuration); + + // Verify that we stopped playing after the pause. + expect(_controller.value.isPlaying, false); + expect(_controller.value.position, pausedPosition); + }, + ); + + testWidgets( + 'stay paused when seeking after video completed', + (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); + Duration tenMillisBeforeEnd = + _controller.value.duration - const Duration(milliseconds: 10); + await _controller.seekTo(tenMillisBeforeEnd); + await _controller.play(); + await tester.pumpAndSettle(_playDuration); + expect(_controller.value.isPlaying, false); + expect(_controller.value.position, _controller.value.duration); + + await _controller.seekTo(tenMillisBeforeEnd); + await tester.pumpAndSettle(_playDuration); + + expect(_controller.value.isPlaying, false); + expect(_controller.value.position, tenMillisBeforeEnd); + }, + ); + + testWidgets( + 'do not exceed duration on play after video completed', + (WidgetTester tester) async { + await _controller.initialize(); + // Mute to allow playing without DOM interaction on Web. + // See https://developers.google.com/web/updates/2017/09/autoplay-policy-changes + await _controller.setVolume(0); + await _controller.seekTo( + _controller.value.duration - const Duration(milliseconds: 10)); + await _controller.play(); + await tester.pumpAndSettle(_playDuration); + expect(_controller.value.isPlaying, false); + expect(_controller.value.position, _controller.value.duration); + + await _controller.play(); + await tester.pumpAndSettle(_playDuration); + + expect(_controller.value.position, + lessThanOrEqualTo(_controller.value.duration)); + }, + ); + + testWidgets('test video player view with local asset', + (WidgetTester tester) async { + Future started() async { + await _controller.initialize(); + await _controller.play(); + 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. + // Extremely flaky on iOS: https://github.com/flutter/flutter/issues/86915 + defaultTargetPlatform == TargetPlatform.iOS); + }); +} diff --git a/packages/video_player/video_player/example/ios/Flutter/AppFrameworkInfo.plist b/packages/video_player/video_player/example/ios/Flutter/AppFrameworkInfo.plist index 6c2de8086bcd..3a9c234f96d4 100644 --- a/packages/video_player/video_player/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/video_player/video_player/example/ios/Flutter/AppFrameworkInfo.plist @@ -25,6 +25,6 @@ arm64 MinimumOSVersion - 8.0 + 9.0 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..2921ef9161be 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; @@ -362,7 +516,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -372,7 +526,6 @@ }; 97C147041CF9000F007C117D /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; @@ -413,7 +566,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 8.0; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; @@ -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 bfe81b9056fb..f035720396dd 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() { @@ -188,7 +188,7 @@ class _ButterFlyAssetVideoState extends State<_ButterFlyAssetVideo> { alignment: Alignment.bottomCenter, children: [ VideoPlayer(_controller), - _PlayPauseOverlay(controller: _controller), + _ControlsOverlay(controller: _controller), VideoProgressIndicator(_controller, allowScrubbing: true), ], ), @@ -206,12 +206,13 @@ class _BumbleBeeRemoteVideo extends StatefulWidget { } class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { - VideoPlayerController _controller; + late VideoPlayerController _controller; Future _loadCaptions() async { final String fileContents = await DefaultAssetBundle.of(context) - .loadString('assets/bumble_bee_captions.srt'); - return SubRipCaptionFile(fileContents); + .loadString('assets/bumble_bee_captions.vtt'); + return WebVTTCaptionFile( + fileContents); // For vtt files, use WebVTTCaptionFile } @override @@ -220,6 +221,7 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { _controller = VideoPlayerController.network( 'https://flutter.github.io/assets-for-api-docs/assets/videos/bee.mp4', closedCaptionFile: _loadCaptions(), + videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true), ); _controller.addListener(() { @@ -251,7 +253,7 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { children: [ VideoPlayer(_controller), ClosedCaption(text: _controller.value.caption.text), - _PlayPauseOverlay(controller: _controller), + _ControlsOverlay(controller: _controller), VideoProgressIndicator(_controller, allowScrubbing: true), ], ), @@ -263,8 +265,20 @@ class _BumbleBeeRemoteVideoState extends State<_BumbleBeeRemoteVideo> { } } -class _PlayPauseOverlay extends StatelessWidget { - const _PlayPauseOverlay({Key key, this.controller}) : super(key: key); +class _ControlsOverlay extends StatelessWidget { + const _ControlsOverlay({Key? key, required this.controller}) + : super(key: key); + + static const _examplePlaybackRates = [ + 0.25, + 0.5, + 1.0, + 1.5, + 2.0, + 3.0, + 5.0, + 10.0, + ]; final VideoPlayerController controller; @@ -293,6 +307,35 @@ class _PlayPauseOverlay extends StatelessWidget { controller.value.isPlaying ? controller.pause() : controller.play(); }, ), + Align( + alignment: Alignment.topRight, + child: PopupMenuButton( + initialValue: controller.value.playbackSpeed, + tooltip: 'Playback speed', + onSelected: (speed) { + controller.setPlaybackSpeed(speed); + }, + itemBuilder: (context) { + return [ + for (final speed in _examplePlaybackRates) + PopupMenuItem( + value: speed, + child: Text('${speed}x'), + ) + ]; + }, + child: Padding( + padding: const EdgeInsets.symmetric( + // Using less vertical padding as the text is also longer + // horizontally, so it feels like it would need more spacing + // horizontally (matching the aspect ratio of the video). + vertical: 12, + horizontal: 16, + ), + child: Text('${controller.value.playbackSpeed}x'), + ), + ), + ), ], ); } @@ -304,7 +347,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 29f1c91e4af3..0539f3c6f56c 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: @@ -12,13 +22,15 @@ dev_dependencies: sdk: flutter flutter_driver: sdk: flutter - e2e: "^0.2.0" + integration_test: + sdk: flutter test: any - pedantic: ^1.8.0 + pedantic: ^1.10.0 flutter: uses-material-design: true assets: - - assets/flutter-mark-square-64.png - - assets/Butterfly-209.mp4 - - assets/bumble_bee_captions.srt + - assets/flutter-mark-square-64.png + - assets/Butterfly-209.mp4 + - assets/bumble_bee_captions.srt + - assets/bumble_bee_captions.vtt 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 new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/video_player/video_player/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/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_e2e.dart b/packages/video_player/video_player/example/test_driver/video_player_e2e.dart deleted file mode 100644 index bf35cf50b728..000000000000 --- a/packages/video_player/video_player/example/test_driver/video_player_e2e.dart +++ /dev/null @@ -1,66 +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:io'; -import 'package:e2e/e2e.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:video_player/video_player.dart'; - -const Duration _playDuration = Duration(seconds: 1); - -void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); - VideoPlayerController _controller; - tearDown(() async => _controller.dispose()); - - group('asset videos', () { - setUp(() { - _controller = VideoPlayerController.asset('assets/Butterfly-209.mp4'); - }); - - testWidgets('can be initialized', (WidgetTester tester) async { - await _controller.initialize(); - - expect(_controller.value.initialized, 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(); - - await _controller.play(); - await tester.pumpAndSettle(_playDuration); - - expect(_controller.value.isPlaying, true); - expect(_controller.value.position, - (Duration position) => position > const Duration(seconds: 0)); - }, 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)); - }, skip: Platform.isIOS); - - testWidgets('can be paused', (WidgetTester tester) async { - await _controller.initialize(); - - // Play for a second, then pause, and then wait a second. - await _controller.play(); - await tester.pumpAndSettle(_playDuration); - await _controller.pause(); - final Duration pausedPosition = _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); - }, skip: Platform.isIOS); - }); -} diff --git a/packages/video_player/video_player/example/test_driver/video_player_e2e_test.dart b/packages/video_player/video_player/example/test_driver/video_player_e2e_test.dart deleted file mode 100644 index f3aa9e218d82..000000000000 --- a/packages/video_player/video_player/example/test_driver/video_player_e2e_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 'dart:async'; -import 'dart:io'; -import 'package:flutter_driver/flutter_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); -} 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 7dbc1b0bfd11..f0f672d87431 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. @@ -11,11 +11,6 @@ #error Code Requires ARC. #endif -int64_t FLTCMTimeToMillis(CMTime time) { - if (time.timescale == 0) return 0; - return time.value * 1000 / time.timescale; -} - @interface FLTFrameUpdater : NSObject @property(nonatomic) int64_t textureId; @property(nonatomic, weak, readonly) NSObject* registry; @@ -46,7 +41,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 +59,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 { @@ -105,6 +102,16 @@ - (void)itemDidPlayToEndTime:(NSNotification*)notification { } } +const int64_t TIME_UNSET = -9223372036854775807; + +static inline int64_t FLTCMTimeToMillis(CMTime time) { + // When CMTIME_IS_INDEFINITE return a value that matches TIME_UNSET from ExoPlayer2 on Android. + // Fixes https://github.com/flutter/flutter/issues/48670 + if (CMTIME_IS_INDEFINITE(time)) return TIME_UNSET; + if (time.timescale == 0) return 0; + return time.value * 1000 / time.timescale; +} + static inline CGFloat radiansToDegrees(CGFloat radians) { // Input range [-pi, pi] or [-180, 180] CGFloat degrees = GLKMathRadiansToDegrees((float)radians); @@ -162,8 +169,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]; } @@ -359,6 +373,30 @@ - (void)setVolume:(double)volume { _player.volume = (float)((volume < 0.0) ? 0.0 : ((volume > 1.0) ? 1.0 : volume)); } +- (void)setPlaybackSpeed:(double)speed { + // See https://developer.apple.com/library/archive/qa/qa1772/_index.html for an explanation of + // these checks. + if (speed > 2.0 && !_player.currentItem.canPlayFastForward) { + if (_eventSink != nil) { + _eventSink([FlutterError errorWithCode:@"VideoError" + message:@"Video cannot be fast-forwarded beyond 2.0x" + details:nil]); + } + return; + } + + if (speed < 1.0 && !_player.currentItem.canPlaySlowForward) { + if (_eventSink != nil) { + _eventSink([FlutterError errorWithCode:@"VideoError" + message:@"Video cannot be slow-forwarded" + details:nil]); + } + return; + } + + _player.rate = speed; +} + - (CVPixelBufferRef)copyPixelBuffer { CMTime outputItemTime = [_videoOutput itemTimeForHostTime:CACurrentMediaTime()]; if ([_videoOutput hasNewPixelBufferForItemTime:outputItemTime]) { @@ -368,7 +406,7 @@ - (CVPixelBufferRef)copyPixelBuffer { } } -- (void)onTextureUnregistered { +- (void)onTextureUnregistered:(NSObject*)texture { dispatch_async(dispatch_get_main_queue(), ^{ [self dispose]; }); @@ -498,7 +536,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]; @@ -538,6 +577,11 @@ - (void)setVolume:(FLTVolumeMessage*)input error:(FlutterError**)error { [player setVolume:[input.volume doubleValue]]; } +- (void)setPlaybackSpeed:(FLTPlaybackSpeedMessage*)input error:(FlutterError**)error { + FLTVideoPlayer* player = _players[input.textureId]; + [player setPlaybackSpeed:[input.speed doubleValue]]; +} + - (void)play:(FLTTextureMessage*)input error:(FlutterError**)error { FLTVideoPlayer* player = _players[input.textureId]; [player play]; @@ -560,4 +604,15 @@ - (void)pause:(FLTTextureMessage*)input error:(FlutterError**)error { [player pause]; } +- (void)setMixWithOthers:(FLTMixWithOthersMessage*)input + error:(FlutterError* _Nullable __autoreleasing*)error { + if ([input.mixWithOthers boolValue]) { + [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback + withOptions:AVAudioSessionCategoryOptionMixWithOthers + error:nil]; + } else { + [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback error:nil]; + } +} + @end diff --git a/packages/video_player/video_player/ios/Classes/messages.h b/packages/video_player/video_player/ios/Classes/messages.h index 3c89b1f203d1..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.0-experimental.11), 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; @@ -11,7 +15,9 @@ NS_ASSUME_NONNULL_BEGIN @class FLTCreateMessage; @class FLTLoopingMessage; @class FLTVolumeMessage; +@class FLTPlaybackSpeedMessage; @class FLTPositionMessage; +@class FLTMixWithOthersMessage; @interface FLTTextureMessage : NSObject @property(nonatomic, strong, nullable) NSNumber *textureId; @@ -22,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 @@ -34,11 +41,20 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, strong, nullable) NSNumber *volume; @end +@interface FLTPlaybackSpeedMessage : NSObject +@property(nonatomic, strong, nullable) NSNumber *textureId; +@property(nonatomic, strong, nullable) NSNumber *speed; +@end + @interface FLTPositionMessage : NSObject @property(nonatomic, strong, nullable) NSNumber *textureId; @property(nonatomic, strong, nullable) NSNumber *position; @end +@interface FLTMixWithOthersMessage : NSObject +@property(nonatomic, strong, nullable) NSNumber *mixWithOthers; +@end + @protocol FLTVideoPlayerApi - (void)initialize:(FlutterError *_Nullable *_Nonnull)error; - (nullable FLTTextureMessage *)create:(FLTCreateMessage *)input @@ -46,11 +62,15 @@ NS_ASSUME_NONNULL_BEGIN - (void)dispose:(FLTTextureMessage *)input error:(FlutterError *_Nullable *_Nonnull)error; - (void)setLooping:(FLTLoopingMessage *)input error:(FlutterError *_Nullable *_Nonnull)error; - (void)setVolume:(FLTVolumeMessage *)input error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setPlaybackSpeed:(FLTPlaybackSpeedMessage *)input + error:(FlutterError *_Nullable *_Nonnull)error; - (void)play:(FLTTextureMessage *)input error:(FlutterError *_Nullable *_Nonnull)error; - (nullable FLTPositionMessage *)position:(FLTTextureMessage *)input error:(FlutterError *_Nullable *_Nonnull)error; - (void)seekTo:(FLTPositionMessage *)input error:(FlutterError *_Nullable *_Nonnull)error; - (void)pause:(FLTTextureMessage *)input error:(FlutterError *_Nullable *_Nonnull)error; +- (void)setMixWithOthers:(FLTMixWithOthersMessage *)input + error:(FlutterError *_Nullable *_Nonnull)error; @end extern void FLTVideoPlayerApiSetup(id binaryMessenger, diff --git a/packages/video_player/video_player/ios/Classes/messages.m b/packages/video_player/video_player/ios/Classes/messages.m index 3694a11622dc..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.0-experimental.11), 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 () @@ -36,10 +42,18 @@ @interface FLTVolumeMessage () + (FLTVolumeMessage *)fromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end +@interface FLTPlaybackSpeedMessage () ++ (FLTPlaybackSpeedMessage *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end @interface FLTPositionMessage () + (FLTPositionMessage *)fromMap:(NSDictionary *)dict; - (NSDictionary *)toMap; @end +@interface FLTMixWithOthersMessage () ++ (FLTMixWithOthersMessage *)fromMap:(NSDictionary *)dict; +- (NSDictionary *)toMap; +@end @implementation FLTTextureMessage + (FLTTextureMessage *)fromMap:(NSDictionary *)dict { @@ -76,16 +90,22 @@ + (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 { return [NSDictionary dictionaryWithObjectsAndKeys:(self.asset ? self.asset : [NSNull null]), @"asset", (self.uri ? self.uri : [NSNull null]), @"uri", - (self.packageName != nil ? self.packageName : [NSNull null]), + (self.packageName ? self.packageName : [NSNull null]), @"packageName", - (self.formatHint != nil ? self.formatHint : [NSNull null]), - @"formatHint", nil]; + (self.formatHint ? self.formatHint : [NSNull null]), + @"formatHint", + (self.httpHeaders ? self.httpHeaders : [NSNull null]), + @"httpHeaders", nil]; } @end @@ -132,6 +152,27 @@ - (NSDictionary *)toMap { } @end +@implementation FLTPlaybackSpeedMessage ++ (FLTPlaybackSpeedMessage *)fromMap:(NSDictionary *)dict { + FLTPlaybackSpeedMessage *result = [[FLTPlaybackSpeedMessage alloc] init]; + result.textureId = dict[@"textureId"]; + if ((NSNull *)result.textureId == [NSNull null]) { + result.textureId = nil; + } + result.speed = dict[@"speed"]; + if ((NSNull *)result.speed == [NSNull null]) { + result.speed = nil; + } + return result; +} +- (NSDictionary *)toMap { + return [NSDictionary + dictionaryWithObjectsAndKeys:(self.textureId != nil ? self.textureId : [NSNull null]), + @"textureId", (self.speed != nil ? self.speed : [NSNull null]), + @"speed", nil]; +} +@end + @implementation FLTPositionMessage + (FLTPositionMessage *)fromMap:(NSDictionary *)dict { FLTPositionMessage *result = [[FLTPositionMessage alloc] init]; @@ -154,6 +195,22 @@ - (NSDictionary *)toMap { } @end +@implementation FLTMixWithOthersMessage ++ (FLTMixWithOthersMessage *)fromMap:(NSDictionary *)dict { + FLTMixWithOthersMessage *result = [[FLTMixWithOthersMessage alloc] init]; + result.mixWithOthers = dict[@"mixWithOthers"]; + if ((NSNull *)result.mixWithOthers == [NSNull null]) { + result.mixWithOthers = nil; + } + return result; +} +- (NSDictionary *)toMap { + return [NSDictionary + dictionaryWithObjectsAndKeys:(self.mixWithOthers != nil ? self.mixWithOthers : [NSNull null]), + @"mixWithOthers", nil]; +} +@end + void FLTVideoPlayerApiSetup(id binaryMessenger, id api) { { FlutterBasicMessageChannel *channel = [FlutterBasicMessageChannel @@ -175,8 +232,8 @@ void FLTVideoPlayerApiSetup(id binaryMessenger, id binaryMessenger, id binaryMessenger, id binaryMessenger, id binaryMessenger, id binaryMessenger, id binaryMessenger, id binaryMessenger, id binaryMessenger, id 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' } + s.platform = :ios, '9.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } end diff --git a/packages/video_player/video_player/lib/src/closed_caption_file.dart b/packages/video_player/video_player/lib/src/closed_caption_file.dart index 2d9242a675d5..e410e2652ad3 100644 --- a/packages/video_player/video_player/lib/src/closed_caption_file.dart +++ b/packages/video_player/video_player/lib/src/closed_caption_file.dart @@ -1,10 +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. import 'sub_rip.dart'; export 'sub_rip.dart' show SubRipCaptionFile; +import 'web_vtt.dart'; +export 'web_vtt.dart' show WebVTTCaptionFile; + /// A structured representation of a parsed closed caption file. /// /// A closed caption file includes a list of captions, each with a start and end @@ -15,6 +18,7 @@ export 'sub_rip.dart' show SubRipCaptionFile; /// /// See: /// * [SubRipCaptionFile]. +/// * [WebVTTCaptionFile]. abstract class ClosedCaptionFile { /// The full list of captions from a given file. /// @@ -31,7 +35,12 @@ class Caption { /// /// This is not recommended for direct use unless you are writing a parser for /// a new closed captioning file type. - const Caption({this.number, this.start, this.end, this.text}); + const Caption({ + required this.number, + required this.start, + required this.end, + required this.text, + }); /// The number that this caption was assigned. final int number; @@ -45,4 +54,22 @@ class Caption { /// The actual text that should appear on screen to be read between [start] /// and [end]. final String text; + + /// A no caption object. This is a caption with [start] and [end] durations of zero, + /// and an empty [text] string. + static const Caption none = Caption( + number: 0, + start: Duration.zero, + end: Duration.zero, + text: '', + ); + + @override + String toString() { + return '$runtimeType(' + 'number: $number, ' + 'start: $start, ' + 'end: $end, ' + 'text: $text)'; + } } diff --git a/packages/video_player/video_player/lib/src/sub_rip.dart b/packages/video_player/video_player/lib/src/sub_rip.dart index 15dc43cbbd24..5d6863f72bb8 100644 --- a/packages/video_player/video_player/lib/src/sub_rip.dart +++ b/packages/video_player/video_player/lib/src/sub_rip.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,6 +16,8 @@ class SubRipCaptionFile extends ClosedCaptionFile { : _captions = _parseCaptionsFromSubRipString(fileContents); /// The entire body of the SubRip file. + // TODO(cyanglaz): Remove this public member as it doesn't seem need to exist. + // https://github.com/flutter/flutter/issues/90471 final String fileContents; @override @@ -30,19 +32,18 @@ List _parseCaptionsFromSubRipString(String file) { if (captionLines.length < 3) break; final int captionNumber = int.parse(captionLines[0]); - final _StartAndEnd startAndEnd = - _StartAndEnd.fromSubRipString(captionLines[1]); + final _CaptionRange captionRange = + _CaptionRange.fromSubRipString(captionLines[1]); final String text = captionLines.sublist(2).join('\n'); final Caption newCaption = Caption( number: captionNumber, - start: startAndEnd.start, - end: startAndEnd.end, + start: captionRange.start, + end: captionRange.end, text: text, ); - - if (newCaption.start != null && newCaption.end != null) { + if (newCaption.start != newCaption.end) { captions.add(newCaption); } } @@ -50,21 +51,21 @@ List _parseCaptionsFromSubRipString(String file) { return captions; } -class _StartAndEnd { +class _CaptionRange { final Duration start; final Duration end; - _StartAndEnd(this.start, this.end); + _CaptionRange(this.start, this.end); // Assumes format from an SubRip file. // For example: // 00:01:54,724 --> 00:01:56,760 - static _StartAndEnd fromSubRipString(String line) { + static _CaptionRange fromSubRipString(String line) { final RegExp format = RegExp(_subRipTimeStamp + _subRipArrow + _subRipTimeStamp); if (!format.hasMatch(line)) { - return _StartAndEnd(null, null); + return _CaptionRange(Duration.zero, Duration.zero); } final List times = line.split(_subRipArrow); @@ -72,7 +73,7 @@ class _StartAndEnd { final Duration start = _parseSubRipTimestamp(times[0]); final Duration end = _parseSubRipTimestamp(times[1]); - return _StartAndEnd(start, end); + return _CaptionRange(start, end); } } @@ -84,7 +85,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/src/web_vtt.dart b/packages/video_player/video_player/lib/src/web_vtt.dart new file mode 100644 index 000000000000..6c4527d34d67 --- /dev/null +++ b/packages/video_player/video_player/lib/src/web_vtt.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 'dart:convert'; + +import 'package:html/dom.dart'; + +import 'closed_caption_file.dart'; +import 'package:html/parser.dart' as html_parser; + +/// Represents a [ClosedCaptionFile], parsed from the WebVTT file format. +/// See: https://en.wikipedia.org/wiki/WebVTT +class WebVTTCaptionFile extends ClosedCaptionFile { + /// Parses a string into a [ClosedCaptionFile], assuming [fileContents] is in + /// the WebVTT file format. + /// * See: https://en.wikipedia.org/wiki/WebVTT + WebVTTCaptionFile(String fileContents) + : _captions = _parseCaptionsFromWebVTTString(fileContents); + + @override + List get captions => _captions; + + final List _captions; +} + +List _parseCaptionsFromWebVTTString(String file) { + final List captions = []; + + // Ignore metadata + Set metadata = {'HEADER', 'NOTE', 'REGION', 'WEBVTT'}; + + int captionNumber = 1; + for (List captionLines in _readWebVTTFile(file)) { + // CaptionLines represent a complete caption. + // E.g + // [ + // [00:00.000 --> 01:24.000 align:center] + // ['Introduction'] + // ] + // If caption has just header or time, but no text, `captionLines.length` will be 1. + if (captionLines.length < 2) continue; + + // If caption has header equal metadata, ignore. + String metadaType = captionLines[0].split(' ')[0]; + if (metadata.contains(metadaType)) continue; + + // Caption has header + bool hasHeader = captionLines.length > 2; + if (hasHeader) { + final int? tryParseCaptionNumber = int.tryParse(captionLines[0]); + if (tryParseCaptionNumber != null) { + captionNumber = tryParseCaptionNumber; + } + } + + final _CaptionRange? captionRange = _CaptionRange.fromWebVTTString( + hasHeader ? captionLines[1] : captionLines[0], + ); + + if (captionRange == null) { + continue; + } + + final String text = captionLines.sublist(hasHeader ? 2 : 1).join('\n'); + + // TODO(cyanglaz): Handle special syntax in VTT captions. + // https://github.com/flutter/flutter/issues/90007. + final String textWithoutFormat = _extractTextFromHtml(text); + + final Caption newCaption = Caption( + number: captionNumber, + start: captionRange.start, + end: captionRange.end, + text: textWithoutFormat, + ); + captions.add(newCaption); + captionNumber++; + } + + return captions; +} + +class _CaptionRange { + final Duration start; + final Duration end; + + _CaptionRange(this.start, this.end); + + // Assumes format from an VTT file. + // For example: + // 00:09.000 --> 00:11.000 + static _CaptionRange? fromWebVTTString(String line) { + final RegExp format = + RegExp(_webVTTTimeStamp + _webVTTArrow + _webVTTTimeStamp); + + if (!format.hasMatch(line)) { + return null; + } + + final List times = line.split(_webVTTArrow); + + final Duration? start = _parseWebVTTTimestamp(times[0]); + final Duration? end = _parseWebVTTTimestamp(times[1]); + + if (start == null || end == null) { + return null; + } + + return _CaptionRange(start, end); + } +} + +String _extractTextFromHtml(String htmlString) { + final Document document = html_parser.parse(htmlString); + final Element? body = document.body; + if (body == null) { + return ''; + } + final Element? bodyElement = html_parser.parse(body.text).documentElement; + return bodyElement?.text ?? ''; +} + +// Parses a time stamp in an VTT file into a Duration. +// +// Returns `null` if `timestampString` is in an invalid format. +// +// For example: +// +// _parseWebVTTTimestamp('00:01:08.430') +// returns +// Duration(hours: 0, minutes: 1, seconds: 8, milliseconds: 430) +Duration? _parseWebVTTTimestamp(String timestampString) { + if (!RegExp(_webVTTTimeStamp).hasMatch(timestampString)) { + return null; + } + + final List dotSections = timestampString.split('.'); + final List timeComponents = dotSections[0].split(':'); + + // Validating and parsing the `timestampString`, invalid format will result this method + // to return `null`. See https://www.w3.org/TR/webvtt1/#webvtt-timestamp for valid + // WebVTT timestamp format. + if (timeComponents.length > 3 || timeComponents.length < 2) { + return null; + } + int hours = 0; + if (timeComponents.length == 3) { + final String hourString = timeComponents.removeAt(0); + if (hourString.length < 2) { + return null; + } + hours = int.parse(hourString); + } + final int minutes = int.parse(timeComponents.removeAt(0)); + if (minutes < 0 || minutes > 59) { + return null; + } + final int seconds = int.parse(timeComponents.removeAt(0)); + if (seconds < 0 || seconds > 59) { + return null; + } + + List milisecondsStyles = dotSections[1].split(" "); + + // TODO(cyanglaz): Handle caption styles. + // https://github.com/flutter/flutter/issues/90009. + // ```dart + // if (milisecondsStyles.length > 1) { + // List styles = milisecondsStyles.sublist(1); + // } + // ``` + // For a better readable code style, style parsing should happen before + // calling this method. See: https://github.com/flutter/plugins/pull/2878/files#r713381134. + int milliseconds = int.parse(milisecondsStyles[0]); + + return Duration( + hours: hours, + minutes: minutes, + seconds: seconds, + milliseconds: milliseconds, + ); +} + +// Reads on VTT file and splits it into Lists of strings where each list is one +// caption. +List> _readWebVTTFile(String file) { + final List lines = LineSplitter.split(file).toList(); + + final List> captionStrings = >[]; + List currentCaption = []; + int lineIndex = 0; + for (final String line in lines) { + final bool isLineBlank = line.trim().isEmpty; + if (!isLineBlank) { + currentCaption.add(line); + } + + if (isLineBlank || lineIndex == lines.length - 1) { + captionStrings.add(currentCaption); + currentCaption = []; + } + + lineIndex += 1; + } + + return captionStrings; +} + +const String _webVTTTimeStamp = r'(\d+):(\d{2})(:\d{2})?\.(\d{3})'; +const String _webVTTArrow = r' --> '; diff --git a/packages/video_player/video_player/lib/video_player.dart b/packages/video_player/video_player/lib/video_player.dart index f2f9289c1fda..fe3437593a81 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. @@ -6,13 +6,13 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:meta/meta.dart'; - import 'package:video_player_platform_interface/video_player_platform_interface.dart'; + export 'package:video_player_platform_interface/video_player_platform_interface.dart' - show DurationRange, DataSourceType, VideoFormat; + show DurationRange, DataSourceType, VideoFormat, VideoPlayerOptions; import 'src/closed_caption_file.dart'; export 'src/closed_caption_file.dart'; @@ -28,29 +28,34 @@ 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, this.volume = 1.0, + this.playbackSpeed = 1.0, 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. @@ -59,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. @@ -77,27 +82,32 @@ class VideoPlayerValue { /// The current volume of the playback. final double volume; + /// The current speed of the playback. + final double playbackSpeed; + /// A description of the error if present. /// - /// If [hasError] is false this is [null]. - final String errorDescription; + /// 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) { + if (!isInitialized || size.width == 0 || size.height == 0) { return 1.0; } final double aspectRatio = size.width / size.height; @@ -110,16 +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, - 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, @@ -127,10 +139,12 @@ 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, volume: volume ?? this.volume, + playbackSpeed: playbackSpeed ?? this.playbackSpeed, errorDescription: errorDescription ?? this.errorDescription, ); } @@ -143,10 +157,12 @@ class VideoPlayerValue { 'position: $position, ' 'caption: $caption, ' 'buffered: [${buffered.join(', ')}], ' + 'isInitialized: $isInitialized, ' 'isPlaying: $isPlaying, ' 'isLooping: $isLooping, ' - 'isBuffering: $isBuffering' + 'isBuffering: $isBuffering, ' 'volume: $volume, ' + 'playbackSpeed: $playbackSpeed, ' 'errorDescription: $errorDescription)'; } } @@ -168,10 +184,11 @@ class VideoPlayerController extends ValueNotifier { /// null. The [package] argument must be non-null when the asset comes from a /// package and null otherwise. VideoPlayerController.asset(this.dataSource, - {this.package, this.closedCaptionFile}) + {this.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. @@ -180,53 +197,87 @@ 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}) - : 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. /// /// This will load the file from the file-URI given by: /// `'file://${file.path}'`. - VideoPlayerController.file(File file, {this.closedCaptionFile}) + VideoPlayerController.file(File file, + {this.closedCaptionFile, this.videoPlayerOptions}) : dataSource = 'file://${file.path}', dataSourceType = DataSourceType.file, package = null, formatHint = null, - super(VideoPlayerValue(duration: null)); + httpHeaders = const {}, + super(VideoPlayerValue(duration: Duration.zero)); - int _textureId; + /// Constructs a [VideoPlayerController] playing a video from a contentUri. + /// + /// This will load the video from the input content-URI. + /// This is supported on Android only. + VideoPlayerController.contentUri(Uri contentUri, + {this.closedCaptionFile, this.videoPlayerOptions}) + : assert(defaultTargetPlatform == TargetPlatform.android, + 'VideoPlayerController.contentUri is only supported on Android.'), + dataSource = contentUri.toString(), + dataSourceType = DataSourceType.contentUri, + package = null, + formatHint = null, + 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; + /// 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. @@ -239,7 +290,7 @@ class VideoPlayerController extends ValueNotifier { _lifeCycleObserver.initialize(); _creatingCompleter = Completer(); - DataSource dataSourceDescription; + late DataSource dataSourceDescription; switch (dataSourceType) { case DataSourceType.asset: dataSourceDescription = DataSource( @@ -253,6 +304,7 @@ class VideoPlayerController extends ValueNotifier { sourceType: DataSourceType.network, uri: dataSource, formatHint: formatHint, + httpHeaders: httpHeaders, ); break; case DataSourceType.file: @@ -261,9 +313,22 @@ class VideoPlayerController extends ValueNotifier { uri: dataSource, ); break; + case DataSourceType.contentUri: + dataSourceDescription = DataSource( + sourceType: DataSourceType.contentUri, + uri: dataSource, + ); + break; } - _textureId = await _videoPlayerPlatform.create(dataSourceDescription); - _creatingCompleter.complete(null); + + if (videoPlayerOptions?.mixWithOthers != null) { + await _videoPlayerPlatform + .setMixWithOthers(videoPlayerOptions!.mixWithOthers); + } + + _textureId = (await _videoPlayerPlatform.create(dataSourceDescription)) ?? + kUninitializedTextureId; + _creatingCompleter!.complete(null); final Completer initializingCompleter = Completer(); void eventListener(VideoEvent event) { @@ -276,6 +341,7 @@ class VideoPlayerController extends ValueNotifier { value = value.copyWith( duration: event.duration, size: event.size, + isInitialized: event.duration != null, ); initializingCompleter.complete(null); _applyLooping(); @@ -283,8 +349,11 @@ class VideoPlayerController extends ValueNotifier { _applyPlayPause(); break; case VideoEventType.completed: - value = value.copyWith(isPlaying: false, position: value.duration); - _timer?.cancel(); + // In this case we need to stop _timer, set isPlaying=false, and + // position=value.duration. Instead of setting the values directly, + // we use pause() and seekTo() to ensure the platform stops playing + // and seeks to the last frame of the video. + pause().then((void pauseResult) => seekTo(value.duration)); break; case VideoEventType.bufferingUpdate: value = value.copyWith(buffered: event.buffered); @@ -308,8 +377,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); @@ -325,7 +394,7 @@ class VideoPlayerController extends ValueNotifier { @override Future dispose() async { if (_creatingCompleter != null) { - await _creatingCompleter.future; + await _creatingCompleter!.future; if (!_isDisposed) { _isDisposed = true; _timer?.cancel(); @@ -340,10 +409,15 @@ class VideoPlayerController extends ValueNotifier { /// Starts playing the video. /// + /// If the video is at the end, this method starts playing from the beginning. + /// /// This method returns a future that completes as soon as the "play" command /// has been sent to the platform, not when playback itself is totally /// finished. Future play() async { + if (value.position == value.duration) { + await seekTo(const Duration()); + } value = value.copyWith(isPlaying: true); await _applyPlayPause(); } @@ -362,31 +436,39 @@ class VideoPlayerController extends ValueNotifier { } Future _applyLooping() async { - if (!value.initialized || _isDisposed) { + if (_isDisposedOrNotInitialized) { return; } await _videoPlayerPlatform.setLooping(_textureId, value.isLooping); } Future _applyPlayPause() async { - if (!value.initialized || _isDisposed) { + if (_isDisposedOrNotInitialized) { 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); }, ); + + // This ensures that the correct playback speed is always applied when + // playing back. This is necessary because we do not set playback speed + // when paused. + await _applyPlaybackSpeed(); } else { _timer?.cancel(); await _videoPlayerPlatform.pause(_textureId); @@ -394,14 +476,30 @@ class VideoPlayerController extends ValueNotifier { } Future _applyVolume() async { - if (!value.initialized || _isDisposed) { + if (_isDisposedOrNotInitialized) { return; } await _videoPlayerPlatform.setVolume(_textureId, value.volume); } + Future _applyPlaybackSpeed() async { + if (_isDisposedOrNotInitialized) { + return; + } + + // Setting the playback speed on iOS will trigger the video to play. We + // prevent this from happening by not applying the playback speed until + // the video is manually played from Flutter. + if (!value.isPlaying) return; + + await _videoPlayerPlatform.setPlaybackSpeed( + _textureId, + value.playbackSpeed, + ); + } + /// The position in the current video. - Future get position async { + Future get position async { if (_isDisposed) { return null; } @@ -414,7 +512,7 @@ class VideoPlayerController extends ValueNotifier { /// If [moment] is outside of the video's full range it will be automatically /// and silently clamped. Future seekTo(Duration position) async { - if (_isDisposed) { + if (_isDisposedOrNotInitialized) { return; } if (position > value.duration) { @@ -435,6 +533,40 @@ class VideoPlayerController extends ValueNotifier { await _applyVolume(); } + /// Sets the playback speed of [this]. + /// + /// [speed] indicates a speed value with different platforms accepting + /// different ranges for speed values. The [speed] must be greater than 0. + /// + /// The values will be handled as follows: + /// * On web, the audio will be muted at some speed when the browser + /// determines that the sound would not be useful anymore. For example, + /// "Gecko mutes the sound outside the range `0.25` to `5.0`" (see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/playbackRate). + /// * On Android, some very extreme speeds will not be played back accurately. + /// Instead, your video will still be played back, but the speed will be + /// clamped by ExoPlayer (but the values are allowed by the player, like on + /// web). + /// * On iOS, you can sometimes not go above `2.0` playback speed on a video. + /// An error will be thrown for if the option is unsupported. It is also + /// possible that your specific video cannot be slowed down, in which case + /// the plugin also reports errors. + Future setPlaybackSpeed(double speed) async { + if (speed < 0) { + throw ArgumentError.value( + speed, + 'Negative playback speeds are generally unsupported.', + ); + } else if (speed == 0) { + throw ArgumentError.value( + speed, + 'Zero playback speed is generally unsupported. Consider using [pause].', + ); + } + + value = value.copyWith(playbackSpeed: speed); + await _applyPlaybackSpeed(); + } + /// The closed caption based on the current [position] in the video. /// /// If there are no closed captions at the current [position], this will @@ -444,23 +576,34 @@ 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) { value = value.copyWith(position: position); value = value.copyWith(caption: _getCaptionAt(position)); } + + @override + void removeListener(VoidCallback listener) { + // Prevent VideoPlayer from causing an exception to be thrown when attempting to + // remove its own listener after the controller has already been disposed. + if (!_isDisposed) { + super.removeListener(listener); + } + } + + bool get _isDisposedOrNotInitialized => _isDisposed || !value.isInitialized; } class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver { @@ -470,7 +613,7 @@ class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver { final VideoPlayerController _controller; void initialize() { - WidgetsBinding.instance.addObserver(this); + _ambiguate(WidgetsBinding.instance)!.addObserver(this); } @override @@ -490,7 +633,7 @@ class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver { } void dispose() { - WidgetsBinding.instance.removeObserver(this); + _ambiguate(WidgetsBinding.instance)!.removeObserver(this); } } @@ -519,8 +662,9 @@ class _VideoPlayerState extends State { }; } - VoidCallback _listener; - int _textureId; + late VoidCallback _listener; + + late int _textureId; @override void initState() { @@ -547,7 +691,7 @@ class _VideoPlayerState extends State { @override Widget build(BuildContext context) { - return _textureId == null + return _textureId == VideoPlayerController.kUninitializedTextureId ? Container() : _videoPlayerPlatform.buildView(_textureId); } @@ -571,7 +715,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), @@ -595,8 +739,8 @@ class VideoProgressColors { class _VideoScrubber extends StatefulWidget { _VideoScrubber({ - @required this.child, - @required this.controller, + required this.child, + required this.controller, }); final Widget child; @@ -614,7 +758,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; @@ -625,7 +769,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; @@ -634,7 +778,7 @@ class _VideoScrubberState extends State<_VideoScrubber> { } }, onHorizontalDragUpdate: (DragUpdateDetails details) { - if (!controller.value.initialized) { + if (!controller.value.isInitialized) { return; } seekToRelativePosition(details.globalPosition); @@ -645,7 +789,7 @@ class _VideoScrubberState extends State<_VideoScrubber> { } }, onTapDown: (TapDownDetails details) { - if (!controller.value.initialized) { + if (!controller.value.isInitialized) { return; } seekToRelativePosition(details.globalPosition); @@ -670,10 +814,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. @@ -710,7 +854,7 @@ class _VideoProgressIndicatorState extends State { }; } - VoidCallback listener; + late VoidCallback listener; VideoPlayerController get controller => widget.controller; @@ -731,7 +875,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; @@ -802,31 +946,33 @@ class ClosedCaption extends StatelessWidget { /// Creates a a new closed caption, designed to be used with /// [VideoPlayerValue.caption]. /// - /// If [text] is null, nothing will be displayed. - const ClosedCaption({Key key, this.text, this.textStyle}) : super(key: key); + /// If [text] is null or empty, nothing will be displayed. + 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; + /// If the text is empty the caption will not be shown. + 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) { + final text = this.text; + if (text == null || text.isEmpty) { + return SizedBox.shrink(); + } + final TextStyle effectiveTextStyle = textStyle ?? DefaultTextStyle.of(context).style.copyWith( fontSize: 36.0, color: Colors.white, ); - if (text == null) { - return SizedBox.shrink(); - } - return Align( alignment: Alignment.bottomCenter, child: Padding( @@ -845,3 +991,10 @@ class ClosedCaption extends StatelessWidget { ); } } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +// TODO(ianh): Remove this once we roll stable in late 2021. +T? _ambiguate(T? value) => value; diff --git a/packages/video_player/video_player/pigeons/messages.dart b/packages/video_player/video_player/pigeons/messages.dart index 2df5b78f13f8..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 { @@ -14,6 +20,11 @@ class VolumeMessage { double volume; } +class PlaybackSpeedMessage { + int textureId; + double speed; +} + class PositionMessage { int textureId; int position; @@ -24,23 +35,31 @@ class CreateMessage { String uri; String packageName; String formatHint; + Map httpHeaders; +} + +class MixWithOthersMessage { + bool mixWithOthers; } -@HostApi() +@HostApi(dartHostTestHandler: 'TestHostVideoPlayerApi') abstract class VideoPlayerApi { void initialize(); TextureMessage create(CreateMessage msg); void dispose(TextureMessage msg); void setLooping(LoopingMessage msg); void setVolume(VolumeMessage msg); + void setPlaybackSpeed(PlaybackSpeedMessage msg); void play(TextureMessage msg); PositionMessage position(TextureMessage msg); void seekTo(PositionMessage msg); void pause(TextureMessage msg); + void setMixWithOthers(MixWithOthersMessage msg); } void configurePigeon(PigeonOptions opts) { opts.dartOut = '../video_player_platform_interface/lib/messages.dart'; + opts.dartTestOut = '../video_player_platform_interface/lib/test.dart'; opts.objcHeaderOut = 'ios/Classes/messages.h'; opts.objcSourceOut = 'ios/Classes/messages.m'; opts.objcOptions.prefix = 'FLT'; diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index 2a4a5b0e9900..a6ee2d594656 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 and iOS. -# 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.10.11 -homepage: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player + widgets on Android, iOS, and web. +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.2.5 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" flutter: plugin: @@ -19,24 +21,21 @@ flutter: default_package: video_player_web dependencies: - meta: "^1.0.5" - video_player_platform_interface: ^2.0.0 + flutter: + sdk: flutter + meta: ^1.3.0 + video_player_platform_interface: ^4.2.0 # The design on https://flutter.dev/go/federated-plugins was to leave # this constraint as "any". We cannot do it right now as it fails pub publish - # validation, so we set a ^ constraint. - # TODO(amirh): Revisit this (either update this part in the design or the pub tool). + # validation, so we set a ^ constraint. The exact value doesn't matter since + # the constraints on the interface pins it. + # TODO(amirh): Revisit this (either update this part in the design or the pub tool). # https://github.com/flutter/flutter/issues/46264 - video_player_web: '>=0.1.1 <2.0.0' - - flutter: - sdk: flutter + video_player_web: ^2.0.0 + html: ^0.15.0 dev_dependencies: flutter_test: sdk: flutter - pedantic: ^1.8.0 - pigeon: 0.1.0-experimental.11 - -environment: - sdk: ">=2.1.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5 <2.0.0" + 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 ac8459d0c9e9..a929c6827fd0 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -1,21 +1,23 @@ -// 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:io'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; -import 'package:video_player/video_player.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:video_player_platform_interface/video_player_platform_interface.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 { @@ -23,35 +25,52 @@ 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; @override Future seekTo(Duration moment) async {} + @override Future setVolume(double volume) async {} + + @override + Future setPlaybackSpeed(double speed) async {} + @override Future initialize() async {} + @override Future pause() async {} + @override Future play() async {} + @override Future setLooping(bool looping) async {} @override - VideoFormat get formatHint => null; + VideoFormat? get formatHint => null; @override Future get closedCaptionFile => _loadClosedCaption(); + + @override + VideoPlayerOptions? get videoPlayerOptions => null; } Future _loadClosedCaption() async => @@ -63,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), ), @@ -84,6 +105,7 @@ void main() { controller.textureId = 123; controller.value = controller.value.copyWith( duration: const Duration(milliseconds: 100), + isInitialized: true, ); await tester.pump(); @@ -116,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 { @@ -132,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 { @@ -140,6 +162,11 @@ void main() { expect(find.byType(Text), findsNothing); }); + testWidgets('handles empty text', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp(home: ClosedCaption(text: ''))); + expect(find.byType(Text), findsNothing); + }); + testWidgets('Passes text contrast ratio guidelines', (WidgetTester tester) async { final String text = 'foo'; @@ -156,7 +183,7 @@ void main() { }); group('VideoPlayerController', () { - FakeVideoPlayerPlatform fakeVideoPlayerPlatform; + late FakeVideoPlayerPlatform fakeVideoPlayerPlatform; setUp(() { fakeVideoPlayerPlatform = FakeVideoPlayerPlatform(); @@ -181,22 +208,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 { @@ -204,7 +269,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; @@ -224,17 +289,27 @@ void main() { }); }); + test('contentUri', () async { + final VideoPlayerController controller = + VideoPlayerController.contentUri(Uri.parse('content://video')); + await controller.initialize(); + + expect(fakeVideoPlayerPlatform.dataSourceDescriptions[0].uri, + 'content://video'); + }); + test('dispose', () async { 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); }); @@ -247,7 +322,42 @@ void main() { await controller.play(); expect(controller.value.isPlaying, isTrue); - expect(fakeVideoPlayerPlatform.calls.last, 'play'); + + // The two last calls will be "play" and then "setPlaybackSpeed". The + // reason for this is that "play" calls "setPlaybackSpeed" internally. + expect( + fakeVideoPlayerPlatform + .calls[fakeVideoPlayerPlatform.calls.length - 2], + 'play'); + expect(fakeVideoPlayerPlatform.calls.last, 'setPlaybackSpeed'); + }); + + test('play before initialized does not call platform', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + expect(controller.value.isInitialized, isFalse); + + await controller.play(); + + expect(fakeVideoPlayerPlatform.calls, isEmpty); + }); + + test('play restarts from beginning if video is at end', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + await controller.initialize(); + const Duration nonzeroDuration = Duration(milliseconds: 100); + controller.value = controller.value.copyWith(duration: nonzeroDuration); + await controller.seekTo(nonzeroDuration); + expect(controller.value.isPlaying, isFalse); + expect(controller.value.position, nonzeroDuration); + + await controller.play(); + + expect(controller.value.isPlaying, isTrue); + expect(controller.value.position, Duration.zero); }); test('setLooping', () async { @@ -288,6 +398,17 @@ void main() { expect(await controller.position, const Duration(milliseconds: 500)); }); + test('before initialized does not call platform', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + expect(controller.value.isInitialized, isFalse); + + await controller.seekTo(const Duration(milliseconds: 500)); + + expect(fakeVideoPlayerPlatform.calls, isEmpty); + }); + test('clamps values that are too high or low', () async { final VideoPlayerController controller = VideoPlayerController.network( 'https://127.0.0.1', @@ -332,6 +453,31 @@ void main() { }); }); + group('setPlaybackSpeed', () { + test('works', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + await controller.initialize(); + expect(controller.value.playbackSpeed, 1.0); + + const double speed = 1.5; + await controller.setPlaybackSpeed(speed); + + expect(controller.value.playbackSpeed, speed); + }); + + test('rejects negative values', () async { + final VideoPlayerController controller = VideoPlayerController.network( + 'https://127.0.0.1', + ); + await controller.initialize(); + expect(controller.value.playbackSpeed, 1.0); + + expect(() => controller.setPlaybackSpeed(-1), throwsArgumentError); + }); + }); + group('caption', () { test('works when seeking', () async { final VideoPlayerController controller = VideoPlayerController.network( @@ -341,19 +487,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'); @@ -366,19 +512,20 @@ void main() { 'https://127.0.0.1', ); await controller.initialize(); + const Duration nonzeroDuration = Duration(milliseconds: 100); + controller.value = controller.value.copyWith(duration: nonzeroDuration); expect(controller.value.isPlaying, isFalse); 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'}); await tester.pumpAndSettle(); expect(controller.value.isPlaying, isFalse); - expect(controller.value.position, controller.value.duration); + expect(controller.value.position, nonzeroDuration); }); testWidgets('buffering status', (WidgetTester tester) async { @@ -389,8 +536,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'}); @@ -447,18 +593,18 @@ 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); expect(uninitialized.isBuffering, isFalse); 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); }); @@ -467,18 +613,18 @@ 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); expect(error.isBuffering, isFalse); 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); }); @@ -487,28 +633,46 @@ 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; const double volume = 0.5; + const double playbackSpeed = 1.5; final VideoPlayerValue value = VideoPlayerValue( - duration: duration, - size: size, - position: position, - caption: caption, - buffered: buffered, - isPlaying: isPlaying, - isLooping: isLooping, - isBuffering: isBuffering, - volume: volume); - - expect(value.toString(), - 'VideoPlayerValue(duration: 0:00:05.000000, size: Size(400.0, 300.0), position: 0:00:01.000000, caption: Instance of \'Caption\', buffered: [DurationRange(start: 0:00:00.000000, end: 0:00:04.000000)], isPlaying: true, isLooping: true, isBuffering: truevolume: 0.5, errorDescription: null)'); + duration: duration, + size: size, + position: position, + caption: caption, + buffered: buffered, + isInitialized: isInitialized, + isPlaying: isPlaying, + isLooping: isLooping, + isBuffering: isBuffering, + volume: volume, + playbackSpeed: playbackSpeed, + ); + + expect( + value.toString(), + 'VideoPlayerValue(duration: 0:00:05.000000, ' + 'size: Size(400.0, 300.0), ' + 'position: 0:00:01.000000, ' + 'caption: Caption(number: 0, start: 0:00:00.000000, end: 0:00:00.000000, text: foo), ' + 'buffered: [DurationRange(start: 0:00:00.000000, end: 0:00:04.000000)], ' + 'isInitialized: true, ' + 'isPlaying: true, ' + 'isLooping: true, ' + 'isBuffering: true, ' + 'volume: 0.5, ' + 'playbackSpeed: 1.5, ' + 'errorDescription: null)'); }); test('copyWith()', () { @@ -517,6 +681,52 @@ void main() { expect(exactCopy.toString(), original.toString()); }); + + 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('no size -> 1.0', () { + final value = VideoPlayerValue( + isInitialized: true, + duration: Duration(seconds: 1), + ); + expect(value.aspectRatio, 1.0); + }); + + test('height = 0 -> 1.0', () { + final value = VideoPlayerValue( + isInitialized: true, + size: Size(640, 0), + duration: Duration(seconds: 1), + ); + expect(value.aspectRatio, 1.0); + }); + + test('width = 0 -> 1.0', () { + final value = VideoPlayerValue( + isInitialized: true, + size: Size(0, 480), + duration: Duration(seconds: 1), + ); + expect(value.aspectRatio, 1.0); + }); + + test('negative aspect ratio -> 1.0', () { + final value = VideoPlayerValue( + isInitialized: true, + size: Size(640, -480), + duration: Duration(seconds: 1), + ); + expect(value.aspectRatio, 1.0); + }); + }); }); test('VideoProgressColors', () { @@ -533,11 +743,19 @@ void main() { expect(colors.bufferedColor, bufferedColor); expect(colors.backgroundColor, backgroundColor); }); + + test('setMixWithOthers', () async { + final VideoPlayerController controller = VideoPlayerController.file( + File(''), + videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true)); + await controller.initialize(); + expect(controller.videoPlayerOptions!.mixWithOthers, true); + }); } -class FakeVideoPlayerPlatform extends VideoPlayerApiTest { +class FakeVideoPlayerPlatform extends TestHostVideoPlayerApi { FakeVideoPlayerPlatform() { - VideoPlayerApiTestSetup(this); + TestHostVideoPlayerApi.setup(this); } Completer initialized = Completer(); @@ -591,7 +809,7 @@ class FakeVideoPlayerPlatform extends VideoPlayerApiTest { @override void seekTo(PositionMessage arg) { calls.add('seekTo'); - _positions[arg.textureId] = Duration(milliseconds: arg.position); + _positions[arg.textureId!] = Duration(milliseconds: arg.position!); } @override @@ -603,6 +821,16 @@ class FakeVideoPlayerPlatform extends VideoPlayerApiTest { void setVolume(VolumeMessage arg) { calls.add('setVolume'); } + + @override + void setPlaybackSpeed(PlaybackSpeedMessage arg) { + calls.add('setPlaybackSpeed'); + } + + @override + void setMixWithOthers(MixWithOthersMessage arg) { + calls.add('setMixWithOthers'); + } } class FakeVideoEventStream { @@ -617,7 +845,7 @@ class FakeVideoEventStream { int height; Duration duration; bool initWithError; - FakeEventsChannel eventsChannel; + late FakeEventsChannel eventsChannel; void onListen() { if (!initWithError) { @@ -639,7 +867,7 @@ class FakeEventsChannel { eventsMethodChannel.setMockMethodCallHandler(onMethodCall); } - MethodChannel eventsMethodChannel; + late MethodChannel eventsMethodChannel; VoidCallback onListen; Future onMethodCall(MethodCall call) { @@ -655,7 +883,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, @@ -664,11 +892,16 @@ 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) {}); + _ambiguate(ServicesBinding.instance)! + .defaultBinaryMessenger + .handlePlatformMessage( + eventsMethodChannel.name, data, (ByteData? data) {}); } } + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +// TODO(ianh): Remove this once we roll stable in late 2021. +T? _ambiguate(T? value) => value; diff --git a/packages/video_player/video_player/test/web_vtt_test.dart b/packages/video_player/video_player/test/web_vtt_test.dart new file mode 100644 index 000000000000..59fce98c5b71 --- /dev/null +++ b/packages/video_player/video_player/test/web_vtt_test.dart @@ -0,0 +1,261 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:video_player/src/closed_caption_file.dart'; +import 'package:video_player/video_player.dart'; + +void main() { + group('Parse VTT file', () { + WebVTTCaptionFile parsedFile; + + test('with Metadata', () { + parsedFile = WebVTTCaptionFile(_valid_vtt_with_metadata); + expect(parsedFile.captions.length, 1); + + expect(parsedFile.captions[0].start, Duration(seconds: 1)); + expect( + parsedFile.captions[0].end, Duration(seconds: 2, milliseconds: 500)); + expect(parsedFile.captions[0].text, 'We are in New York City'); + }); + + test('with Multiline', () { + parsedFile = WebVTTCaptionFile(_valid_vtt_with_multiline); + expect(parsedFile.captions.length, 1); + + expect(parsedFile.captions[0].start, + Duration(seconds: 2, milliseconds: 800)); + expect( + parsedFile.captions[0].end, Duration(seconds: 3, milliseconds: 283)); + expect(parsedFile.captions[0].text, + "— It will perforate your stomach.\n— You could die."); + }); + + test('with styles tags', () { + parsedFile = WebVTTCaptionFile(_valid_vtt_with_styles); + expect(parsedFile.captions.length, 3); + + expect(parsedFile.captions[0].start, + Duration(seconds: 5, milliseconds: 200)); + expect( + parsedFile.captions[0].end, Duration(seconds: 6, milliseconds: 000)); + expect(parsedFile.captions[0].text, + "You know I'm so excited my glasses are falling off here."); + }); + + test('with subtitling features', () { + parsedFile = WebVTTCaptionFile(_valid_vtt_with_subtitling_features); + expect(parsedFile.captions.length, 3); + + expect(parsedFile.captions[0].number, 1); + expect(parsedFile.captions.last.start, Duration(seconds: 4)); + expect(parsedFile.captions.last.end, Duration(seconds: 5)); + expect(parsedFile.captions.last.text, "Transcrit par Célestes™"); + }); + + test('with [hours]:[minutes]:[seconds].[milliseconds].', () { + parsedFile = WebVTTCaptionFile(_valid_vtt_with_hours); + expect(parsedFile.captions.length, 1); + + expect(parsedFile.captions[0].number, 1); + expect(parsedFile.captions.last.start, Duration(seconds: 1)); + expect(parsedFile.captions.last.end, Duration(seconds: 2)); + expect(parsedFile.captions.last.text, "This is a test."); + }); + + test('with [minutes]:[seconds].[milliseconds].', () { + parsedFile = WebVTTCaptionFile(_valid_vtt_without_hours); + expect(parsedFile.captions.length, 1); + + expect(parsedFile.captions[0].number, 1); + expect(parsedFile.captions.last.start, Duration(seconds: 3)); + expect(parsedFile.captions.last.end, Duration(seconds: 4)); + expect(parsedFile.captions.last.text, "This is a test."); + }); + + test('with invalid seconds format returns empty captions.', () { + parsedFile = WebVTTCaptionFile(_invalid_seconds); + expect(parsedFile.captions, isEmpty); + }); + + test('with invalid minutes format returns empty captions.', () { + parsedFile = WebVTTCaptionFile(_invalid_minutes); + expect(parsedFile.captions, isEmpty); + }); + + test('with invalid hours format returns empty captions.', () { + parsedFile = WebVTTCaptionFile(_invalid_hours); + expect(parsedFile.captions, isEmpty); + }); + + test('with invalid component length returns empty captions.', () { + parsedFile = WebVTTCaptionFile(_time_component_too_long); + expect(parsedFile.captions, isEmpty); + + parsedFile = WebVTTCaptionFile(_time_component_too_short); + expect(parsedFile.captions, isEmpty); + }); + }); + + test('Parses VTT file with malformed input.', () { + final ClosedCaptionFile parsedFile = WebVTTCaptionFile(_malformedVTT); + + expect(parsedFile.captions.length, 1); + + final Caption firstCaption = parsedFile.captions.single; + expect(firstCaption.number, 1); + expect(firstCaption.start, Duration(seconds: 13)); + expect(firstCaption.end, Duration(seconds: 16, milliseconds: 0)); + expect(firstCaption.text, 'Valid'); + }); +} + +/// See https://www.w3.org/TR/webvtt1/#introduction-comments +const String _valid_vtt_with_metadata = ''' +WEBVTT Kind: captions; Language: en + +REGION +id:bill +width:40% +lines:3 +regionanchor:100%,100% +viewportanchor:90%,90% +scroll:up + +NOTE +This file was written by Jill. I hope +you enjoy reading it. Some things to +bear in mind: +- I was lip-reading, so the cues may +not be 100% accurate +- I didn’t pay too close attention to +when the cues should start or end. + +1 +00:01.000 --> 00:02.500 +We are in New York City +'''; + +/// See https://www.w3.org/TR/webvtt1/#introduction-multiple-lines +const String _valid_vtt_with_multiline = ''' +WEBVTT + +2 +00:02.800 --> 00:03.283 +— It will perforate your stomach. +— You could die. + +'''; + +/// See https://www.w3.org/TR/webvtt1/#styling +const String _valid_vtt_with_styles = ''' +WEBVTT + +00:05.200 --> 00:06.000 align:start size:50% +You know I'm so excited my glasses are falling off here. + +00:00:06.050 --> 00:00:06.150 +I have a different time! + +00:06.200 --> 00:06.900 +This is yellow text on a blue background + +'''; + +//See https://www.w3.org/TR/webvtt1/#introduction-other-features +const String _valid_vtt_with_subtitling_features = ''' +WEBVTT + +test +00:00.000 --> 00:02.000 +This is a test. + +Slide 1 +00:00:00.000 --> 00:00:10.700 +Title Slide + +crédit de transcription +00:04.000 --> 00:05.000 +Transcrit par Célestes™ + +'''; + +/// With format [hours]:[minutes]:[seconds].[milliseconds] +const String _valid_vtt_with_hours = ''' +WEBVTT + +test +00:00:01.000 --> 00:00:02.000 +This is a test. + +'''; + +/// Invalid seconds format. +const String _invalid_seconds = ''' +WEBVTT + +60:00:000.000 --> 60:02:000.000 +This is a test. + +'''; + +/// Invalid minutes format. +const String _invalid_minutes = ''' +WEBVTT + +60:60:00.000 --> 60:70:00.000 +This is a test. + +'''; + +/// Invalid hours format. +const String _invalid_hours = ''' +WEBVTT + +5:00:00.000 --> 5:02:00.000 +This is a test. + +'''; + +/// Invalid seconds format. +const String _time_component_too_long = ''' +WEBVTT + +60:00:00:00.000 --> 60:02:00:00.000 +This is a test. + +'''; + +/// Invalid seconds format. +const String _time_component_too_short = ''' +WEBVTT + +60:00.000 --> 60:02.000 +This is a test. + +'''; + +/// With format [minutes]:[seconds].[milliseconds] +const String _valid_vtt_without_hours = ''' +WEBVTT + +00:03.000 --> 00:04.000 +This is a test. + +'''; + +const String _malformedVTT = ''' + +WEBVTT Kind: captions; Language: en + +00:09.000--> 00:11.430 +This one should be ignored because the arrow needs a space. + +00:13.000 --> 00:16.000 +Valid + +00:16.000 --> 00:8.000 +This one should be ignored because the time is missing a digit. + +'''; diff --git a/packages/video_player/video_player_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 f5aa5208e93c..b3da9c8924ef 100644 --- a/packages/video_player/video_player_platform_interface/CHANGELOG.md +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -1,3 +1,40 @@ +## 4.2.0 + +* Add `contentUri` to `DataSourceType`. + +## 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. + +## 2.1.1 + +* Fix mixWithOthers test channel. + +## 2.1.0 + +* Add VideoPlayerOptions with audio mix mode + ## 2.0.2 * Migrated tests to use pigeon correctly. diff --git a/packages/video_player/video_player_platform_interface/LICENSE b/packages/video_player/video_player_platform_interface/LICENSE index c89293372cf3..c6823b81eb84 100644 --- a/packages/video_player/video_player_platform_interface/LICENSE +++ b/packages/video_player/video_player_platform_interface/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/video_player/video_player_platform_interface/lib/messages.dart b/packages/video_player/video_player_platform_interface/lib/messages.dart index ea117a51bb07..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,408 +1,423 @@ -// Autogenerated from Pigeon (v0.1.0-experimental.10), 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.12 import 'dart:async'; +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) { - 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) { - 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) { - 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) { - 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 PositionMessage { - int textureId; - int position; - // ignore: unused_element - Map _toMap() { - final Map pigeonMap = {}; +class PlaybackSpeedMessage { + int? textureId; + double? speed; + + Object encode() { + final Map pigeonMap = {}; pigeonMap['textureId'] = textureId; - pigeonMap['position'] = position; + pigeonMap['speed'] = speed; return pigeonMap; } - // ignore: unused_element - static PositionMessage _fromMap(Map pigeonMap) { - final PositionMessage result = PositionMessage(); - result.textureId = pigeonMap['textureId']; - result.position = pigeonMap['position']; - 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?; } } -abstract class VideoPlayerApiTest { - void initialize(); - TextureMessage create(CreateMessage arg); - void dispose(TextureMessage arg); - void setLooping(LoopingMessage arg); - void setVolume(VolumeMessage arg); - void play(TextureMessage arg); - PositionMessage position(TextureMessage arg); - void seekTo(PositionMessage arg); - void pause(TextureMessage arg); -} +class PositionMessage { + int? textureId; + int? position; -void VideoPlayerApiTestSetup(VideoPlayerApiTest 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.play', StandardMessageCodec()); - channel.setMockMessageHandler((dynamic message) async { - final Map mapMessage = message as Map; - final TextureMessage input = TextureMessage._fromMap(mapMessage); - api.play(input); - return {}; - }); + Object encode() { + final Map pigeonMap = {}; + pigeonMap['textureId'] = textureId; + pigeonMap['position'] = position; + return pigeonMap; } - { - 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()}; - }); + + static PositionMessage decode(Object message) { + final Map pigeonMap = message as Map; + return PositionMessage() + ..textureId = pigeonMap['textureId'] as int? + ..position = pigeonMap['position'] as int?; } - { - 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 {}; - }); +} + +class MixWithOthersMessage { + bool? mixWithOthers; + + Object encode() { + final Map pigeonMap = {}; + pigeonMap['mixWithOthers'] = mixWithOthers; + return pigeonMap; } - { - 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 {}; - }); + + 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(encoded) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + replyMap['error'] as Map; + throw PlatformException( + code: error['code'] as String, + message: error['message'] as String?, + details: error['details'], + ); + } else { + // noop + } + } - final Map replyMap = await channel.send(requestMap); + Future setPlaybackSpeed(PlaybackSpeedMessage arg) async { + final Object encoded = arg.encode(); + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.setPlaybackSpeed', + StandardMessageCodec()); + final Map? replyMap = + await channel.send(encoded) as Map?; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + 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(encoded) as Map?; + if (replyMap == null) { + throw PlatformException( + code: 'channel-error', + message: 'Unable to establish connection on channel.', + details: null, + ); + } else if (replyMap['error'] != null) { + final Map error = + replyMap['error'] as Map; + throw PlatformException( + code: error['code'] as String, + message: error['message'] as String?, + details: error['details'], + ); + } else { + // noop + } + } - final Map replyMap = await channel.send(requestMap); + Future setMixWithOthers(MixWithOthersMessage arg) async { + final Object encoded = arg.encode(); + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers', + StandardMessageCodec()); + final Map? replyMap = + await channel.send(encoded) as Map?; if (replyMap == null) { throw PlatformException( - code: 'channel-error', - message: 'Unable to establish connection on channel.', - details: null); + 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 } 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 4b28100e1642..e01e5b8c072c 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,10 +37,14 @@ 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; break; + case DataSourceType.contentUri: + message.uri = dataSource.uri; + break; } TextureMessage response = await _api.create(message); @@ -71,6 +75,15 @@ class MethodChannelVideoPlayer extends VideoPlayerPlatform { ..volume = volume); } + @override + Future setPlaybackSpeed(int textureId, double speed) { + assert(speed > 0); + + return _api.setPlaybackSpeed(PlaybackSpeedMessage() + ..textureId = textureId + ..speed = speed); + } + @override Future seekTo(int textureId, Duration position) { return _api.seekTo(PositionMessage() @@ -82,7 +95,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 @@ -125,6 +138,13 @@ class MethodChannelVideoPlayer extends VideoPlayerPlatform { return Texture(textureId: textureId); } + @override + Future setMixWithOthers(bool mixWithOthers) { + return _api.setMixWithOthers( + MixWithOthersMessage()..mixWithOthers = mixWithOthers, + ); + } + EventChannel _eventChannelFor(int textureId) { return EventChannel('flutter.io/videoPlayer/videoEvents$textureId'); } 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 4c1f2b67c4fc..21ad972d8e06 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.'); } @@ -100,6 +100,11 @@ abstract class VideoPlayerPlatform { throw UnimplementedError('seekTo() has not been implemented.'); } + /// Sets the playback speed to a [speed] value indicating the playback rate. + Future setPlaybackSpeed(int textureId, double speed) { + throw UnimplementedError('setPlaybackSpeed() has not been implemented.'); + } + /// Gets the video position as [Duration] from the start. Future getPosition(int textureId) { throw UnimplementedError('getPosition() has not been implemented.'); @@ -110,6 +115,11 @@ abstract class VideoPlayerPlatform { throw UnimplementedError('buildView() has not been implemented.'); } + /// Sets the audio mode to mix with other sources + Future setMixWithOthers(bool mixWithOthers) { + throw UnimplementedError('setMixWithOthers() has not been implemented.'); + } + // This method makes sure that VideoPlayer isn't implemented with `implements`. // // See class doc for more details on why implementing this class is forbidden. @@ -136,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. @@ -153,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. @@ -179,7 +195,10 @@ enum DataSourceType { network, /// The video was loaded off of the local filesystem. - file + file, + + /// The video is available via contentUri. Android only. + contentUri, } /// The file format of the given video. @@ -194,7 +213,7 @@ enum VideoFormat { ss, /// Any format other than the other ones defined in this enum. - other + other, } /// Event emitted from the platform implementation. @@ -206,7 +225,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, @@ -218,17 +237,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) { @@ -331,3 +350,16 @@ class DurationRange { @override int get hashCode => start.hashCode ^ end.hashCode; } + +/// [VideoPlayerOptions] can be optionally used to set additional player settings +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 + VideoPlayerOptions({this.mixWithOthers = false}); +} diff --git a/packages/video_player/video_player_platform_interface/pubspec.yaml b/packages/video_player/video_player_platform_interface/pubspec.yaml index b4462679fcfc..35b30793a20f 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.0.2 +version: 4.2.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.1.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 e7bcf26e9d26..f5439b844045 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,24 +1,25 @@ -// 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:ui'; -import 'package:mockito/mockito.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; - +import 'package:video_player_platform_interface/messages.dart'; import 'package:video_player_platform_interface/method_channel_video_player.dart'; +import 'package:video_player_platform_interface/test.dart'; import 'package:video_player_platform_interface/video_player_platform_interface.dart'; -import 'package:video_player_platform_interface/messages.dart'; -class _ApiLogger implements VideoPlayerApiTest { +class _ApiLogger implements TestHostVideoPlayerApi { final List log = []; - TextureMessage textureMessage; - CreateMessage createMessage; - PositionMessage positionMessage; - LoopingMessage loopingMessage; - VolumeMessage volumeMessage; + TextureMessage? textureMessage; + CreateMessage? createMessage; + PositionMessage? positionMessage; + LoopingMessage? loopingMessage; + VolumeMessage? volumeMessage; + PlaybackSpeedMessage? playbackSpeedMessage; + MixWithOthersMessage? mixWithOthersMessage; @override TextureMessage create(CreateMessage arg) { @@ -50,6 +51,12 @@ class _ApiLogger implements VideoPlayerApiTest { textureMessage = arg; } + @override + void setMixWithOthers(MixWithOthersMessage arg) { + log.add('setMixWithOthers'); + mixWithOthersMessage = arg; + } + @override PositionMessage position(TextureMessage arg) { log.add('position'); @@ -74,42 +81,33 @@ class _ApiLogger implements VideoPlayerApiTest { log.add('setVolume'); volumeMessage = arg; } + + @override + void setPlaybackSpeed(PlaybackSpeedMessage arg) { + log.add('setPlaybackSpeed'); + playbackSpeedMessage = arg; + } } void main() { TestWidgetsFlutterBinding.ensureInitialized(); + // Store the initial instance before any tests change it. + final VideoPlayerPlatform initialInstance = VideoPlayerPlatform.instance; + group('$VideoPlayerPlatform', () { test('$MethodChannelVideoPlayer() is the default instance', () { - expect(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(); + expect(initialInstance, isInstanceOf()); }); }); group('$MethodChannelVideoPlayer', () { final MethodChannelVideoPlayer player = MethodChannelVideoPlayer(); - _ApiLogger log; + late _ApiLogger log; setUp(() { log = _ApiLogger(); - VideoPlayerApiTestSetup(log); + TestHostVideoPlayerApi.setup(log); }); test('init', () async { @@ -123,160 +121,183 @@ 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); + + await player.setMixWithOthers(false); + expect(log.log.last, 'setMixWithOthers'); + expect(log.mixWithOthersMessage?.mixWithOthers, false); }); test('setVolume', () async { await player.setVolume(1, 0.7); expect(log.log.last, 'setVolume'); - expect(log.volumeMessage.textureId, 1); - expect(log.volumeMessage.volume, 0.7); + expect(log.volumeMessage?.textureId, 1); + expect(log.volumeMessage?.volume, 0.7); + }); + + test('setPlaybackSpeed', () async { + await player.setPlaybackSpeed(1, 1.5); + expect(log.log.last, 'setPlaybackSpeed'); + expect(log.playbackSpeedMessage?.textureId, 1); + expect(log.playbackSpeedMessage?.speed, 1.5); }); test('seekTo', () async { await player.seekTo(1, const Duration(milliseconds: 12345)); expect(log.log.last, 'seekTo'); - expect(log.positionMessage.textureId, 1); - expect(log.positionMessage.position, 12345); + 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( + _ambiguate(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 _ambiguate(ServicesBinding.instance) + ?.defaultBinaryMessenger + .handlePlatformMessage( + "flutter.io/videoPlayer/videoEvents123", + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'initialized', + 'duration': 98765, + 'width': 1920, + 'height': 1080, + }), + (ByteData? data) {}); + + await _ambiguate(ServicesBinding.instance) + ?.defaultBinaryMessenger + .handlePlatformMessage( + "flutter.io/videoPlayer/videoEvents123", + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'completed', + }), + (ByteData? data) {}); + + await _ambiguate(ServicesBinding.instance) + ?.defaultBinaryMessenger + .handlePlatformMessage( + "flutter.io/videoPlayer/videoEvents123", + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'bufferingUpdate', + 'values': >[ + [0, 1234], + [1235, 4000], + ], + }), + (ByteData? data) {}); + + await _ambiguate(ServicesBinding.instance) + ?.defaultBinaryMessenger + .handlePlatformMessage( + "flutter.io/videoPlayer/videoEvents123", + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'bufferingStart', + }), + (ByteData? data) {}); + + await _ambiguate(ServicesBinding.instance) + ?.defaultBinaryMessenger + .handlePlatformMessage( + "flutter.io/videoPlayer/videoEvents123", + const StandardMethodCodec() + .encodeSuccessEnvelope({ + 'event': 'bufferingEnd', + }), + (ByteData? data) {}); return const StandardMethodCodec().encodeSuccessEnvelope(null); } else if (methodCall.method == 'cancel') { @@ -314,7 +335,9 @@ void main() { }); } -class ImplementsVideoPlayerPlatform extends Mock - implements VideoPlayerPlatform {} - -class ExtendsVideoPlayerPlatform extends VideoPlayerPlatform {} +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +// TODO(ianh): Remove this once we roll stable in late 2021. +T? _ambiguate(T? value) => value; diff --git a/packages/video_player/video_player_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 a8e76b6d2d9f..4eb7c9d610b5 100644 --- a/packages/video_player/video_player_web/CHANGELOG.md +++ b/packages/video_player/video_player_web/CHANGELOG.md @@ -1,3 +1,48 @@ +## 2.0.4 + +* Adopt `video_player_platform_interface` 4.2 and opt out of `contentUri` data source. + +## 2.0.3 + +* Add `implements` to pubspec. + +## 2.0.2 + +* Updated installation instructions in README. + +## 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. + +## 0.1.3+2 + +* Allow users to set the 'muted' attribute on video elements by setting their volume to 0. +* Do not parse URIs on 'network' videos to not break blobs (Safari). + +## 0.1.3+1 + +* Remove Android folder from `video_player_web`. + ## 0.1.3 * Updated video_player_platform_interface, bumped minimum Dart version to 2.1.0. diff --git a/packages/video_player/video_player_web/LICENSE b/packages/video_player/video_player_web/LICENSE index c89293372cf3..c6823b81eb84 100644 --- a/packages/video_player/video_player_web/LICENSE +++ b/packages/video_player/video_player_web/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/video_player/video_player_web/README.md b/packages/video_player/video_player_web/README.md index 216b926bf26e..85e55ebcbe80 100644 --- a/packages/video_player/video_player_web/README.md +++ b/packages/video_player/video_player_web/README.md @@ -2,30 +2,11 @@ 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`. - -No further modifications to your `pubspec.yaml` should be required in a recent enough version of Flutter (`>=1.12.13+hotfix.4`): - -```yaml -... -dependencies: - ... - video_player: ^0.10.5 - ... -``` - -Once you have the correct `video_player` dependency in your pubspec, you should -be able to use `package:video_player` as normal, even from your web code. +This package is [endorsed](https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin), +which means you can simply use `video_player` +normally. This package will be automatically included in your app when you do. ## dart:io @@ -35,6 +16,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/android/.gitignore b/packages/video_player/video_player_web/android/.gitignore deleted file mode 100644 index c6cbe562a427..000000000000 --- a/packages/video_player/video_player_web/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/video_player/video_player_web/android/build.gradle b/packages/video_player/video_player_web/android/build.gradle deleted file mode 100644 index 34ef5bcf8f1c..000000000000 --- a/packages/video_player/video_player_web/android/build.gradle +++ /dev/null @@ -1,33 +0,0 @@ -group 'io.flutter.plugins.video_player_web' -version '1.0' - -buildscript { - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' - } -} - -rootProject.allprojects { - repositories { - google() - jcenter() - } -} - -apply plugin: 'com.android.library' - -android { - compileSdkVersion 28 - - defaultConfig { - minSdkVersion 16 - } - lintOptions { - disable 'InvalidPackage' - } -} diff --git a/packages/video_player/video_player_web/android/gradle.properties b/packages/video_player/video_player_web/android/gradle.properties deleted file mode 100644 index 7be3d8b46841..000000000000 --- a/packages/video_player/video_player_web/android/gradle.properties +++ /dev/null @@ -1,2 +0,0 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true diff --git a/packages/video_player/video_player_web/android/gradle/wrapper/gradle-wrapper.properties b/packages/video_player/video_player_web/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 019065d1d650..000000000000 --- a/packages/video_player/video_player_web/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/video_player/video_player_web/android/settings.gradle b/packages/video_player/video_player_web/android/settings.gradle deleted file mode 100644 index 51bc4f0ca6a8..000000000000 --- a/packages/video_player/video_player_web/android/settings.gradle +++ /dev/null @@ -1 +0,0 @@ -rootProject.name = 'video_player_web' diff --git a/packages/video_player/video_player_web/android/src/main/AndroidManifest.xml b/packages/video_player/video_player_web/android/src/main/AndroidManifest.xml deleted file mode 100644 index f178d4473d89..000000000000 --- a/packages/video_player/video_player_web/android/src/main/AndroidManifest.xml +++ /dev/null @@ -1,3 +0,0 @@ - - diff --git a/packages/video_player/video_player_web/android/src/main/java/io/flutter/plugins/video_player_web/VideoPlayerWebPlugin.java b/packages/video_player/video_player_web/android/src/main/java/io/flutter/plugins/video_player_web/VideoPlayerWebPlugin.java deleted file mode 100644 index e69bcbadb160..000000000000 --- a/packages/video_player/video_player_web/android/src/main/java/io/flutter/plugins/video_player_web/VideoPlayerWebPlugin.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.flutter.plugins.video_player_web; - -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.plugin.common.PluginRegistry.Registrar; - -/** VideoPlayerWebPlugin */ -public class VideoPlayerWebPlugin implements FlutterPlugin { - @Override - public void onAttachedToEngine(FlutterPluginBinding flutterPluginBinding) {} - - public static void registerWith(Registrar registrar) {} - - @Override - public void onDetachedFromEngine(FlutterPluginBinding binding) {} -} 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..8a6e74b107ea --- /dev/null +++ b/packages/video_player/video_player_web/example/README.md @@ -0,0 +1,9 @@ +# Testing + +This package uses `package:integration_test` to run its tests in a web browser. + +See [Plugin Tests > Web Tests](https://github.com/flutter/flutter/wiki/Plugin-Tests#web-tests) +in the Flutter wiki for instructions to setup and run the tests in this package. + +Check [flutter.dev > Integration testing](https://flutter.dev/docs/testing/integration-tests) +for more info. \ No newline at end of file diff --git a/packages/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..2a830c9c573d --- /dev/null +++ b/packages/video_player/video_player_web/example/integration_test/video_player_web_test.dart @@ -0,0 +1,168 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '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('cannot create from content URI', (WidgetTester tester) async { + expect( + VideoPlayerPlatform.instance.create( + DataSource( + sourceType: DataSourceType.contentUri, + uri: 'content://video', + ), + ), + 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 039c3ce65a7e..612d22d2eb3f 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,24 +70,27 @@ class VideoPlayerPlugin extends VideoPlayerPlatform { final int textureId = _textureCounter; _textureCounter++; - Uri uri; + late String uri; switch (dataSource.sourceType) { case DataSourceType.network: - uri = Uri.parse(dataSource.uri); + // Do NOT modify the incoming uri, it can be a Blob, and Safari doesn't + // like blobs that have changed. + 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 = Uri.parse(assetUrl); + uri = assetUrl; break; case DataSourceType.file: return Future.error(UnimplementedError( 'web implementation of video_player cannot play local files')); + case DataSourceType.contentUri: + return Future.error(UnimplementedError( + 'web implementation of video_player cannot play content uri')); } final _VideoPlayer player = _VideoPlayer( @@ -99,66 +106,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); } @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 Uri uri; + 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.toString() + ..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); @@ -167,22 +203,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)); }); } @@ -218,9 +270,21 @@ class _VideoPlayer { } void setVolume(double value) { + // TODO: Do we need to expose a "muted" API? https://github.com/flutter/flutter/issues/60721 + if (value > 0.0) { + videoElement.muted = false; + } else { + videoElement.muted = true; + } videoElement.volume = value; } + void setPlaybackSpeed(double speed) { + assert(speed > 0); + + videoElement.playbackRate = speed; + } + void seekTo(Duration position) { videoElement.currentTime = position.inMilliseconds.toDouble() / 1000; } @@ -237,8 +301,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 aae18ca9915c..b401673c628d 100644 --- a/packages/video_player/video_player_web/pubspec.yaml +++ b/packages/video_player/video_player_web/pubspec.yaml @@ -1,13 +1,16 @@ 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.3 +description: Web platform implementation of video_player. +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.4 + +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" flutter: plugin: + implements: video_player platforms: web: pluginClass: VideoPlayerPlugin @@ -18,16 +21,10 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - meta: ^1.1.7 - video_player_platform_interface: ^2.0.0 + meta: ^1.3.0 + video_player_platform_interface: ^4.2.0 dev_dependencies: flutter_test: sdk: flutter - video_player: - path: ../video_player - 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/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 ef6dc028c529..000000000000 --- a/packages/video_player/video_player_web/test/video_player_web_test.dart +++ /dev/null @@ -1,133 +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 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/CHANGELOG.md b/packages/webview_flutter/CHANGELOG.md deleted file mode 100644 index 36dc43fd0337..000000000000 --- a/packages/webview_flutter/CHANGELOG.md +++ /dev/null @@ -1,324 +0,0 @@ -## 0.3.21 - -* Enable programmatic scrolling using Android's WebView.scrollTo & iOS WKWebView.scrollView.contentOffset. - -## 0.3.20+2 - -* Fix CocoaPods podspec lint warnings. - -## 0.3.20+1 - -* OCMock module import -> #import, unit tests compile generated as library. -* Fix select drop down crash on old Android tablets (https://github.com/flutter/flutter/issues/54164). - -## 0.3.20 - -* Added support for receiving web resource loading errors. See `WebView.onWebResourceError`. - -## 0.3.19+10 - -* Replace deprecated `getFlutterEngine` call on Android. - -## 0.3.19+9 - -* Remove example app's iOS workspace settings. - -## 0.3.19+8 - -* Make the pedantic dev_dependency explicit. - -## 0.3.19+7 - -* Remove the Flutter SDK constraint upper bound. - -## 0.3.19+6 - -* Enable opening links that target the "_blank" window (links open in same window). - -## 0.3.19+5 - -* On iOS, always keep contentInsets of the WebView to be 0. -* Fix XCTest case to follow XCTest naming convention. - -## 0.3.19+4 - -* On iOS, fix the scroll view content inset is automatically adjusted. After the fix, the content position of the WebView is customizable by Flutter. -* Fix an iOS 13 bug where the scroll indicator shows at random location. - -## 0.3.19+3 - -* Setup XCTests. - -## 0.3.19+2 - -* Migrate from deprecated BinaryMessages to ServicesBinding.instance.defaultBinaryMessenger. - -## 0.3.19+1 - -* Raise min Flutter SDK requirement to the latest stable. v2 embedding apps no - longer need to special case their Flutter SDK requirement like they have - since v0.3.15+3. - -## 0.3.19 - -* Add setting for iOS to allow gesture based navigation. - -## 0.3.18+1 - -* Be explicit that keyboard is not ready for production in README.md. - -## 0.3.18 - -* Add support for onPageStarted event. -* Remove the deprecated `author:` field from pubspec.yaml -* Migrate to the new pubspec platforms manifest. -* Require Flutter SDK 1.10.0 or greater. - -## 0.3.17 - -* Fix pedantic lint errors. Added missing documentation and awaited some futures - in tests and the example app. - -## 0.3.16 - -* Add support for async NavigationDelegates. Synchronous NavigationDelegates - should still continue to function without any change in behavior. - -## 0.3.15+3 - -* Re-land support for the v2 Android embedding. This correctly sets the minimum - SDK to the latest stable and avoid any compile errors. *WARNING:* the V2 - embedding itself still requires the current Flutter master channel - (flutter/flutter@1d4d63a) for text input to work properly on all Android - versions. - -## 0.3.15+2 - -* Remove AndroidX warnings. - -## 0.3.15+1 - -* Revert the prior embedding support add since it requires an API that hasn't - rolled to stable. - -## 0.3.15 - -* Add support for the v2 Android embedding. This shouldn't affect existing - functionality. Plugin authors who use the V2 embedding can now register the - plugin and expect that it correctly responds to app lifecycle changes. - -## 0.3.14+2 - -* Define clang module for iOS. - -## 0.3.14+1 - -* Allow underscores anywhere for Javascript Channel name. - -## 0.3.14 - -* Added a getTitle getter to WebViewController. - -## 0.3.13 - -* Add an optional `userAgent` property to set a custom User Agent. - -## 0.3.12+1 - -* Temporarily revert getTitle (doing this as a patch bump shortly after publishing). - -## 0.3.12 - -* Added a getTitle getter to WebViewController. - -## 0.3.11+6 - -* Calling destroy on Android webview when flutter webview is getting disposed. - -## 0.3.11+5 - -* Reduce compiler warnings regarding iOS9 compatibility by moving a single - method back into a `@available` block. - -## 0.3.11+4 - -* Removed noisy log messages on iOS. - -## 0.3.11+3 - -* Apply the display listeners workaround that was shipped in 0.3.11+1 on - all Android versions prior to P. - -## 0.3.11+2 - -* Add fix for input connection being dropped after a screen resize on certain - Android devices. - -## 0.3.11+1 - -* Work around a bug in old Android WebView versions that was causing a crash - when resizing the webview on old devices. - -## 0.3.11 - -* Add an initialAutoMediaPlaybackPolicy setting for controlling how auto media - playback is restricted. - -## 0.3.10+5 - -* Add dependency on `androidx.annotation:annotation:1.0.0`. - -## 0.3.10+4 - -* Add keyboard text to README. - -## 0.3.10+3 - -* Don't log an unknown setting key error for 'debuggingEnabled' on iOS. - -## 0.3.10+2 - -* Fix InputConnection being lost when combined with route transitions. - -## 0.3.10+1 - -* Add support for simultaenous Flutter `TextInput` and WebView text fields. - -## 0.3.10 - -* Add partial WebView keyboard support for Android versions prior to N. Support - for UIs that also have Flutter `TextInput` fields is still pending. This basic - support currently only works with Flutter `master`. The keyboard will still - appear when it previously did not when run with older versions of Flutter. But - if the WebView is resized while showing the keyboard the text field will need - to be focused multiple times for any input to be registered. - -## 0.3.9+2 - -* Update Dart code to conform to current Dart formatter. - -## 0.3.9+1 - -* Add missing template type parameter to `invokeMethod` calls. -* Bump minimum Flutter version to 1.5.0. -* Replace invokeMethod with invokeMapMethod wherever necessary. - -## 0.3.9 - -* Allow external packages to provide webview implementations for new platforms. - -## 0.3.8+1 - -* Suppress deprecation warning for BinaryMessages. See: https://github.com/flutter/flutter/issues/33446 - -## 0.3.8 - -* Add `debuggingEnabled` property. - -## 0.3.7+1 - -* Fix an issue where JavaScriptChannel messages weren't sent from the platform thread on Android. - -## 0.3.7 - -* Fix loadUrlWithHeaders flaky test. - -## 0.3.6+1 - -* Remove un-used method params in webview\_flutter - -## 0.3.6 - -* Add an optional `headers` field to the controller. - -## 0.3.5+5 - -* Fixed error in documentation of `javascriptChannels`. - -## 0.3.5+4 - -* Fix bugs in the example app by updating it to use a `StatefulWidget`. - -## 0.3.5+3 - -* Make sure to post javascript channel messages from the platform thread. - -## 0.3.5+2 - -* Fix crash from `NavigationDelegate` on later versions of Android. - -## 0.3.5+1 - -* Fix a bug where updates to onPageFinished were ignored. - -## 0.3.5 - -* Added an onPageFinished callback. - -## 0.3.4 - -* Support specifying navigation delegates that can prevent navigations from being executed. - -## 0.3.3+2 - -* Exclude LongPress handler from semantics tree since it does nothing. - -## 0.3.3+1 - -* Fixed a memory leak on Android - the WebView was not properly disposed. - -## 0.3.3 - -* Add clearCache method to WebView controller. - -## 0.3.2+1 - -* Log a more detailed warning at build time about the previous AndroidX - migration. - -## 0.3.2 - -* Added CookieManager to interface with WebView cookies. Currently has the ability to clear cookies. - -## 0.3.1 - -* Added JavaScript channels to facilitate message passing from JavaScript code running inside - the WebView to the Flutter app's Dart code. - -## 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.0 - -* Added a evaluateJavascript method to WebView controller. -* (BREAKING CHANGE) Renamed the `JavaScriptMode` enum to `JavascriptMode`, and the WebView `javasScriptMode` parameter to `javascriptMode`. - -## 0.1.2 - -* Added a reload method to the WebView controller. - -## 0.1.1 - -* Added a `currentUrl` accessor for the WebView controller to look up what URL - is being displayed. - -## 0.1.0+1 - -* Fix null crash when initialUrl is unset on iOS. - -## 0.1.0 - -* Add goBack, goForward, canGoBack, and canGoForward methods to the WebView controller. - -## 0.0.1+1 - -* Fix case for "FLTWebViewFlutterPlugin" (iOS was failing to buld on case-sensitive file systems). - -## 0.0.1 - -* Initial release. diff --git a/packages/webview_flutter/LICENSE b/packages/webview_flutter/LICENSE deleted file mode 100644 index 8940a4be1b58..000000000000 --- a/packages/webview_flutter/LICENSE +++ /dev/null @@ -1,27 +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/webview_flutter/README.md b/packages/webview_flutter/README.md deleted file mode 100644 index c86993a15a09..000000000000 --- a/packages/webview_flutter/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# WebView for Flutter (Developers Preview) - -[![pub package](https://img.shields.io/pub/v/webview_flutter.svg)](https://pub.dartlang.org/packages/webview_flutter) - -A Flutter plugin that provides a WebView widget. - -On iOS the WebView widget is backed by a [WKWebView](https://developer.apple.com/documentation/webkit/wkwebview); -On Android the WebView widget is backed by a [WebView](https://developer.android.com/reference/android/webkit/WebView). - -## Developers Preview Status -The plugin relies on Flutter's new mechanism for embedding Android and iOS views. -As that mechanism is currently in a developers preview, this plugin should also be -considered a developers preview. - -Known issues are tagged with the [platform-views](https://github.com/flutter/flutter/labels/a%3A%20platform-views) and/or [webview](https://github.com/flutter/flutter/labels/p%3A%20webview) labels. - -To use this plugin on iOS you need to opt-in for the embedded views preview by -adding a boolean property to the app's `Info.plist` file, with the key `io.flutter.embedded_views_preview` -and the value `YES`. - -## Keyboard support - not ready for production use -Keyboard support within webviews is experimental. The Android version relies on some low-level knobs that have not been well tested -on a broad spectrum of devices yet, and therefore **it is not recommended to rely on webview keyboard in production apps yet**. -See the [webview-keyboard](https://github.com/flutter/flutter/issues?q=is%3Aopen+is%3Aissue+label%3A%22p%3A+webview-keyboard%22) for known issues with keyboard input. - -## Setup - -### iOS -Opt-in to the embedded views preview by adding a boolean property to the app's `Info.plist` file -with the key `io.flutter.embedded_views_preview` and the value `YES`. - -## Usage -Add `webview_flutter` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). - -You can now include a WebView widget in your widget tree. -See the WebView widget's Dartdoc for more details on how to use the widget. 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 deleted file mode 100644 index 893badc0e175..000000000000 --- a/packages/webview_flutter/android/build.gradle +++ /dev/null @@ -1,39 +0,0 @@ -group 'io.flutter.plugins.webviewflutter' -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 'androidx.webkit:webkit:1.0.0' - } -} 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 deleted file mode 100644 index f9659d9873f4..000000000000 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java +++ /dev/null @@ -1,363 +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. - -package io.flutter.plugins.webviewflutter; - -import android.annotation.TargetApi; -import android.content.Context; -import android.hardware.display.DisplayManager; -import android.os.Build; -import android.os.Handler; -import android.view.View; -import android.webkit.WebStorage; -import android.webkit.WebViewClient; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugin.platform.PlatformView; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -public class FlutterWebView implements PlatformView, MethodCallHandler { - private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames"; - private final InputAwareWebView webView; - private final MethodChannel methodChannel; - private final FlutterWebViewClient flutterWebViewClient; - private final Handler platformThreadHandler; - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) - @SuppressWarnings("unchecked") - FlutterWebView( - final Context context, - BinaryMessenger messenger, - int id, - Map params, - View containerView) { - - DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy(); - DisplayManager displayManager = - (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); - displayListenerProxy.onPreWebViewInitialization(displayManager); - webView = new InputAwareWebView(context, containerView); - displayListenerProxy.onPostWebViewInitialization(displayManager); - - platformThreadHandler = new Handler(context.getMainLooper()); - // Allow local storage. - webView.getSettings().setDomStorageEnabled(true); - webView.getSettings().setJavaScriptCanOpenWindowsAutomatically(true); - - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/webview_" + id); - methodChannel.setMethodCallHandler(this); - - flutterWebViewClient = new FlutterWebViewClient(methodChannel); - applySettings((Map) params.get("settings")); - - if (params.containsKey(JS_CHANNEL_NAMES_FIELD)) { - registerJavaScriptChannelNames((List) params.get(JS_CHANNEL_NAMES_FIELD)); - } - - updateAutoMediaPlaybackPolicy((Integer) params.get("autoMediaPlaybackPolicy")); - if (params.containsKey("userAgent")) { - String userAgent = (String) params.get("userAgent"); - updateUserAgent(userAgent); - } - if (params.containsKey("initialUrl")) { - String url = (String) params.get("initialUrl"); - webView.loadUrl(url); - } - } - - @Override - public View getView() { - return webView; - } - - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable. - public void onInputConnectionUnlocked() { - webView.unlockInputConnection(); - } - - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once flutter/engine#9727 rolls to stable. - public void onInputConnectionLocked() { - webView.lockInputConnection(); - } - - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once stable passes v1.10.9. - public void onFlutterViewAttached(View flutterView) { - webView.setContainerView(flutterView); - } - - // @Override - // This is overriding a method that hasn't rolled into stable Flutter yet. Including the - // annotation would cause compile time failures in versions of Flutter too old to include the new - // method. However leaving it raw like this means that the method will be ignored in old versions - // of Flutter but used as an override anyway wherever it's actually defined. - // TODO(mklim): Add the @Override annotation once stable passes v1.10.9. - public void onFlutterViewDetached() { - webView.setContainerView(null); - } - - @Override - public void onMethodCall(MethodCall methodCall, Result result) { - switch (methodCall.method) { - case "loadUrl": - loadUrl(methodCall, result); - break; - case "updateSettings": - updateSettings(methodCall, result); - break; - case "canGoBack": - canGoBack(result); - break; - case "canGoForward": - canGoForward(result); - break; - case "goBack": - goBack(result); - break; - case "goForward": - goForward(result); - break; - case "reload": - reload(result); - break; - case "currentUrl": - currentUrl(result); - break; - case "evaluateJavascript": - evaluateJavaScript(methodCall, result); - break; - case "addJavascriptChannels": - addJavaScriptChannels(methodCall, result); - break; - case "removeJavascriptChannels": - removeJavaScriptChannels(methodCall, result); - break; - case "clearCache": - clearCache(result); - break; - case "getTitle": - getTitle(result); - break; - case "scrollTo": - scrollTo(methodCall, result); - break; - case "scrollBy": - scrollBy(methodCall, result); - break; - case "getScrollX": - getScrollX(result); - break; - case "getScrollY": - getScrollY(result); - break; - default: - result.notImplemented(); - } - } - - @SuppressWarnings("unchecked") - private void loadUrl(MethodCall methodCall, Result result) { - Map request = (Map) methodCall.arguments; - String url = (String) request.get("url"); - Map headers = (Map) request.get("headers"); - if (headers == null) { - headers = Collections.emptyMap(); - } - webView.loadUrl(url, headers); - result.success(null); - } - - private void canGoBack(Result result) { - result.success(webView.canGoBack()); - } - - private void canGoForward(Result result) { - result.success(webView.canGoForward()); - } - - private void goBack(Result result) { - if (webView.canGoBack()) { - webView.goBack(); - } - result.success(null); - } - - private void goForward(Result result) { - if (webView.canGoForward()) { - webView.goForward(); - } - result.success(null); - } - - private void reload(Result result) { - webView.reload(); - result.success(null); - } - - private void currentUrl(Result result) { - result.success(webView.getUrl()); - } - - @SuppressWarnings("unchecked") - private void updateSettings(MethodCall methodCall, Result result) { - applySettings((Map) methodCall.arguments); - result.success(null); - } - - @TargetApi(Build.VERSION_CODES.KITKAT) - private void evaluateJavaScript(MethodCall methodCall, final Result result) { - String jsString = (String) methodCall.arguments; - if (jsString == null) { - throw new UnsupportedOperationException("JavaScript string cannot be null"); - } - webView.evaluateJavascript( - jsString, - new android.webkit.ValueCallback() { - @Override - public void onReceiveValue(String value) { - result.success(value); - } - }); - } - - @SuppressWarnings("unchecked") - private void addJavaScriptChannels(MethodCall methodCall, Result result) { - List channelNames = (List) methodCall.arguments; - registerJavaScriptChannelNames(channelNames); - result.success(null); - } - - @SuppressWarnings("unchecked") - private void removeJavaScriptChannels(MethodCall methodCall, Result result) { - List channelNames = (List) methodCall.arguments; - for (String channelName : channelNames) { - webView.removeJavascriptInterface(channelName); - } - result.success(null); - } - - private void clearCache(Result result) { - webView.clearCache(true); - WebStorage.getInstance().deleteAllData(); - result.success(null); - } - - private void getTitle(Result result) { - result.success(webView.getTitle()); - } - - private void scrollTo(MethodCall methodCall, Result result) { - Map request = (Map) methodCall.arguments; - int x = (int) request.get("x"); - int y = (int) request.get("y"); - - webView.scrollTo(x, y); - - result.success(null); - } - - private void scrollBy(MethodCall methodCall, Result result) { - Map request = (Map) methodCall.arguments; - int x = (int) request.get("x"); - int y = (int) request.get("y"); - - webView.scrollBy(x, y); - result.success(null); - } - - private void getScrollX(Result result) { - result.success(webView.getScrollX()); - } - - private void getScrollY(Result result) { - result.success(webView.getScrollY()); - } - - private void applySettings(Map settings) { - for (String key : settings.keySet()) { - switch (key) { - case "jsMode": - updateJsMode((Integer) settings.get(key)); - break; - case "hasNavigationDelegate": - final boolean hasNavigationDelegate = (boolean) settings.get(key); - - final WebViewClient webViewClient = - flutterWebViewClient.createWebViewClient(hasNavigationDelegate); - - webView.setWebViewClient(webViewClient); - break; - case "debuggingEnabled": - final boolean debuggingEnabled = (boolean) settings.get(key); - - webView.setWebContentsDebuggingEnabled(debuggingEnabled); - break; - case "gestureNavigationEnabled": - break; - case "userAgent": - updateUserAgent((String) settings.get(key)); - break; - default: - throw new IllegalArgumentException("Unknown WebView setting: " + key); - } - } - } - - private void updateJsMode(int mode) { - switch (mode) { - case 0: // disabled - webView.getSettings().setJavaScriptEnabled(false); - break; - case 1: // unrestricted - webView.getSettings().setJavaScriptEnabled(true); - break; - default: - throw new IllegalArgumentException("Trying to set unknown JavaScript mode: " + mode); - } - } - - private void updateAutoMediaPlaybackPolicy(int mode) { - // This is the index of the AutoMediaPlaybackPolicy enum, index 1 is always_allow, for all - // other values we require a user gesture. - boolean requireUserGesture = mode != 1; - webView.getSettings().setMediaPlaybackRequiresUserGesture(requireUserGesture); - } - - private void registerJavaScriptChannelNames(List channelNames) { - for (String channelName : channelNames) { - webView.addJavascriptInterface( - new JavaScriptChannel(methodChannel, channelName, platformThreadHandler), channelName); - } - } - - private void updateUserAgent(String userAgent) { - webView.getSettings().setUserAgentString(userAgent); - } - - @Override - public void dispose() { - methodChannel.setMethodCallHandler(null); - webView.dispose(); - webView.destroy(); - } -} 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 deleted file mode 100644 index 6fdc36fbe545..000000000000 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFactory.java +++ /dev/null @@ -1,31 +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. - -package io.flutter.plugins.webviewflutter; - -import android.content.Context; -import android.view.View; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.StandardMessageCodec; -import io.flutter.plugin.platform.PlatformView; -import io.flutter.plugin.platform.PlatformViewFactory; -import java.util.Map; - -public final class WebViewFactory extends PlatformViewFactory { - private final BinaryMessenger messenger; - private final View containerView; - - WebViewFactory(BinaryMessenger messenger, View containerView) { - super(StandardMessageCodec.INSTANCE); - this.messenger = messenger; - this.containerView = containerView; - } - - @SuppressWarnings("unchecked") - @Override - public PlatformView create(Context context, int id, Object args) { - Map params = (Map) args; - return new FlutterWebView(context, messenger, id, params, containerView); - } -} diff --git a/packages/webview_flutter/example/README.md b/packages/webview_flutter/example/README.md deleted file mode 100644 index bf2f819e87b3..000000000000 --- a/packages/webview_flutter/example/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# webview_flutter_example - -Demonstrates how to use the webview_flutter plugin. - -## Getting Started - -For help getting started with Flutter, view our online -[documentation](https://flutter.io/). diff --git a/packages/webview_flutter/example/android/app/build.gradle b/packages/webview_flutter/example/android/app/build.gradle deleted file mode 100644 index 706d501c4060..000000000000 --- a/packages/webview_flutter/example/android/app/build.gradle +++ /dev/null @@ -1,62 +0,0 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -apply plugin: 'com.android.application' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -android { - compileSdkVersion 28 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "io.flutter.plugins.webviewflutterexample" - 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:rules:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' -} diff --git a/packages/webview_flutter/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/webview_flutter/example/android/app/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 9a4163a4f5ee..000000000000 --- a/packages/webview_flutter/example/android/app/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-all.zip -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/packages/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 deleted file mode 100644 index fe10c6155e5a..000000000000 --- a/packages/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/EmbeddingV1ActivityTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.flutter.plugins.webviewflutterexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class EmbeddingV1ActivityTest { - @Rule - public ActivityTestRule rule = - new ActivityTestRule<>(EmbeddingV1Activity.class); -} 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 deleted file mode 100644 index 73387b8d1160..000000000000 --- a/packages/webview_flutter/example/android/app/src/androidTestDebug/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.flutter.plugins.webviewflutterexample; - -import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterRunner; -import io.flutter.embedding.android.FlutterActivity; -import org.junit.Rule; -import org.junit.runner.RunWith; - -@RunWith(FlutterRunner.class) -public class MainActivityTest { - @Rule - public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); -} diff --git a/packages/webview_flutter/example/android/app/src/main/AndroidManifest.xml b/packages/webview_flutter/example/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index f895f92bd7a4..000000000000 --- a/packages/webview_flutter/example/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/packages/webview_flutter/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/EmbeddingV1Activity.java b/packages/webview_flutter/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/EmbeddingV1Activity.java deleted file mode 100644 index d0f538e89ce3..000000000000 --- a/packages/webview_flutter/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/EmbeddingV1Activity.java +++ /dev/null @@ -1,20 +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.webviewflutterexample; - -import android.os.Bundle; -import dev.flutter.plugins.e2e.E2EPlugin; -import io.flutter.app.FlutterActivity; -import io.flutter.plugins.webviewflutter.WebViewFlutterPlugin; - -public class EmbeddingV1Activity extends FlutterActivity { - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - E2EPlugin.registerWith(registrarFor("dev.flutter.plugins.e2e.E2EPlugin")); - WebViewFlutterPlugin.registerWith( - registrarFor("io.flutter.plugins.webviewflutter.WebViewFlutterPlugin")); - } -} diff --git a/packages/webview_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/webview_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4b7b09..000000000000 Binary files a/packages/webview_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/packages/webview_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/webview_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17987b79bb8a..000000000000 Binary files a/packages/webview_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/packages/webview_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/webview_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 09d4391482be..000000000000 Binary files a/packages/webview_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/webview_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/webview_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d34e7a..000000000000 Binary files a/packages/webview_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/webview_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/webview_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372eebdb2..000000000000 Binary files a/packages/webview_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/packages/webview_flutter/example/android/app/src/main/res/values/styles.xml b/packages/webview_flutter/example/android/app/src/main/res/values/styles.xml deleted file mode 100644 index 00fa4417cfbe..000000000000 --- a/packages/webview_flutter/example/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - diff --git a/packages/webview_flutter/example/android/build.gradle b/packages/webview_flutter/example/android/build.gradle deleted file mode 100644 index 541636cc492a..000000000000 --- a/packages/webview_flutter/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/webview_flutter/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/webview_flutter/example/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 2819f022f1fd..000000000000 --- a/packages/webview_flutter/example/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Fri Jun 23 08:50:38 CEST 2017 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip diff --git a/packages/webview_flutter/example/ios/Flutter/AppFrameworkInfo.plist b/packages/webview_flutter/example/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 9367d483e44e..000000000000 --- a/packages/webview_flutter/example/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,26 +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 - MinimumOSVersion - 8.0 - - diff --git a/packages/webview_flutter/example/ios/Flutter/Debug.xcconfig b/packages/webview_flutter/example/ios/Flutter/Debug.xcconfig deleted file mode 100644 index e8efba114687..000000000000 --- a/packages/webview_flutter/example/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/webview_flutter/example/ios/Flutter/Release.xcconfig b/packages/webview_flutter/example/ios/Flutter/Release.xcconfig deleted file mode 100644 index 399e9340e6f6..000000000000 --- a/packages/webview_flutter/example/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/packages/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index ef6bc3e620ce..000000000000 --- a/packages/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,616 +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 */; }; - 68BDCAF623C3F97800D9C032 /* FLTWebViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */; }; - 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; - 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; - 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; - 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; - 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - 9D26F6F82D91F92CC095EBA9 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 68BDCAEE23C3F7CB00D9C032 /* 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 */ - 127772EEA7782174BE0D74B5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 68BDCAE923C3F7CB00D9C032 /* webview_flutter_exampleTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = webview_flutter_exampleTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 68BDCAED23C3F7CB00D9C032 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FLTWebViewTests.m; path = ../../../ios/Tests/FLTWebViewTests.m; sourceTree = ""; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; - 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; - 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 = ""; }; - C475C484BD510DD9CB2E403C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 68BDCAE623C3F7CB00D9C032 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 9D26F6F82D91F92CC095EBA9 /* libPods-Runner.a in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 68BDCAEA23C3F7CB00D9C032 /* webview_flutter_exampleTests */ = { - isa = PBXGroup; - children = ( - 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */, - 68BDCAED23C3F7CB00D9C032 /* Info.plist */, - ); - path = webview_flutter_exampleTests; - 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 */, - 68BDCAEA23C3F7CB00D9C032 /* webview_flutter_exampleTests */, - 97C146EF1CF9000F007C117D /* Products */, - C6FFB52F5C2B8A41A7E39DE2 /* Pods */, - B6736FC417BDCCDA377E779D /* Frameworks */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - 68BDCAE923C3F7CB00D9C032 /* webview_flutter_exampleTests.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 = ""; - }; - B6736FC417BDCCDA377E779D /* Frameworks */ = { - isa = PBXGroup; - children = ( - 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */, - ); - name = Frameworks; - sourceTree = ""; - }; - C6FFB52F5C2B8A41A7E39DE2 /* Pods */ = { - isa = PBXGroup; - children = ( - 127772EEA7782174BE0D74B5 /* Pods-Runner.debug.xcconfig */, - C475C484BD510DD9CB2E403C /* Pods-Runner.release.xcconfig */, - ); - name = Pods; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 68BDCAE823C3F7CB00D9C032 /* webview_flutter_exampleTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 68BDCAF223C3F7CB00D9C032 /* Build configuration list for PBXNativeTarget "webview_flutter_exampleTests" */; - buildPhases = ( - 68BDCAE523C3F7CB00D9C032 /* Sources */, - 68BDCAE623C3F7CB00D9C032 /* Frameworks */, - 68BDCAE723C3F7CB00D9C032 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 68BDCAEF23C3F7CB00D9C032 /* PBXTargetDependency */, - ); - name = webview_flutter_exampleTests; - productName = webview_flutter_exampleTests; - productReference = 68BDCAE923C3F7CB00D9C032 /* webview_flutter_exampleTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 97C146ED1CF9000F007C117D /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - B71376B4FB8384EF9D5F3F84 /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - A1F14D6FD37A3C5047F5A5AD /* [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 = { - DefaultBuildSystemTypeForWorkspace = Original; - LastUpgradeCheck = 1030; - 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 */, - 68BDCAE823C3F7CB00D9C032 /* webview_flutter_exampleTests */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 68BDCAE723C3F7CB00D9C032 /* 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\n/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin\n"; - }; - 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"; - }; - A1F14D6FD37A3C5047F5A5AD /* [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; - }; - B71376B4FB8384EF9D5F3F84 /* [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 */ - 68BDCAE523C3F7CB00D9C032 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 68BDCAF623C3F97800D9C032 /* FLTWebViewTests.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 */ - 68BDCAEF23C3F7CB00D9C032 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 68BDCAEE23C3F7CB00D9C032 /* 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 */ - 68BDCAF023C3F7CB00D9C032 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* 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 = webview_flutter_exampleTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.2; - 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.webview-flutter-exampleTests"; - PRODUCT_NAME = "$(TARGET_NAME)"; - TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/Runner"; - }; - name = Debug; - }; - 68BDCAF123C3F7CB00D9C032 /* 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_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_STYLE = Automatic; - GCC_C_LANGUAGE_STANDARD = gnu11; - INFOPLIST_FILE = webview_flutter_exampleTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 13.2; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "com.google.webview-flutter-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; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - 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 = io.flutter.plugins.webviewFlutterExample; - 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)"; - 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 = io.flutter.plugins.webviewFlutterExample; - PRODUCT_NAME = "$(TARGET_NAME)"; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 68BDCAF223C3F7CB00D9C032 /* Build configuration list for PBXNativeTarget "webview_flutter_exampleTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 68BDCAF023C3F7CB00D9C032 /* Debug */, - 68BDCAF123C3F7CB00D9C032 /* 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/webview_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/webview_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 1d526a16ed0f..000000000000 --- a/packages/webview_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/packages/webview_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/webview_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 7bcb47d19031..000000000000 --- a/packages/webview_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,97 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/webview_flutter/example/ios/Runner/AppDelegate.h b/packages/webview_flutter/example/ios/Runner/AppDelegate.h deleted file mode 100644 index d129e6e65e7a..000000000000 --- a/packages/webview_flutter/example/ios/Runner/AppDelegate.h +++ /dev/null @@ -1,10 +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 -#import - -@interface AppDelegate : FlutterAppDelegate - -@end diff --git a/packages/webview_flutter/example/ios/Runner/AppDelegate.m b/packages/webview_flutter/example/ios/Runner/AppDelegate.m deleted file mode 100644 index e5b5ebef5767..000000000000 --- a/packages/webview_flutter/example/ios/Runner/AppDelegate.m +++ /dev/null @@ -1,17 +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. - -#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/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 28c6bf03016f..000000000000 Binary files a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index f091b6b0bca8..000000000000 Binary files a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cde12118dda..000000000000 Binary files a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index d0ef06e7edb8..000000000000 Binary files a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index dcdc2306c285..000000000000 Binary files a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 2ccbfd967d96..000000000000 Binary files a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index c8f9ed8f5cee..000000000000 Binary files a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index a6d6b8609df0..000000000000 Binary files a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index 75b2d164a5a9..000000000000 Binary files a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index c4df70d39da7..000000000000 Binary files a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 6a84f41e14e2..000000000000 Binary files a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/packages/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/webview_flutter/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/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/packages/webview_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/webview_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f2e259c7c939..000000000000 --- a/packages/webview_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/webview_flutter/example/ios/Runner/Base.lproj/Main.storyboard b/packages/webview_flutter/example/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516fb38..000000000000 --- a/packages/webview_flutter/example/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/packages/webview_flutter/example/ios/Runner/Info.plist b/packages/webview_flutter/example/ios/Runner/Info.plist deleted file mode 100644 index 94f5857376a2..000000000000 --- a/packages/webview_flutter/example/ios/Runner/Info.plist +++ /dev/null @@ -1,47 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - webview_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 - - io.flutter.embedded_views_preview - - UIViewControllerBasedStatusBarAppearance - - - diff --git a/packages/webview_flutter/example/ios/Runner/main.m b/packages/webview_flutter/example/ios/Runner/main.m deleted file mode 100644 index bc098e4e00a4..000000000000 --- a/packages/webview_flutter/example/ios/Runner/main.m +++ /dev/null @@ -1,13 +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 -#import -#import "AppDelegate.h" - -int main(int argc, char* argv[]) { - @autoreleasepool { - return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); - } -} diff --git a/packages/webview_flutter/example/lib/main.dart b/packages/webview_flutter/example/lib/main.dart deleted file mode 100644 index 59c87a25dedf..000000000000 --- a/packages/webview_flutter/example/lib/main.dart +++ /dev/null @@ -1,338 +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. - -// ignore_for_file: public_member_api_docs - -import 'dart:async'; -import 'dart:convert'; -import 'package:flutter/material.dart'; -import 'package:webview_flutter/webview_flutter.dart'; - -void main() => runApp(MaterialApp(home: WebViewExample())); - -const String kNavigationExamplePage = ''' - -Navigation Delegate Example - -

      -The navigation delegate is set to block navigation to the youtube website. -

      - - - -'''; - -class WebViewExample extends StatefulWidget { - @override - _WebViewExampleState createState() => _WebViewExampleState(); -} - -class _WebViewExampleState extends State { - final Completer _controller = - Completer(); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Flutter WebView example'), - // This drop down menu demonstrates that Flutter widgets can be shown over the web view. - actions: [ - NavigationControls(_controller.future), - SampleMenu(_controller.future), - ], - ), - // We're using a Builder here so we have a context that is below the Scaffold - // to allow calling Scaffold.of(context) so we can show a snackbar. - body: Builder(builder: (BuildContext context) { - return WebView( - initialUrl: 'https://flutter.dev', - javascriptMode: JavascriptMode.unrestricted, - onWebViewCreated: (WebViewController webViewController) { - _controller.complete(webViewController); - }, - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // ignore: prefer_collection_literals - javascriptChannels: [ - _toasterJavascriptChannel(context), - ].toSet(), - navigationDelegate: (NavigationRequest request) { - if (request.url.startsWith('https://www.youtube.com/')) { - print('blocking navigation to $request}'); - return NavigationDecision.prevent; - } - print('allowing navigation to $request'); - return NavigationDecision.navigate; - }, - onPageStarted: (String url) { - print('Page started loading: $url'); - }, - onPageFinished: (String url) { - print('Page finished loading: $url'); - }, - gestureNavigationEnabled: true, - ); - }), - floatingActionButton: favoriteButton(), - ); - } - - JavascriptChannel _toasterJavascriptChannel(BuildContext context) { - return JavascriptChannel( - name: 'Toaster', - onMessageReceived: (JavascriptMessage message) { - Scaffold.of(context).showSnackBar( - SnackBar(content: Text(message.message)), - ); - }); - } - - Widget favoriteButton() { - return FutureBuilder( - future: _controller.future, - builder: (BuildContext context, - AsyncSnapshot controller) { - if (controller.hasData) { - return FloatingActionButton( - onPressed: () async { - final String url = await controller.data.currentUrl(); - Scaffold.of(context).showSnackBar( - SnackBar(content: Text('Favorited $url')), - ); - }, - child: const Icon(Icons.favorite), - ); - } - return Container(); - }); - } -} - -enum MenuOptions { - showUserAgent, - listCookies, - clearCookies, - addToCache, - listCache, - clearCache, - navigationDelegate, -} - -class SampleMenu extends StatelessWidget { - SampleMenu(this.controller); - - final Future controller; - final CookieManager cookieManager = CookieManager(); - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: controller, - builder: - (BuildContext context, AsyncSnapshot controller) { - return PopupMenuButton( - onSelected: (MenuOptions value) { - switch (value) { - case MenuOptions.showUserAgent: - _onShowUserAgent(controller.data, context); - break; - case MenuOptions.listCookies: - _onListCookies(controller.data, context); - break; - case MenuOptions.clearCookies: - _onClearCookies(context); - break; - case MenuOptions.addToCache: - _onAddToCache(controller.data, context); - break; - case MenuOptions.listCache: - _onListCache(controller.data, context); - break; - case MenuOptions.clearCache: - _onClearCache(controller.data, context); - break; - case MenuOptions.navigationDelegate: - _onNavigationDelegateExample(controller.data, context); - break; - } - }, - itemBuilder: (BuildContext context) => >[ - PopupMenuItem( - value: MenuOptions.showUserAgent, - child: const Text('Show user agent'), - enabled: controller.hasData, - ), - const PopupMenuItem( - value: MenuOptions.listCookies, - child: Text('List cookies'), - ), - const PopupMenuItem( - value: MenuOptions.clearCookies, - child: Text('Clear cookies'), - ), - const PopupMenuItem( - value: MenuOptions.addToCache, - child: Text('Add to cache'), - ), - const PopupMenuItem( - value: MenuOptions.listCache, - child: Text('List cache'), - ), - const PopupMenuItem( - value: MenuOptions.clearCache, - child: Text('Clear cache'), - ), - const PopupMenuItem( - value: MenuOptions.navigationDelegate, - child: Text('Navigation Delegate example'), - ), - ], - ); - }, - ); - } - - void _onShowUserAgent( - WebViewController controller, BuildContext context) async { - // Send a message with the user agent string to the Toaster JavaScript channel we registered - // with the WebView. - await controller.evaluateJavascript( - 'Toaster.postMessage("User Agent: " + navigator.userAgent);'); - } - - void _onListCookies( - WebViewController controller, BuildContext context) async { - final String cookies = - await controller.evaluateJavascript('document.cookie'); - Scaffold.of(context).showSnackBar(SnackBar( - content: Column( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - const Text('Cookies:'), - _getCookieList(cookies), - ], - ), - )); - } - - void _onAddToCache(WebViewController controller, BuildContext context) async { - await controller.evaluateJavascript( - 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); - Scaffold.of(context).showSnackBar(const SnackBar( - content: Text('Added a test entry to cache.'), - )); - } - - void _onListCache(WebViewController controller, BuildContext context) async { - await controller.evaluateJavascript('caches.keys()' - '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' - '.then((caches) => Toaster.postMessage(caches))'); - } - - void _onClearCache(WebViewController controller, BuildContext context) async { - await controller.clearCache(); - Scaffold.of(context).showSnackBar(const SnackBar( - content: Text("Cache cleared."), - )); - } - - void _onClearCookies(BuildContext context) async { - final bool hadCookies = await cookieManager.clearCookies(); - String message = 'There were cookies. Now, they are gone!'; - if (!hadCookies) { - message = 'There are no cookies.'; - } - Scaffold.of(context).showSnackBar(SnackBar( - content: Text(message), - )); - } - - void _onNavigationDelegateExample( - WebViewController controller, BuildContext context) async { - final String contentBase64 = - base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); - await controller.loadUrl('data:text/html;base64,$contentBase64'); - } - - Widget _getCookieList(String cookies) { - if (cookies == null || cookies == '""') { - return Container(); - } - final List cookieList = cookies.split(';'); - final Iterable cookieWidgets = - cookieList.map((String cookie) => Text(cookie)); - return Column( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: cookieWidgets.toList(), - ); - } -} - -class NavigationControls extends StatelessWidget { - const NavigationControls(this._webViewControllerFuture) - : assert(_webViewControllerFuture != null); - - final Future _webViewControllerFuture; - - @override - Widget build(BuildContext context) { - return FutureBuilder( - future: _webViewControllerFuture, - builder: - (BuildContext context, AsyncSnapshot snapshot) { - final bool webViewReady = - snapshot.connectionState == ConnectionState.done; - final WebViewController controller = snapshot.data; - return Row( - children: [ - IconButton( - icon: const Icon(Icons.arrow_back_ios), - onPressed: !webViewReady - ? null - : () async { - if (await controller.canGoBack()) { - await controller.goBack(); - } else { - Scaffold.of(context).showSnackBar( - const SnackBar(content: Text("No back history item")), - ); - return; - } - }, - ), - IconButton( - icon: const Icon(Icons.arrow_forward_ios), - onPressed: !webViewReady - ? null - : () async { - if (await controller.canGoForward()) { - await controller.goForward(); - } else { - Scaffold.of(context).showSnackBar( - const SnackBar( - content: Text("No forward history item")), - ); - return; - } - }, - ), - IconButton( - icon: const Icon(Icons.replay), - onPressed: !webViewReady - ? null - : () { - controller.reload(); - }, - ), - ], - ); - }, - ); - } -} diff --git a/packages/webview_flutter/example/pubspec.yaml b/packages/webview_flutter/example/pubspec.yaml deleted file mode 100644 index f5842fc6c163..000000000000 --- a/packages/webview_flutter/example/pubspec.yaml +++ /dev/null @@ -1,24 +0,0 @@ -name: webview_flutter_example -description: Demonstrates how to use the webview_flutter plugin. - -environment: - sdk: ">=2.0.0-dev.68.0 <3.0.0" - -dependencies: - flutter: - sdk: flutter - webview_flutter: - path: ../ - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_driver: - sdk: flutter - e2e: "^0.2.0" - pedantic: ^1.8.0 - -flutter: - uses-material-design: true - assets: - - assets/sample_audio.ogg diff --git a/packages/webview_flutter/example/test_driver/webview_flutter_e2e.dart b/packages/webview_flutter/example/test_driver/webview_flutter_e2e.dart deleted file mode 100644 index a8a2196ebf7c..000000000000 --- a/packages/webview_flutter/example/test_driver/webview_flutter_e2e.dart +++ /dev/null @@ -1,861 +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 'dart:typed_data'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:webview_flutter/platform_interface.dart'; -import 'package:webview_flutter/webview_flutter.dart'; -import 'package:e2e/e2e.dart'; - -void main() { - E2EWidgetsFlutterBinding.ensureInitialized(); - - testWidgets('initalUrl', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: 'https://flutter.dev/', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - final String currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://flutter.dev/'); - }); - - testWidgets('loadUrl', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: 'https://flutter.dev/', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - await controller.loadUrl('https://www.google.com/'); - final String currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.google.com/'); - }); - - // enable this once https://github.com/flutter/flutter/issues/31510 - // is resolved. - testWidgets('loadUrl with headers', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final StreamController pageStarts = StreamController(); - final StreamController pageLoads = StreamController(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: 'https://flutter.dev/', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageStarted: (String url) { - pageStarts.add(url); - }, - onPageFinished: (String url) { - pageLoads.add(url); - }, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - final Map headers = { - 'test_header': 'flutter_test_header' - }; - await controller.loadUrl('https://flutter-header-echo.herokuapp.com/', - headers: headers); - final String currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://flutter-header-echo.herokuapp.com/'); - - await pageStarts.stream.firstWhere((String url) => url == currentUrl); - await pageLoads.stream.firstWhere((String url) => url == currentUrl); - - final String content = await controller - .evaluateJavascript('document.documentElement.innerText'); - expect(content.contains('flutter_test_header'), isTrue); - }); - - testWidgets('JavaScriptChannel', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final Completer pageStarted = Completer(); - final Completer pageLoaded = Completer(); - final List messagesReceived = []; - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - // This is the data URL for: '' - initialUrl: - 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // ignore: prefer_collection_literals - javascriptChannels: [ - JavascriptChannel( - name: 'Echo', - onMessageReceived: (JavascriptMessage message) { - messagesReceived.add(message.message); - }, - ), - ].toSet(), - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; - - expect(messagesReceived, isEmpty); - await controller.evaluateJavascript('Echo.postMessage("hello");'); - expect(messagesReceived, equals(['hello'])); - }); - - testWidgets('resize webview', (WidgetTester tester) async { - final String resizeTest = ''' - - Resize test - - - - - - '''; - final String resizeTestBase64 = - base64Encode(const Utf8Encoder().convert(resizeTest)); - final Completer resizeCompleter = Completer(); - final Completer pageStarted = Completer(); - final Completer pageLoaded = Completer(); - final Completer controllerCompleter = - Completer(); - final GlobalKey key = GlobalKey(); - - final WebView webView = WebView( - key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$resizeTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // ignore: prefer_collection_literals - javascriptChannels: [ - JavascriptChannel( - name: 'Resize', - onMessageReceived: (JavascriptMessage message) { - resizeCompleter.complete(true); - }, - ), - ].toSet(), - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - javascriptMode: JavascriptMode.unrestricted, - ); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Column( - children: [ - SizedBox( - width: 200, - height: 200, - child: webView, - ), - ], - ), - ), - ); - - await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; - - expect(resizeCompleter.isCompleted, false); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: Column( - children: [ - SizedBox( - width: 400, - height: 400, - child: webView, - ), - ], - ), - ), - ); - - await resizeCompleter.future; - }); - - testWidgets('set custom userAgent', (WidgetTester tester) async { - final Completer controllerCompleter1 = - Completer(); - final GlobalKey _globalKey = GlobalKey(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: _globalKey, - initialUrl: 'about:blank', - javascriptMode: JavascriptMode.unrestricted, - userAgent: 'Custom_User_Agent1', - onWebViewCreated: (WebViewController controller) { - controllerCompleter1.complete(controller); - }, - ), - ), - ); - final WebViewController controller1 = await controllerCompleter1.future; - final String customUserAgent1 = await _getUserAgent(controller1); - expect(customUserAgent1, 'Custom_User_Agent1'); - // rebuild the WebView with a different user agent. - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: _globalKey, - initialUrl: 'about:blank', - javascriptMode: JavascriptMode.unrestricted, - userAgent: 'Custom_User_Agent2', - ), - ), - ); - - final String customUserAgent2 = await _getUserAgent(controller1); - expect(customUserAgent2, 'Custom_User_Agent2'); - }); - - testWidgets('use default platform userAgent after webView is rebuilt', - (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final GlobalKey _globalKey = GlobalKey(); - // Build the webView with no user agent to get the default platform user agent. - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: _globalKey, - initialUrl: 'https://flutter.dev/', - javascriptMode: JavascriptMode.unrestricted, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - final String defaultPlatformUserAgent = await _getUserAgent(controller); - // rebuild the WebView with a custom user agent. - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: _globalKey, - initialUrl: 'about:blank', - javascriptMode: JavascriptMode.unrestricted, - userAgent: 'Custom_User_Agent', - ), - ), - ); - final String customUserAgent = await _getUserAgent(controller); - expect(customUserAgent, 'Custom_User_Agent'); - // rebuilds the WebView with no user agent. - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: _globalKey, - initialUrl: 'about:blank', - javascriptMode: JavascriptMode.unrestricted, - ), - ), - ); - - final String customUserAgent2 = await _getUserAgent(controller); - expect(customUserAgent2, defaultPlatformUserAgent); - }); - - group('Media playback policy', () { - String audioTestBase64; - setUpAll(() async { - final ByteData audioData = - await rootBundle.load('assets/sample_audio.ogg'); - final String base64AudioData = - base64Encode(Uint8List.view(audioData.buffer)); - final String audioTest = ''' - - Audio auto play - - - - - - - '''; - audioTestBase64 = base64Encode(const Utf8Encoder().convert(audioTest)); - }); - - testWidgets('Auto media playback', (WidgetTester tester) async { - Completer controllerCompleter = - Completer(); - Completer pageStarted = Completer(); - Completer pageLoaded = Completer(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, - ), - ), - ); - WebViewController controller = await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; - - String isPaused = await controller.evaluateJavascript('isPaused();'); - expect(isPaused, _webviewBool(false)); - - controllerCompleter = Completer(); - pageStarted = Completer(); - pageLoaded = Completer(); - - // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - initialMediaPlaybackPolicy: - AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, - ), - ), - ); - - controller = await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; - - isPaused = await controller.evaluateJavascript('isPaused();'); - expect(isPaused, _webviewBool(true)); - }); - - testWidgets('Changes to initialMediaPlaybackPolocy are ignored', - (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - Completer pageStarted = Completer(); - Completer pageLoaded = Completer(); - - final GlobalKey key = GlobalKey(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; - - String isPaused = await controller.evaluateJavascript('isPaused();'); - expect(isPaused, _webviewBool(false)); - - pageStarted = Completer(); - pageLoaded = Completer(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: key, - initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - initialMediaPlaybackPolicy: - AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, - ), - ), - ); - - await controller.reload(); - - await pageStarted.future; - await pageLoaded.future; - - isPaused = await controller.evaluateJavascript('isPaused();'); - expect(isPaused, _webviewBool(false)); - }); - }); - - testWidgets('getTitle', (WidgetTester tester) async { - final String getTitleTest = ''' - - Some title - - - - - '''; - final String getTitleTestBase64 = - base64Encode(const Utf8Encoder().convert(getTitleTest)); - final Completer pageStarted = Completer(); - final Completer pageLoaded = Completer(); - final Completer controllerCompleter = - Completer(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - onPageStarted: (String url) { - pageStarted.complete(null); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - ), - ), - ); - - final WebViewController controller = await controllerCompleter.future; - await pageStarted.future; - await pageLoaded.future; - - final String title = await controller.getTitle(); - expect(title, 'Some title'); - }); - - group('Programmatic Scroll', () { - testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { - final String scrollTestPage = ''' - - - - - - - - - - - '''; - - final String scrollTestPageBase64 = - base64Encode(const Utf8Encoder().convert(scrollTestPage)); - - final Completer pageLoaded = Completer(); - final Completer controllerCompleter = - Completer(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - initialUrl: - 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - ), - ), - ); - - final WebViewController controller = await controllerCompleter.future; - await pageLoaded.future; - - // Check scrollTo() - const int X_SCROLL = 123; - const int Y_SCROLL = 321; - - await controller.scrollTo(X_SCROLL, Y_SCROLL); - int scrollPosX = await controller.getScrollX(); - int scrollPosY = await controller.getScrollY(); - expect(X_SCROLL, scrollPosX); - expect(Y_SCROLL, scrollPosY); - - // Check scrollBy() (on top of scrollTo()) - await controller.scrollBy(X_SCROLL, Y_SCROLL); - scrollPosX = await controller.getScrollX(); - scrollPosY = await controller.getScrollY(); - expect(X_SCROLL * 2, scrollPosX); - expect(Y_SCROLL * 2, scrollPosY); - }); - }); - - group('NavigationDelegate', () { - final String blankPage = ""; - final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + - base64Encode(const Utf8Encoder().convert(blankPage)); - - testWidgets('can allow requests', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final StreamController pageLoads = - StreamController.broadcast(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: blankPageEncoded, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - navigationDelegate: (NavigationRequest request) { - return (request.url.contains('youtube.com')) - ? NavigationDecision.prevent - : NavigationDecision.navigate; - }, - onPageFinished: (String url) => pageLoads.add(url), - ), - ), - ); - - await pageLoads.stream.first; // Wait for initial page load. - final WebViewController controller = await controllerCompleter.future; - await controller - .evaluateJavascript('location.href = "https://www.google.com/"'); - - await pageLoads.stream.first; // Wait for the next page load. - final String currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.google.com/'); - }); - - testWidgets('onWebResourceError', (WidgetTester tester) async { - final Completer errorCompleter = - Completer(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: 'https://www.notawebsite..com', - onWebResourceError: (WebResourceError error) { - errorCompleter.complete(error); - }, - ), - ), - ); - - final WebResourceError error = await errorCompleter.future; - expect(error, isNotNull); - - if (Platform.isIOS) expect(error.domain, isNotNull); - if (Platform.isAndroid) expect(error.errorType, isNotNull); - }); - - testWidgets('onWebResourceError is not called with valid url', - (WidgetTester tester) async { - final Completer errorCompleter = - Completer(); - - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: - 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', - onWebResourceError: (WebResourceError error) { - errorCompleter.complete(error); - }, - ), - ), - ); - - expect(errorCompleter.future, doesNotComplete); - }); - - testWidgets('can block requests', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final StreamController pageLoads = - StreamController.broadcast(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: blankPageEncoded, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - navigationDelegate: (NavigationRequest request) { - return (request.url.contains('youtube.com')) - ? NavigationDecision.prevent - : NavigationDecision.navigate; - }, - onPageFinished: (String url) => pageLoads.add(url), - ), - ), - ); - - await pageLoads.stream.first; // Wait for initial page load. - final WebViewController controller = await controllerCompleter.future; - await controller - .evaluateJavascript('location.href = "https://www.youtube.com/"'); - - // There should never be any second page load, since our new URL is - // blocked. Still wait for a potential page change for some time in order - // to give the test a chance to fail. - await pageLoads.stream.first - .timeout(const Duration(milliseconds: 500), onTimeout: () => null); - final String currentUrl = await controller.currentUrl(); - expect(currentUrl, isNot(contains('youtube.com'))); - }); - - testWidgets('supports asynchronous decisions', (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final StreamController pageLoads = - StreamController.broadcast(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - initialUrl: blankPageEncoded, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - navigationDelegate: (NavigationRequest request) async { - NavigationDecision decision = NavigationDecision.prevent; - decision = await Future.delayed( - const Duration(milliseconds: 10), - () => NavigationDecision.navigate); - return decision; - }, - onPageFinished: (String url) => pageLoads.add(url), - ), - ), - ); - - await pageLoads.stream.first; // Wait for initial page load. - final WebViewController controller = await controllerCompleter.future; - await controller - .evaluateJavascript('location.href = "https://www.google.com"'); - - await pageLoads.stream.first; // Wait for second page to load. - final String currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://www.google.com/'); - }); - }); - - testWidgets('launches with gestureNavigationEnabled on iOS', - (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: SizedBox( - width: 400, - height: 300, - child: WebView( - key: GlobalKey(), - initialUrl: 'https://flutter.dev/', - gestureNavigationEnabled: true, - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - ), - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - final String currentUrl = await controller.currentUrl(); - expect(currentUrl, 'https://flutter.dev/'); - }); - - testWidgets('target _blank opens in same window', - (WidgetTester tester) async { - final Completer controllerCompleter = - Completer(); - final Completer pageLoaded = Completer(); - await tester.pumpWidget( - Directionality( - textDirection: TextDirection.ltr, - child: WebView( - key: GlobalKey(), - onWebViewCreated: (WebViewController controller) { - controllerCompleter.complete(controller); - }, - javascriptMode: JavascriptMode.unrestricted, - onPageFinished: (String url) { - pageLoaded.complete(null); - }, - ), - ), - ); - final WebViewController controller = await controllerCompleter.future; - await controller.evaluateJavascript('window.open("about:blank", "_blank")'); - await pageLoaded.future; - final String currentUrl = await controller.currentUrl(); - expect(currentUrl, 'about:blank'); - }); -} - -// JavaScript booleans evaluate to different string values on Android and iOS. -// This utility method returns the string boolean value of the current platform. -String _webviewBool(bool value) { - if (defaultTargetPlatform == TargetPlatform.iOS) { - return value ? '1' : '0'; - } - return value ? 'true' : 'false'; -} - -/// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. -Future _getUserAgent(WebViewController controller) async { - if (defaultTargetPlatform == TargetPlatform.iOS) { - return await controller.evaluateJavascript('navigator.userAgent;'); - } - return jsonDecode( - await controller.evaluateJavascript('navigator.userAgent;')); -} diff --git a/packages/webview_flutter/example/test_driver/webview_flutter_e2e_test.dart b/packages/webview_flutter/example/test_driver/webview_flutter_e2e_test.dart deleted file mode 100644 index ccd716607d60..000000000000 --- a/packages/webview_flutter/example/test_driver/webview_flutter_e2e_test.dart +++ /dev/null @@ -1,16 +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:io'; - -import 'package:flutter_driver/flutter_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); -} diff --git a/packages/webview_flutter/ios/Assets/.gitkeep b/packages/webview_flutter/ios/Assets/.gitkeep deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/packages/webview_flutter/ios/Tests/FLTWebViewTests.m b/packages/webview_flutter/ios/Tests/FLTWebViewTests.m deleted file mode 100644 index 40b79e356389..000000000000 --- a/packages/webview_flutter/ios/Tests/FLTWebViewTests.m +++ /dev/null @@ -1,91 +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 webview_flutter; - -// OCMock library doesn't generate a valid modulemap. -#import - -static bool feq(CGFloat a, CGFloat b) { return fabs(b - a) < FLT_EPSILON; } - -@interface FLTWebViewTests : XCTestCase - -@property(strong, nonatomic) NSObject *mockBinaryMessenger; - -@end - -@implementation FLTWebViewTests - -- (void)setUp { - [super setUp]; - self.mockBinaryMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); -} - -- (void)testCanInitFLTWebViewController { - FLTWebViewController *controller = - [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) - viewIdentifier:1 - arguments:nil - binaryMessenger:self.mockBinaryMessenger]; - XCTAssertNotNil(controller); -} - -- (void)testCanInitFLTWebViewFactory { - FLTWebViewFactory *factory = - [[FLTWebViewFactory alloc] initWithMessenger:self.mockBinaryMessenger]; - XCTAssertNotNil(factory); -} - -- (void)webViewContentInsetBehaviorShouldBeNeverOnIOS11 { - if (@available(iOS 11, *)) { - FLTWebViewController *controller = - [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) - viewIdentifier:1 - arguments:nil - binaryMessenger:self.mockBinaryMessenger]; - UIView *view = controller.view; - XCTAssertTrue([view isKindOfClass:WKWebView.class]); - WKWebView *webView = (WKWebView *)view; - XCTAssertEqual(webView.scrollView.contentInsetAdjustmentBehavior, - UIScrollViewContentInsetAdjustmentNever); - } -} - -- (void)testWebViewScrollIndicatorAticautomaticallyAdjustsScrollIndicatorInsetsShouldbeNoOnIOS13 { - if (@available(iOS 13, *)) { - FLTWebViewController *controller = - [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) - viewIdentifier:1 - arguments:nil - binaryMessenger:self.mockBinaryMessenger]; - UIView *view = controller.view; - XCTAssertTrue([view isKindOfClass:WKWebView.class]); - WKWebView *webView = (WKWebView *)view; - XCTAssertFalse(webView.scrollView.automaticallyAdjustsScrollIndicatorInsets); - } -} - -- (void)testContentInsetsSumAlwaysZeroAfterSetFrame { - FLTWKWebView *webView = [[FLTWKWebView alloc] initWithFrame:CGRectMake(0, 0, 300, 400)]; - webView.scrollView.contentInset = UIEdgeInsetsMake(0, 0, 300, 0); - XCTAssertFalse(UIEdgeInsetsEqualToEdgeInsets(webView.scrollView.contentInset, UIEdgeInsetsZero)); - webView.frame = CGRectMake(0, 0, 300, 200); - XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(webView.scrollView.contentInset, UIEdgeInsetsZero)); - XCTAssertTrue(CGRectEqualToRect(webView.frame, CGRectMake(0, 0, 300, 200))); - - if (@available(iOS 11, *)) { - // After iOS 11, we need to make sure the contentInset compensates the adjustedContentInset. - UIScrollView *partialMockScrollView = OCMPartialMock(webView.scrollView); - UIEdgeInsets insetToAdjust = UIEdgeInsetsMake(0, 0, 300, 0); - OCMStub(partialMockScrollView.adjustedContentInset).andReturn(insetToAdjust); - XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(webView.scrollView.contentInset, UIEdgeInsetsZero)); - webView.frame = CGRectMake(0, 0, 300, 100); - XCTAssertTrue(feq(webView.scrollView.contentInset.bottom, -insetToAdjust.bottom)); - XCTAssertTrue(CGRectEqualToRect(webView.frame, CGRectMake(0, 0, 300, 100))); - } -} - -@end diff --git a/packages/webview_flutter/ios/webview_flutter.podspec b/packages/webview_flutter/ios/webview_flutter.podspec deleted file mode 100644 index 469195ae6449..000000000000 --- a/packages/webview_flutter/ios/webview_flutter.podspec +++ /dev/null @@ -1,28 +0,0 @@ -# -# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html -# -Pod::Spec.new do |s| - s.name = 'webview_flutter' - s.version = '0.0.1' - s.summary = 'A WebView Plugin for Flutter.' - s.description = <<-DESC -A Flutter plugin that provides a WebView widget. -Downloaded by pub (not CocoaPods). - DESC - s.homepage = 'https://github.com/flutter/plugins' - s.license = { :type => 'BSD', :file => '../LICENSE' } - s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } - s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/webview_flutter' } - s.documentation_url = 'https://pub.dev/packages/webview_flutter' - s.source_files = 'Classes/**/*' - s.public_header_files = 'Classes/**/*.h' - s.dependency 'Flutter' - - s.platform = :ios, '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/webview_flutter/lib/platform_interface.dart b/packages/webview_flutter/lib/platform_interface.dart deleted file mode 100644 index 238d25b9c677..000000000000 --- a/packages/webview_flutter/lib/platform_interface.dart +++ /dev/null @@ -1,515 +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/gestures.dart'; -import 'package:flutter/widgets.dart'; - -import 'webview_flutter.dart'; - -/// Interface for callbacks made by [WebViewPlatformController]. -/// -/// The webview plugin implements this class, and passes an instance to the [WebViewPlatformController]. -/// [WebViewPlatformController] is notifying this handler on events that happened on the platform's webview. -abstract class WebViewPlatformCallbacksHandler { - /// Invoked by [WebViewPlatformController] when a JavaScript channel message is received. - void onJavaScriptChannelMessage(String channel, String message); - - /// Invoked by [WebViewPlatformController] when a navigation request is pending. - /// - /// If true is returned the navigation is allowed, otherwise it is blocked. - FutureOr onNavigationRequest({String url, bool isForMainFrame}); - - /// Invoked by [WebViewPlatformController] when a page has started loading. - void onPageStarted(String url); - - /// Invoked by [WebViewPlatformController] when a page has finished loading. - void onPageFinished(String url); - - /// Report web resource loading error to the host application. - void onWebResourceError(WebResourceError error); -} - -/// Possible error type categorizations used by [WebResourceError]. -enum WebResourceErrorType { - /// User authentication failed on server. - authentication, - - /// Malformed URL. - badUrl, - - /// Failed to connect to the server. - connect, - - /// Failed to perform SSL handshake. - failedSslHandshake, - - /// Generic file error. - file, - - /// File not found. - fileNotFound, - - /// Server or proxy hostname lookup failed. - hostLookup, - - /// Failed to read or write to the server. - io, - - /// User authentication failed on proxy. - proxyAuthentication, - - /// Too many redirects. - redirectLoop, - - /// Connection timed out. - timeout, - - /// Too many requests during this load. - tooManyRequests, - - /// Generic error. - unknown, - - /// Resource load was canceled by Safe Browsing. - unsafeResource, - - /// Unsupported authentication scheme (not basic or digest). - unsupportedAuthScheme, - - /// Unsupported URI scheme. - unsupportedScheme, - - /// The web content process was terminated. - webContentProcessTerminated, - - /// The web view was invalidated. - webViewInvalidated, - - /// A JavaScript exception occurred. - javaScriptExceptionOccurred, - - /// The result of JavaScript execution could not be returned. - javaScriptResultTypeIsUnsupported, -} - -/// Error returned in `WebView.onWebResourceError` when a web resource loading error has occurred. -class WebResourceError { - /// Creates a new [WebResourceError] - /// - /// A user should not need to instantiate this class, but will receive one in - /// [WebResourceErrorCallback]. - WebResourceError({ - @required this.errorCode, - @required this.description, - this.domain, - this.errorType, - }) : assert(errorCode != null), - assert(description != null); - - /// Raw code of the error from the respective platform. - /// - /// On Android, the error code will be a constant from a - /// [WebViewClient](https://developer.android.com/reference/android/webkit/WebViewClient#summary) and - /// will have a corresponding [errorType]. - /// - /// On iOS, the error code will be a constant from `NSError.code` in - /// Objective-C. See - /// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html - /// for more information on error handling on iOS. Some possible error codes - /// can be found at https://developer.apple.com/documentation/webkit/wkerrorcode?language=objc. - final int errorCode; - - /// The domain of where to find the error code. - /// - /// This field is only available on iOS and represents a "domain" from where - /// the [errorCode] is from. This value is taken directly from an `NSError` - /// in Objective-C. See - /// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html - /// for more information on error handling on iOS. - final String domain; - - /// Description of the error that can be used to communicate the problem to the user. - final String description; - - /// The type this error can be categorized as. - /// - /// This will never be `null` on Android, but can be `null` on iOS. - final WebResourceErrorType errorType; -} - -/// Interface for talking to the webview's platform implementation. -/// -/// An instance implementing this interface is passed to the `onWebViewPlatformCreated` callback that is -/// passed to [WebViewPlatformBuilder#onWebViewPlatformCreated]. -/// -/// Platform implementations that live in a separate package should extend this class rather than -/// implement it as webview_flutter does not consider newly added methods to be breaking changes. -/// Extending this class (using `extends`) ensures that the subclass will get the default -/// implementation, while platform implementations that `implements` this interface will be broken -/// by newly added [WebViewPlatformController] methods. -abstract class WebViewPlatformController { - /// Creates a new WebViewPlatform. - /// - /// Callbacks made by the WebView will be delegated to `handler`. - /// - /// The `handler` parameter must not be null. - WebViewPlatformController(WebViewPlatformCallbacksHandler handler); - - /// Loads the specified URL. - /// - /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will - /// be added as key value pairs of HTTP headers for the request. - /// - /// `url` must not be null. - /// - /// Throws an ArgumentError if `url` is not a valid URL string. - Future loadUrl( - String url, - Map headers, - ) { - throw UnimplementedError( - "WebView loadUrl is not implemented on the current platform"); - } - - /// Updates the webview settings. - /// - /// Any non null field in `settings` will be set as the new setting value. - /// All null fields in `settings` are ignored. - Future updateSettings(WebSettings setting) { - throw UnimplementedError( - "WebView updateSettings is not implemented on the current platform"); - } - - /// Accessor to the current URL that the WebView is displaying. - /// - /// If no URL was ever loaded, returns `null`. - Future currentUrl() { - throw UnimplementedError( - "WebView currentUrl is not implemented on the current platform"); - } - - /// Checks whether there's a back history item. - Future canGoBack() { - throw UnimplementedError( - "WebView canGoBack is not implemented on the current platform"); - } - - /// Checks whether there's a forward history item. - Future canGoForward() { - throw UnimplementedError( - "WebView canGoForward is not implemented on the current platform"); - } - - /// Goes back in the history of this WebView. - /// - /// If there is no back history item this is a no-op. - Future goBack() { - throw UnimplementedError( - "WebView goBack is not implemented on the current platform"); - } - - /// Goes forward in the history of this WebView. - /// - /// If there is no forward history item this is a no-op. - Future goForward() { - throw UnimplementedError( - "WebView goForward is not implemented on the current platform"); - } - - /// Reloads the current URL. - Future reload() { - throw UnimplementedError( - "WebView reload is not implemented on the current platform"); - } - - /// Clears all caches used by the [WebView]. - /// - /// The following caches are cleared: - /// 1. Browser HTTP Cache. - /// 2. [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api) caches. - /// These are not yet supported in iOS WkWebView. Service workers tend to use this cache. - /// 3. Application cache. - /// 4. Local Storage. - Future clearCache() { - throw UnimplementedError( - "WebView clearCache is not implemented on the current platform"); - } - - /// Evaluates a JavaScript expression in the context of the current page. - /// - /// The Future completes with an error if a JavaScript error occurred, or if the type of the - /// evaluated expression is not supported(e.g on iOS not all non primitive type can be evaluated). - Future evaluateJavascript(String javascriptString) { - throw UnimplementedError( - "WebView evaluateJavascript is not implemented on the current platform"); - } - - /// Adds new JavaScript channels to the set of enabled channels. - /// - /// For each value in this list the platform's webview should make sure that a corresponding - /// property with a postMessage method is set on `window`. For example for a JavaScript channel - /// named `Foo` it should be possible for JavaScript code executing in the webview to do - /// - /// ```javascript - /// Foo.postMessage('hello'); - /// ``` - /// - /// See also: [CreationParams.javascriptChannelNames]. - Future addJavascriptChannels(Set javascriptChannelNames) { - throw UnimplementedError( - "WebView addJavascriptChannels is not implemented on the current platform"); - } - - /// Removes JavaScript channel names from the set of enabled channels. - /// - /// This disables channels that were previously enabled by [addJavaScriptChannels] or through - /// [CreationParams.javascriptChannelNames]. - Future removeJavascriptChannels(Set javascriptChannelNames) { - throw UnimplementedError( - "WebView removeJavascriptChannels is not implemented on the current platform"); - } - - /// Returns the title of the currently loaded page. - Future getTitle() { - throw UnimplementedError( - "WebView getTitle is not implemented on the current platform"); - } - - /// Set the scrolled position of this view. - /// - /// The parameters `x` and `y` specify the position to scroll to in WebView pixels. - Future scrollTo(int x, int y) { - throw UnimplementedError( - "WebView scrollTo is not implemented on the current platform"); - } - - /// Move the scrolled position of this view. - /// - /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by. - Future scrollBy(int x, int y) { - throw UnimplementedError( - "WebView scrollBy is not implemented on the current platform"); - } - - /// Return the horizontal scroll position of this view. - /// - /// Scroll position is measured from left. - Future getScrollX() { - throw UnimplementedError( - "WebView getScrollX is not implemented on the current platform"); - } - - /// Return the vertical scroll position of this view. - /// - /// Scroll position is measured from top. - Future getScrollY() { - throw UnimplementedError( - "WebView getScrollY is not implemented on the current platform"); - } -} - -/// A single setting for configuring a WebViewPlatform which may be absent. -class WebSetting { - /// Constructs an absent setting instance. - /// - /// The [isPresent] field for the instance will be false. - /// - /// Accessing [value] for an absent instance will throw. - WebSetting.absent() - : _value = null, - isPresent = false; - - /// Constructs a setting of the given `value`. - /// - /// The [isPresent] field for the instance will be true. - WebSetting.of(T value) - : _value = value, - isPresent = true; - - final T _value; - - /// The setting's value. - /// - /// Throws if [WebSetting.isPresent] is false. - T get value { - if (!isPresent) { - throw StateError('Cannot access a value of an absent WebSetting'); - } - assert(isPresent); - return _value; - } - - /// True when this web setting instance contains a value. - /// - /// When false the [WebSetting.value] getter throws. - final bool isPresent; - - @override - bool operator ==(Object other) { - if (other.runtimeType != runtimeType) return false; - final WebSetting typedOther = other; - return typedOther.isPresent == isPresent && typedOther._value == _value; - } - - @override - int get hashCode => hashValues(_value, isPresent); -} - -/// Settings for configuring a WebViewPlatform. -/// -/// Initial settings are passed as part of [CreationParams], settings updates are sent with -/// [WebViewPlatform#updateSettings]. -/// -/// The `userAgent` parameter must not be null. -class WebSettings { - /// Construct an instance with initial settings. Future setting changes can be - /// sent with [WebviewPlatform#updateSettings]. - /// - /// The `userAgent` parameter must not be null. - WebSettings({ - this.javascriptMode, - this.hasNavigationDelegate, - this.debuggingEnabled, - this.gestureNavigationEnabled, - @required this.userAgent, - }) : assert(userAgent != null); - - /// The JavaScript execution mode to be used by the webview. - final JavascriptMode javascriptMode; - - /// Whether the [WebView] has a [NavigationDelegate] set. - final bool hasNavigationDelegate; - - /// Whether to enable the platform's webview content debugging tools. - /// - /// See also: [WebView.debuggingEnabled]. - final bool debuggingEnabled; - - /// The value used for the HTTP `User-Agent:` request header. - /// - /// If [userAgent.value] is null the platform's default user agent should be used. - /// - /// An absent value ([userAgent.isPresent] is false) represents no change to this setting from the - /// last time it was set. - /// - /// See also [WebView.userAgent]. - final WebSetting userAgent; - - /// Whether to allow swipe based navigation in iOS. - /// - /// See also: [WebView.gestureNavigationEnabled] - final bool gestureNavigationEnabled; - - @override - String toString() { - return 'WebSettings(javascriptMode: $javascriptMode, hasNavigationDelegate: $hasNavigationDelegate, debuggingEnabled: $debuggingEnabled, gestureNavigationEnabled: $gestureNavigationEnabled, userAgent: $userAgent)'; - } -} - -/// Configuration to use when creating a new [WebViewPlatformController]. -/// -/// The `autoMediaPlaybackPolicy` parameter must not be null. -class CreationParams { - /// Constructs an instance to use when creating a new - /// [WebViewPlatformController]. - /// - /// The `autoMediaPlaybackPolicy` parameter must not be null. - CreationParams({ - this.initialUrl, - this.webSettings, - this.javascriptChannelNames, - this.userAgent, - this.autoMediaPlaybackPolicy = - AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, - }) : assert(autoMediaPlaybackPolicy != null); - - /// The initialUrl to load in the webview. - /// - /// When null the webview will be created without loading any page. - final String initialUrl; - - /// The initial [WebSettings] for the new webview. - /// - /// This can later be updated with [WebViewPlatformController.updateSettings]. - final WebSettings webSettings; - - /// The initial set of JavaScript channels that are configured for this webview. - /// - /// For each value in this set the platform's webview should make sure that a corresponding - /// property with a postMessage method is set on `window`. For example for a JavaScript channel - /// named `Foo` it should be possible for JavaScript code executing in the webview to do - /// - /// ```javascript - /// Foo.postMessage('hello'); - /// ``` - // TODO(amirh): describe what should happen when postMessage is called once that code is migrated - // to PlatformWebView. - final Set javascriptChannelNames; - - /// The value used for the HTTP User-Agent: request header. - /// - /// When null the platform's webview default is used for the User-Agent header. - final String userAgent; - - /// Which restrictions apply on automatic media playback. - final AutoMediaPlaybackPolicy autoMediaPlaybackPolicy; - - @override - String toString() { - return '$runtimeType(initialUrl: $initialUrl, settings: $webSettings, javascriptChannelNames: $javascriptChannelNames, UserAgent: $userAgent)'; - } -} - -typedef WebViewPlatformCreatedCallback = void Function( - WebViewPlatformController webViewPlatformController); - -/// Interface for a platform implementation of a WebView. -/// -/// [WebView.platform] controls the builder that is used by [WebView]. -/// [AndroidWebViewPlatform] and [CupertinoWebViewPlatform] are the default implementations -/// for Android and iOS respectively. -abstract class WebViewPlatform { - /// Builds a new WebView. - /// - /// Returns a Widget tree that embeds the created webview. - /// - /// `creationParams` are the initial parameters used to setup the webview. - /// - /// `webViewPlatformHandler` will be used for handling callbacks that are made by the created - /// [WebViewPlatformController]. - /// - /// `onWebViewPlatformCreated` will be invoked after the platform specific [WebViewPlatformController] - /// implementation is created with the [WebViewPlatformController] instance as a parameter. - /// - /// `gestureRecognizers` specifies which gestures should be consumed by the web view. - /// It is possible for other gesture recognizers to be competing with the web view on pointer - /// events, e.g if the web view is inside a [ListView] the [ListView] will want to handle - /// vertical drags. The web view will claim gestures that are recognized by any of the - /// recognizers on this list. - /// When `gestureRecognizers` is empty or null, the web view will only handle pointer events for gestures that - /// were not claimed by any other gesture recognizer. - /// - /// `webViewPlatformHandler` must not be null. - Widget build({ - BuildContext context, - // TODO(amirh): convert this to be the actual parameters. - // I'm starting without it as the PR is starting to become pretty big. - // I'll followup with the conversion PR. - CreationParams creationParams, - @required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, - WebViewPlatformCreatedCallback onWebViewPlatformCreated, - Set> gestureRecognizers, - }); - - /// Clears all cookies for all [WebView] instances. - /// - /// Returns true if cookies were present before clearing, else false. - Future clearCookies() { - throw UnimplementedError( - "WebView clearCookies is not implemented on the current platform"); - } -} diff --git a/packages/webview_flutter/lib/src/webview_android.dart b/packages/webview_flutter/lib/src/webview_android.dart deleted file mode 100644 index f7afcc0637a3..000000000000 --- a/packages/webview_flutter/lib/src/webview_android.dart +++ /dev/null @@ -1,63 +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/gestures.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; - -import '../platform_interface.dart'; -import 'webview_method_channel.dart'; - -/// Builds an Android webview. -/// -/// This is used as the default implementation for [WebView.platform] on Android. It uses -/// an [AndroidView] to embed the webview in the widget hierarchy, and uses a method channel to -/// communicate with the platform code. -class AndroidWebView implements WebViewPlatform { - @override - Widget build({ - BuildContext context, - CreationParams creationParams, - @required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, - WebViewPlatformCreatedCallback onWebViewPlatformCreated, - Set> gestureRecognizers, - }) { - assert(webViewPlatformCallbacksHandler != null); - return GestureDetector( - // We prevent text selection by intercepting the long press event. - // This is a temporary stop gap due to issues with text selection on Android: - // https://github.com/flutter/flutter/issues/24585 - the text selection - // dialog is not responding to touch events. - // https://github.com/flutter/flutter/issues/24584 - the text selection - // handles are not showing. - // TODO(amirh): remove this when the issues above are fixed. - onLongPress: () {}, - excludeFromSemantics: true, - child: AndroidView( - viewType: 'plugins.flutter.io/webview', - onPlatformViewCreated: (int id) { - if (onWebViewPlatformCreated == null) { - return; - } - onWebViewPlatformCreated(MethodChannelWebViewPlatform( - id, webViewPlatformCallbacksHandler)); - }, - gestureRecognizers: gestureRecognizers, - // WebView content is not affected by the Android view's layout direction, - // we explicitly set it here so that the widget doesn't require an ambient - // directionality. - layoutDirection: TextDirection.rtl, - creationParams: - MethodChannelWebViewPlatform.creationParamsToMap(creationParams), - creationParamsCodec: const StandardMessageCodec(), - ), - ); - } - - @override - Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); -} diff --git a/packages/webview_flutter/lib/src/webview_cupertino.dart b/packages/webview_flutter/lib/src/webview_cupertino.dart deleted file mode 100644 index 0e84908261e4..000000000000 --- a/packages/webview_flutter/lib/src/webview_cupertino.dart +++ /dev/null @@ -1,47 +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/gestures.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter/widgets.dart'; - -import '../platform_interface.dart'; -import 'webview_method_channel.dart'; - -/// Builds an iOS webview. -/// -/// This is used as the default implementation for [WebView.platform] on iOS. It uses -/// a [UiKitView] to embed the webview in the widget hierarchy, and uses a method channel to -/// communicate with the platform code. -class CupertinoWebView implements WebViewPlatform { - @override - Widget build({ - BuildContext context, - CreationParams creationParams, - @required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, - WebViewPlatformCreatedCallback onWebViewPlatformCreated, - Set> gestureRecognizers, - }) { - return UiKitView( - viewType: 'plugins.flutter.io/webview', - onPlatformViewCreated: (int id) { - if (onWebViewPlatformCreated == null) { - return; - } - onWebViewPlatformCreated( - MethodChannelWebViewPlatform(id, webViewPlatformCallbacksHandler)); - }, - gestureRecognizers: gestureRecognizers, - creationParams: - MethodChannelWebViewPlatform.creationParamsToMap(creationParams), - creationParamsCodec: const StandardMessageCodec(), - ); - } - - @override - Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); -} diff --git a/packages/webview_flutter/lib/src/webview_method_channel.dart b/packages/webview_flutter/lib/src/webview_method_channel.dart deleted file mode 100644 index a75dee3c172b..000000000000 --- a/packages/webview_flutter/lib/src/webview_method_channel.dart +++ /dev/null @@ -1,201 +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 '../platform_interface.dart'; - -/// A [WebViewPlatformController] that uses a method channel to control the webview. -class MethodChannelWebViewPlatform implements WebViewPlatformController { - /// Constructs an instance that will listen for webviews broadcasting to the - /// given [id], using the given [WebViewPlatformCallbacksHandler]. - MethodChannelWebViewPlatform(int id, this._platformCallbacksHandler) - : assert(_platformCallbacksHandler != null), - _channel = MethodChannel('plugins.flutter.io/webview_$id') { - _channel.setMethodCallHandler(_onMethodCall); - } - - final WebViewPlatformCallbacksHandler _platformCallbacksHandler; - - final MethodChannel _channel; - - static const MethodChannel _cookieManagerChannel = - MethodChannel('plugins.flutter.io/cookie_manager'); - - Future _onMethodCall(MethodCall call) async { - switch (call.method) { - case 'javascriptChannelMessage': - final String channel = call.arguments['channel']; - final String message = call.arguments['message']; - _platformCallbacksHandler.onJavaScriptChannelMessage(channel, message); - return true; - case 'navigationRequest': - return await _platformCallbacksHandler.onNavigationRequest( - url: call.arguments['url'], - isForMainFrame: call.arguments['isForMainFrame'], - ); - case 'onPageFinished': - _platformCallbacksHandler.onPageFinished(call.arguments['url']); - return null; - case 'onPageStarted': - _platformCallbacksHandler.onPageStarted(call.arguments['url']); - return null; - case 'onWebResourceError': - _platformCallbacksHandler.onWebResourceError( - WebResourceError( - errorCode: call.arguments['errorCode'], - description: call.arguments['description'], - domain: call.arguments['domain'], - errorType: call.arguments['errorType'] == null - ? null - : WebResourceErrorType.values.firstWhere( - (WebResourceErrorType type) { - return type.toString() == - '$WebResourceErrorType.${call.arguments['errorType']}'; - }, - ), - ), - ); - return null; - } - - throw MissingPluginException( - '${call.method} was invoked but has no handler', - ); - } - - @override - Future loadUrl( - String url, - Map headers, - ) async { - assert(url != null); - return _channel.invokeMethod('loadUrl', { - 'url': url, - 'headers': headers, - }); - } - - @override - Future currentUrl() => _channel.invokeMethod('currentUrl'); - - @override - Future canGoBack() => _channel.invokeMethod("canGoBack"); - - @override - Future canGoForward() => _channel.invokeMethod("canGoForward"); - - @override - Future goBack() => _channel.invokeMethod("goBack"); - - @override - Future goForward() => _channel.invokeMethod("goForward"); - - @override - Future reload() => _channel.invokeMethod("reload"); - - @override - Future clearCache() => _channel.invokeMethod("clearCache"); - - @override - Future updateSettings(WebSettings settings) { - final Map updatesMap = _webSettingsToMap(settings); - if (updatesMap.isEmpty) { - return null; - } - return _channel.invokeMethod('updateSettings', updatesMap); - } - - @override - Future evaluateJavascript(String javascriptString) { - return _channel.invokeMethod( - 'evaluateJavascript', javascriptString); - } - - @override - Future addJavascriptChannels(Set javascriptChannelNames) { - return _channel.invokeMethod( - 'addJavascriptChannels', javascriptChannelNames.toList()); - } - - @override - Future removeJavascriptChannels(Set javascriptChannelNames) { - return _channel.invokeMethod( - 'removeJavascriptChannels', javascriptChannelNames.toList()); - } - - @override - Future getTitle() => _channel.invokeMethod("getTitle"); - - @override - Future scrollTo(int x, int y) { - return _channel.invokeMethod('scrollTo', { - 'x': x, - 'y': y, - }); - } - - @override - Future scrollBy(int x, int y) { - return _channel.invokeMethod('scrollBy', { - 'x': x, - 'y': y, - }); - } - - @override - Future getScrollX() => _channel.invokeMethod("getScrollX"); - - @override - Future getScrollY() => _channel.invokeMethod("getScrollY"); - - /// Method channel implementation for [WebViewPlatform.clearCookies]. - static Future clearCookies() { - return _cookieManagerChannel - .invokeMethod('clearCookies') - .then((dynamic result) => result); - } - - static Map _webSettingsToMap(WebSettings settings) { - final Map map = {}; - void _addIfNonNull(String key, dynamic value) { - if (value == null) { - return; - } - map[key] = value; - } - - void _addSettingIfPresent(String key, WebSetting setting) { - if (!setting.isPresent) { - return; - } - map[key] = setting.value; - } - - _addIfNonNull('jsMode', settings.javascriptMode?.index); - _addIfNonNull('hasNavigationDelegate', settings.hasNavigationDelegate); - _addIfNonNull('debuggingEnabled', settings.debuggingEnabled); - _addIfNonNull( - 'gestureNavigationEnabled', settings.gestureNavigationEnabled); - _addSettingIfPresent('userAgent', settings.userAgent); - return map; - } - - /// Converts a [CreationParams] object to a map as expected by `platform_views` channel. - /// - /// This is used for the `creationParams` argument of the platform views created by - /// [AndroidWebViewBuilder] and [CupertinoWebViewBuilder]. - static Map creationParamsToMap( - CreationParams creationParams) { - return { - 'initialUrl': creationParams.initialUrl, - 'settings': _webSettingsToMap(creationParams.webSettings), - 'javascriptChannelNames': creationParams.javascriptChannelNames.toList(), - 'userAgent': creationParams.userAgent, - 'autoMediaPlaybackPolicy': creationParams.autoMediaPlaybackPolicy.index, - }; - } -} diff --git a/packages/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/lib/webview_flutter.dart deleted file mode 100644 index 2635b0446fa2..000000000000 --- a/packages/webview_flutter/lib/webview_flutter.dart +++ /dev/null @@ -1,738 +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 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/widgets.dart'; - -import 'platform_interface.dart'; -import 'src/webview_android.dart'; -import 'src/webview_cupertino.dart'; - -/// Optional callback invoked when a web view is first created. [controller] is -/// the [WebViewController] for the created web view. -typedef void WebViewCreatedCallback(WebViewController controller); - -/// Describes the state of JavaScript support in a given web view. -enum JavascriptMode { - /// JavaScript execution is disabled. - disabled, - - /// JavaScript execution is not restricted. - unrestricted, -} - -/// A message that was sent by JavaScript code running in a [WebView]. -class JavascriptMessage { - /// Constructs a JavaScript message object. - /// - /// The `message` parameter must not be null. - const JavascriptMessage(this.message) : assert(message != null); - - /// The contents of the message that was sent by the JavaScript code. - final String message; -} - -/// Callback type for handling messages sent from Javascript running in a web view. -typedef void JavascriptMessageHandler(JavascriptMessage message); - -/// Information about a navigation action that is about to be executed. -class NavigationRequest { - NavigationRequest._({this.url, this.isForMainFrame}); - - /// The URL that will be loaded if the navigation is executed. - final String url; - - /// Whether the navigation request is to be loaded as the main frame. - final bool isForMainFrame; - - @override - String toString() { - return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)'; - } -} - -/// A decision on how to handle a navigation request. -enum NavigationDecision { - /// Prevent the navigation from taking place. - prevent, - - /// Allow the navigation to take place. - navigate, -} - -/// Decides how to handle a specific navigation request. -/// -/// The returned [NavigationDecision] determines how the navigation described by -/// `navigation` should be handled. -/// -/// See also: [WebView.navigationDelegate]. -typedef FutureOr NavigationDelegate( - NavigationRequest navigation); - -/// Signature for when a [WebView] has started loading a page. -typedef void PageStartedCallback(String url); - -/// Signature for when a [WebView] has finished loading a page. -typedef void PageFinishedCallback(String url); - -/// Signature for when a [WebView] has failed to load a resource. -typedef void WebResourceErrorCallback(WebResourceError error); - -/// Specifies possible restrictions on automatic media playback. -/// -/// This is typically used in [WebView.initialMediaPlaybackPolicy]. -// The method channel implementation is marshalling this enum to the value's index, so the order -// is important. -enum AutoMediaPlaybackPolicy { - /// Starting any kind of media playback requires a user action. - /// - /// For example: JavaScript code cannot start playing media unless the code was executed - /// as a result of a user action (like a touch event). - require_user_action_for_all_media_types, - - /// Starting any kind of media playback is always allowed. - /// - /// For example: JavaScript code that's triggered when the page is loaded can start playing - /// video or audio. - always_allow, -} - -final RegExp _validChannelNames = RegExp('^[a-zA-Z_][a-zA-Z0-9_]*\$'); - -/// A named channel for receiving messaged from JavaScript code running inside a web view. -class JavascriptChannel { - /// Constructs a Javascript channel. - /// - /// The parameters `name` and `onMessageReceived` must not be null. - JavascriptChannel({ - @required this.name, - @required this.onMessageReceived, - }) : assert(name != null), - assert(onMessageReceived != null), - assert(_validChannelNames.hasMatch(name)); - - /// The channel's name. - /// - /// Passing this channel object as part of a [WebView.javascriptChannels] adds a channel object to - /// the Javascript window object's property named `name`. - /// - /// The name must start with a letter or underscore(_), followed by any combination of those - /// characters plus digits. - /// - /// Note that any JavaScript existing `window` property with this name will be overriden. - /// - /// See also [WebView.javascriptChannels] for more details on the channel registration mechanism. - final String name; - - /// A callback that's invoked when a message is received through the channel. - final JavascriptMessageHandler onMessageReceived; -} - -/// A web view widget for showing html content. -class WebView extends StatefulWidget { - /// Creates a new web view. - /// - /// The web view can be controlled using a `WebViewController` that is passed to the - /// `onWebViewCreated` callback once the web view is created. - /// - /// The `javascriptMode` and `autoMediaPlaybackPolicy` parameters must not be null. - const WebView({ - Key key, - this.onWebViewCreated, - this.initialUrl, - this.javascriptMode = JavascriptMode.disabled, - this.javascriptChannels, - this.navigationDelegate, - this.gestureRecognizers, - this.onPageStarted, - this.onPageFinished, - this.onWebResourceError, - this.debuggingEnabled = false, - this.gestureNavigationEnabled = false, - this.userAgent, - this.initialMediaPlaybackPolicy = - AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, - }) : assert(javascriptMode != null), - assert(initialMediaPlaybackPolicy != null), - super(key: key); - - static WebViewPlatform _platform; - - /// Sets a custom [WebViewPlatform]. - /// - /// This property can be set to use a custom platform implementation for WebViews. - /// - /// Setting `platform` doesn't affect [WebView]s that were already created. - /// - /// The default value is [AndroidWebView] on Android and [CupertinoWebView] on iOS. - static set platform(WebViewPlatform platform) { - _platform = platform; - } - - /// The WebView platform that's used by this WebView. - /// - /// The default value is [AndroidWebView] on Android and [CupertinoWebView] on iOS. - static WebViewPlatform get platform { - if (_platform == null) { - switch (defaultTargetPlatform) { - case TargetPlatform.android: - _platform = AndroidWebView(); - break; - case TargetPlatform.iOS: - _platform = CupertinoWebView(); - break; - default: - throw UnsupportedError( - "Trying to use the default webview implementation for $defaultTargetPlatform but there isn't a default one"); - } - } - return _platform; - } - - /// If not null invoked once the web view is created. - final WebViewCreatedCallback onWebViewCreated; - - /// Which gestures should be consumed by the web view. - /// - /// It is possible for other gesture recognizers to be competing with the web view on pointer - /// events, e.g if the web view is inside a [ListView] the [ListView] will want to handle - /// vertical drags. The web view will claim gestures that are recognized by any of the - /// recognizers on this list. - /// - /// When this set is empty or null, the web view will only handle pointer events for gestures that - /// were not claimed by any other gesture recognizer. - final Set> gestureRecognizers; - - /// The initial URL to load. - final String initialUrl; - - /// Whether Javascript execution is enabled. - final JavascriptMode javascriptMode; - - /// The set of [JavascriptChannel]s available to JavaScript code running in the web view. - /// - /// For each [JavascriptChannel] in the set, a channel object is made available for the - /// JavaScript code in a window property named [JavascriptChannel.name]. - /// The JavaScript code can then call `postMessage` on that object to send a message that will be - /// passed to [JavascriptChannel.onMessageReceived]. - /// - /// For example for the following JavascriptChannel: - /// - /// ```dart - /// JavascriptChannel(name: 'Print', onMessageReceived: (JavascriptMessage message) { print(message.message); }); - /// ``` - /// - /// JavaScript code can call: - /// - /// ```javascript - /// Print.postMessage('Hello'); - /// ``` - /// - /// To asynchronously invoke the message handler which will print the message to standard output. - /// - /// Adding a new JavaScript channel only takes affect after the next page is loaded. - /// - /// Set values must not be null. A [JavascriptChannel.name] cannot be the same for multiple - /// channels in the list. - /// - /// A null value is equivalent to an empty set. - final Set javascriptChannels; - - /// A delegate function that decides how to handle navigation actions. - /// - /// When a navigation is initiated by the WebView (e.g when a user clicks a link) - /// this delegate is called and has to decide how to proceed with the navigation. - /// - /// See [NavigationDecision] for possible decisions the delegate can take. - /// - /// When null all navigation actions are allowed. - /// - /// Caveats on Android: - /// - /// * Navigation actions targeted to the main frame can be intercepted, - /// navigation actions targeted to subframes are allowed regardless of the value - /// returned by this delegate. - /// * Setting a navigationDelegate makes the WebView treat all navigations as if they were - /// triggered by a user gesture, this disables some of Chromium's security mechanisms. - /// A navigationDelegate should only be set when loading trusted content. - /// * On Android WebView versions earlier than 67(most devices running at least Android L+ should have - /// a later version): - /// * When a navigationDelegate is set pages with frames are not properly handled by the - /// webview, and frames will be opened in the main frame. - /// * When a navigationDelegate is set HTTP requests do not include the HTTP referer header. - final NavigationDelegate navigationDelegate; - - /// Invoked when a page starts loading. - final PageStartedCallback onPageStarted; - - /// Invoked when a page has finished loading. - /// - /// This is invoked only for the main frame. - /// - /// When [onPageFinished] is invoked on Android, the page being rendered may - /// not be updated yet. - /// - /// When invoked on iOS or Android, any Javascript code that is embedded - /// directly in the HTML has been loaded and code injected with - /// [WebViewController.evaluateJavascript] can assume this. - final PageFinishedCallback onPageFinished; - - /// Invoked when a web resource has failed to load. - /// - /// This can be called for any resource (iframe, image, etc.), not just for - /// the main page. - final WebResourceErrorCallback onWebResourceError; - - /// Controls whether WebView debugging is enabled. - /// - /// Setting this to true enables [WebView debugging on Android](https://developers.google.com/web/tools/chrome-devtools/remote-debugging/). - /// - /// WebView debugging is enabled by default in dev builds on iOS. - /// - /// To debug WebViews on iOS: - /// - Enable developer options (Open Safari, go to Preferences -> Advanced and make sure "Show Develop Menu in Menubar" is on.) - /// - From the Menu-bar (of Safari) select Develop -> iPhone Simulator -> - /// - /// By default `debuggingEnabled` is false. - final bool debuggingEnabled; - - /// A Boolean value indicating whether horizontal swipe gestures will trigger back-forward list navigations. - /// - /// This only works on iOS. - /// - /// By default `gestureNavigationEnabled` is false. - final bool gestureNavigationEnabled; - - /// The value used for the HTTP User-Agent: request header. - /// - /// When null the platform's webview default is used for the User-Agent header. - /// - /// When the [WebView] is rebuilt with a different `userAgent`, the page reloads and the request uses the new User Agent. - /// - /// When [WebViewController.goBack] is called after changing `userAgent` the previous `userAgent` value is used until the page is reloaded. - /// - /// This field is ignored on iOS versions prior to 9 as the platform does not support a custom - /// user agent. - /// - /// By default `userAgent` is null. - final String userAgent; - - /// Which restrictions apply on automatic media playback. - /// - /// This initial value is applied to the platform's webview upon creation. Any following - /// changes to this parameter are ignored (as long as the state of the [WebView] is preserved). - /// - /// The default policy is [AutoMediaPlaybackPolicy.require_user_action_for_all_media_types]. - final AutoMediaPlaybackPolicy initialMediaPlaybackPolicy; - - @override - State createState() => _WebViewState(); -} - -class _WebViewState extends State { - final Completer _controller = - Completer(); - - _PlatformCallbacksHandler _platformCallbacksHandler; - - @override - Widget build(BuildContext context) { - return WebView.platform.build( - context: context, - onWebViewPlatformCreated: _onWebViewPlatformCreated, - webViewPlatformCallbacksHandler: _platformCallbacksHandler, - gestureRecognizers: widget.gestureRecognizers, - creationParams: _creationParamsfromWidget(widget), - ); - } - - @override - void initState() { - super.initState(); - _assertJavascriptChannelNamesAreUnique(); - _platformCallbacksHandler = _PlatformCallbacksHandler(widget); - } - - @override - void didUpdateWidget(WebView oldWidget) { - super.didUpdateWidget(oldWidget); - _assertJavascriptChannelNamesAreUnique(); - _controller.future.then((WebViewController controller) { - _platformCallbacksHandler._widget = widget; - controller._updateWidget(widget); - }); - } - - void _onWebViewPlatformCreated(WebViewPlatformController webViewPlatform) { - final WebViewController controller = - WebViewController._(widget, webViewPlatform, _platformCallbacksHandler); - _controller.complete(controller); - if (widget.onWebViewCreated != null) { - widget.onWebViewCreated(controller); - } - } - - void _assertJavascriptChannelNamesAreUnique() { - if (widget.javascriptChannels == null || - widget.javascriptChannels.isEmpty) { - return; - } - assert(_extractChannelNames(widget.javascriptChannels).length == - widget.javascriptChannels.length); - } -} - -CreationParams _creationParamsfromWidget(WebView widget) { - return CreationParams( - initialUrl: widget.initialUrl, - webSettings: _webSettingsFromWidget(widget), - javascriptChannelNames: _extractChannelNames(widget.javascriptChannels), - userAgent: widget.userAgent, - autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, - ); -} - -WebSettings _webSettingsFromWidget(WebView widget) { - return WebSettings( - javascriptMode: widget.javascriptMode, - hasNavigationDelegate: widget.navigationDelegate != null, - debuggingEnabled: widget.debuggingEnabled, - gestureNavigationEnabled: widget.gestureNavigationEnabled, - userAgent: WebSetting.of(widget.userAgent), - ); -} - -// This method assumes that no fields in `currentValue` are null. -WebSettings _clearUnchangedWebSettings( - WebSettings currentValue, WebSettings newValue) { - assert(currentValue.javascriptMode != null); - assert(currentValue.hasNavigationDelegate != null); - assert(currentValue.debuggingEnabled != null); - assert(currentValue.userAgent.isPresent); - assert(newValue.javascriptMode != null); - assert(newValue.hasNavigationDelegate != null); - assert(newValue.debuggingEnabled != null); - assert(newValue.userAgent.isPresent); - - JavascriptMode javascriptMode; - bool hasNavigationDelegate; - bool debuggingEnabled; - WebSetting userAgent = WebSetting.absent(); - if (currentValue.javascriptMode != newValue.javascriptMode) { - javascriptMode = newValue.javascriptMode; - } - if (currentValue.hasNavigationDelegate != newValue.hasNavigationDelegate) { - hasNavigationDelegate = newValue.hasNavigationDelegate; - } - if (currentValue.debuggingEnabled != newValue.debuggingEnabled) { - debuggingEnabled = newValue.debuggingEnabled; - } - if (currentValue.userAgent != newValue.userAgent) { - userAgent = newValue.userAgent; - } - - return WebSettings( - javascriptMode: javascriptMode, - hasNavigationDelegate: hasNavigationDelegate, - debuggingEnabled: debuggingEnabled, - userAgent: userAgent, - ); -} - -Set _extractChannelNames(Set channels) { - final Set channelNames = channels == null - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // ignore: prefer_collection_literals - ? Set() - : channels.map((JavascriptChannel channel) => channel.name).toSet(); - return channelNames; -} - -class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { - _PlatformCallbacksHandler(this._widget) { - _updateJavascriptChannelsFromSet(_widget.javascriptChannels); - } - - WebView _widget; - - // Maps a channel name to a channel. - final Map _javascriptChannels = - {}; - - @override - void onJavaScriptChannelMessage(String channel, String message) { - _javascriptChannels[channel].onMessageReceived(JavascriptMessage(message)); - } - - @override - FutureOr onNavigationRequest({String url, bool isForMainFrame}) async { - final NavigationRequest request = - NavigationRequest._(url: url, isForMainFrame: isForMainFrame); - final bool allowNavigation = _widget.navigationDelegate == null || - await _widget.navigationDelegate(request) == - NavigationDecision.navigate; - return allowNavigation; - } - - @override - void onPageStarted(String url) { - if (_widget.onPageStarted != null) { - _widget.onPageStarted(url); - } - } - - @override - void onPageFinished(String url) { - if (_widget.onPageFinished != null) { - _widget.onPageFinished(url); - } - } - - @override - void onWebResourceError(WebResourceError error) { - if (_widget.onWebResourceError != null) { - _widget.onWebResourceError(error); - } - } - - void _updateJavascriptChannelsFromSet(Set channels) { - _javascriptChannels.clear(); - if (channels == null) { - return; - } - for (JavascriptChannel channel in channels) { - _javascriptChannels[channel.name] = channel; - } - } -} - -/// Controls a [WebView]. -/// -/// A [WebViewController] instance can be obtained by setting the [WebView.onWebViewCreated] -/// callback for a [WebView] widget. -class WebViewController { - WebViewController._( - this._widget, - this._webViewPlatformController, - this._platformCallbacksHandler, - ) : assert(_webViewPlatformController != null) { - _settings = _webSettingsFromWidget(_widget); - } - - final WebViewPlatformController _webViewPlatformController; - - final _PlatformCallbacksHandler _platformCallbacksHandler; - - WebSettings _settings; - - WebView _widget; - - /// Loads the specified URL. - /// - /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will - /// be added as key value pairs of HTTP headers for the request. - /// - /// `url` must not be null. - /// - /// Throws an ArgumentError if `url` is not a valid URL string. - Future loadUrl( - String url, { - Map headers, - }) async { - assert(url != null); - _validateUrlString(url); - return _webViewPlatformController.loadUrl(url, headers); - } - - /// Accessor to the current URL that the WebView is displaying. - /// - /// If [WebView.initialUrl] was never specified, returns `null`. - /// Note that this operation is asynchronous, and it is possible that the - /// current URL changes again by the time this function returns (in other - /// words, by the time this future completes, the WebView may be displaying a - /// different URL). - Future currentUrl() { - return _webViewPlatformController.currentUrl(); - } - - /// Checks whether there's a back history item. - /// - /// Note that this operation is asynchronous, and it is possible that the "canGoBack" state has - /// changed by the time the future completed. - Future canGoBack() { - return _webViewPlatformController.canGoBack(); - } - - /// Checks whether there's a forward history item. - /// - /// Note that this operation is asynchronous, and it is possible that the "canGoForward" state has - /// changed by the time the future completed. - Future canGoForward() { - return _webViewPlatformController.canGoForward(); - } - - /// Goes back in the history of this WebView. - /// - /// If there is no back history item this is a no-op. - Future goBack() { - return _webViewPlatformController.goBack(); - } - - /// Goes forward in the history of this WebView. - /// - /// If there is no forward history item this is a no-op. - Future goForward() { - return _webViewPlatformController.goForward(); - } - - /// Reloads the current URL. - Future reload() { - return _webViewPlatformController.reload(); - } - - /// Clears all caches used by the [WebView]. - /// - /// The following caches are cleared: - /// 1. Browser HTTP Cache. - /// 2. [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api) caches. - /// These are not yet supported in iOS WkWebView. Service workers tend to use this cache. - /// 3. Application cache. - /// 4. Local Storage. - /// - /// Note: Calling this method also triggers a reload. - Future clearCache() async { - await _webViewPlatformController.clearCache(); - return reload(); - } - - Future _updateWidget(WebView widget) async { - _widget = widget; - await _updateSettings(_webSettingsFromWidget(widget)); - await _updateJavascriptChannels(widget.javascriptChannels); - } - - Future _updateSettings(WebSettings newSettings) { - final WebSettings update = - _clearUnchangedWebSettings(_settings, newSettings); - _settings = newSettings; - return _webViewPlatformController.updateSettings(update); - } - - Future _updateJavascriptChannels( - Set newChannels) async { - final Set currentChannels = - _platformCallbacksHandler._javascriptChannels.keys.toSet(); - final Set newChannelNames = _extractChannelNames(newChannels); - final Set channelsToAdd = - newChannelNames.difference(currentChannels); - final Set channelsToRemove = - currentChannels.difference(newChannelNames); - if (channelsToRemove.isNotEmpty) { - await _webViewPlatformController - .removeJavascriptChannels(channelsToRemove); - } - if (channelsToAdd.isNotEmpty) { - await _webViewPlatformController.addJavascriptChannels(channelsToAdd); - } - _platformCallbacksHandler._updateJavascriptChannelsFromSet(newChannels); - } - - /// Evaluates a JavaScript expression in the context of the current page. - /// - /// On Android returns the evaluation result as a JSON formatted string. - /// - /// On iOS depending on the value type the return value would be one of: - /// - /// - For primitive JavaScript types: the value string formatted (e.g JavaScript 100 returns '100'). - /// - For JavaScript arrays of supported types: a string formatted NSArray(e.g '(1,2,3), note that the string for NSArray is formatted and might contain newlines and extra spaces.'). - /// - Other non-primitive types are not supported on iOS and will complete the Future with an error. - /// - /// The Future completes with an error if a JavaScript error occurred, or on iOS, if the type of the - /// evaluated expression is not supported as described above. - /// - /// When evaluating Javascript in a [WebView], it is best practice to wait for - /// the [WebView.onPageFinished] callback. This guarantees all the Javascript - /// embedded in the main frame HTML has been loaded. - Future evaluateJavascript(String javascriptString) { - if (_settings.javascriptMode == JavascriptMode.disabled) { - return Future.error(FlutterError( - 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); - } - if (javascriptString == null) { - return Future.error( - ArgumentError('The argument javascriptString must not be null.')); - } - // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. - // https://github.com/flutter/flutter/issues/26431 - // ignore: strong_mode_implicit_dynamic_method - return _webViewPlatformController.evaluateJavascript(javascriptString); - } - - /// Returns the title of the currently loaded page. - Future getTitle() { - return _webViewPlatformController.getTitle(); - } - - /// Sets the WebView's content scroll position. - /// - /// The parameters `x` and `y` specify the scroll position in WebView pixels. - Future scrollTo(int x, int y) { - return _webViewPlatformController.scrollTo(x, y); - } - - /// Move the scrolled position of this view. - /// - /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by horizontally and vertically respectively. - Future scrollBy(int x, int y) { - return _webViewPlatformController.scrollBy(x, y); - } - - /// Return the horizontal scroll position, in WebView pixels, of this view. - /// - /// Scroll position is measured from left. - Future getScrollX() { - return _webViewPlatformController.getScrollX(); - } - - /// Return the vertical scroll position, in WebView pixels, of this view. - /// - /// Scroll position is measured from top. - Future getScrollY() { - return _webViewPlatformController.getScrollY(); - } -} - -/// Manages cookies pertaining to all [WebView]s. -class CookieManager { - /// Creates a [CookieManager] -- returns the instance if it's already been called. - factory CookieManager() { - return _instance ??= CookieManager._(); - } - - CookieManager._(); - - static CookieManager _instance; - - /// Clears all cookies for all [WebView] instances. - /// - /// This is a no op on iOS version smaller than 9. - /// - /// Returns true if cookies were present before clearing, else false. - Future clearCookies() => WebView.platform.clearCookies(); -} - -// Throws an ArgumentError if `url` is not a valid URL string. -void _validateUrlString(String url) { - try { - final Uri uri = Uri.parse(url); - if (uri.scheme.isEmpty) { - throw ArgumentError('Missing scheme in URL string: "$url"'); - } - } on FormatException catch (e) { - throw ArgumentError(e); - } -} diff --git a/packages/webview_flutter/pubspec.yaml b/packages/webview_flutter/pubspec.yaml deleted file mode 100644 index 26e4fbc26edc..000000000000 --- a/packages/webview_flutter/pubspec.yaml +++ /dev/null @@ -1,28 +0,0 @@ -name: webview_flutter -description: A Flutter plugin that provides a WebView widget on Android and iOS. -version: 0.3.21 -homepage: https://github.com/flutter/plugins/tree/master/packages/webview_flutter - -environment: - sdk: ">=2.7.0 <3.0.0" - flutter: ">=1.12.13+hotfix.5" - -dependencies: - flutter: - sdk: flutter - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_driver: - sdk: flutter - pedantic: ^1.8.0 - -flutter: - plugin: - platforms: - android: - package: io.flutter.plugins.webviewflutter - pluginClass: WebViewFlutterPlugin - ios: - pluginClass: FLTWebViewFlutterPlugin diff --git a/packages/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/test/webview_flutter_test.dart deleted file mode 100644 index c7cf46a080d7..000000000000 --- a/packages/webview_flutter/test/webview_flutter_test.dart +++ /dev/null @@ -1,1211 +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:math'; -import 'dart:typed_data'; - -import 'package:flutter/services.dart'; -import 'package:flutter/src/foundation/basic_types.dart'; -import 'package:flutter/src/gestures/recognizer.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:webview_flutter/platform_interface.dart'; -import 'package:webview_flutter/webview_flutter.dart'; - -typedef void VoidCallback(); - -void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - - final _FakePlatformViewsController fakePlatformViewsController = - _FakePlatformViewsController(); - - final _FakeCookieManager _fakeCookieManager = _FakeCookieManager(); - - setUpAll(() { - SystemChannels.platform_views.setMockMethodCallHandler( - fakePlatformViewsController.fakePlatformViewsMethodHandler); - SystemChannels.platform - .setMockMethodCallHandler(_fakeCookieManager.onMethodCall); - }); - - setUp(() { - fakePlatformViewsController.reset(); - _fakeCookieManager.reset(); - }); - - testWidgets('Create WebView', (WidgetTester tester) async { - await tester.pumpWidget(const WebView()); - }); - - testWidgets('Initial url', (WidgetTester tester) async { - WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(await controller.currentUrl(), 'https://youtube.com'); - }); - - testWidgets('Javascript mode', (WidgetTester tester) async { - await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', - javascriptMode: JavascriptMode.unrestricted, - )); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; - - expect(platformWebView.javascriptMode, JavascriptMode.unrestricted); - - await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', - javascriptMode: JavascriptMode.disabled, - )); - expect(platformWebView.javascriptMode, JavascriptMode.disabled); - }); - - testWidgets('Load url', (WidgetTester tester) async { - WebViewController controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - await controller.loadUrl('https://flutter.io'); - - expect(await controller.currentUrl(), 'https://flutter.io'); - }); - - testWidgets('Invalid urls', (WidgetTester tester) async { - WebViewController controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - expect(() => controller.loadUrl(null), throwsA(anything)); - expect(await controller.currentUrl(), isNull); - - expect(() => controller.loadUrl(''), throwsA(anything)); - expect(await controller.currentUrl(), isNull); - - // Missing schema. - expect(() => controller.loadUrl('flutter.io'), throwsA(anything)); - expect(await controller.currentUrl(), isNull); - }); - - testWidgets('Headers in loadUrl', (WidgetTester tester) async { - WebViewController controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - final Map headers = { - 'CACHE-CONTROL': 'ABC' - }; - await controller.loadUrl('https://flutter.io', headers: headers); - expect(await controller.currentUrl(), equals('https://flutter.io')); - }); - - testWidgets("Can't go back before loading a page", - (WidgetTester tester) async { - WebViewController controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - final bool canGoBackNoPageLoaded = await controller.canGoBack(); - - expect(canGoBackNoPageLoaded, false); - }); - - testWidgets("Clear Cache", (WidgetTester tester) async { - WebViewController controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - expect(fakePlatformViewsController.lastCreatedView.hasCache, true); - - await controller.clearCache(); - - expect(fakePlatformViewsController.lastCreatedView.hasCache, false); - }); - - testWidgets("Can't go back with no history", (WidgetTester tester) async { - WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - final bool canGoBackFirstPageLoaded = await controller.canGoBack(); - - expect(canGoBackFirstPageLoaded, false); - }); - - testWidgets('Can go back', (WidgetTester tester) async { - WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - await controller.loadUrl('https://www.google.com'); - final bool canGoBackSecondPageLoaded = await controller.canGoBack(); - - expect(canGoBackSecondPageLoaded, true); - }); - - testWidgets("Can't go forward before loading a page", - (WidgetTester tester) async { - WebViewController controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - final bool canGoForwardNoPageLoaded = await controller.canGoForward(); - - expect(canGoForwardNoPageLoaded, false); - }); - - testWidgets("Can't go forward with no history", (WidgetTester tester) async { - WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - final bool canGoForwardFirstPageLoaded = await controller.canGoForward(); - - expect(canGoForwardFirstPageLoaded, false); - }); - - testWidgets('Can go forward', (WidgetTester tester) async { - WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - await controller.loadUrl('https://youtube.com'); - await controller.goBack(); - final bool canGoForwardFirstPageBacked = await controller.canGoForward(); - - expect(canGoForwardFirstPageBacked, true); - }); - - testWidgets('Go back', (WidgetTester tester) async { - WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - expect(await controller.currentUrl(), 'https://youtube.com'); - - await controller.loadUrl('https://flutter.io'); - - expect(await controller.currentUrl(), 'https://flutter.io'); - - await controller.goBack(); - - expect(await controller.currentUrl(), 'https://youtube.com'); - }); - - testWidgets('Go forward', (WidgetTester tester) async { - WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - expect(await controller.currentUrl(), 'https://youtube.com'); - - await controller.loadUrl('https://flutter.io'); - - expect(await controller.currentUrl(), 'https://flutter.io'); - - await controller.goBack(); - - expect(await controller.currentUrl(), 'https://youtube.com'); - - await controller.goForward(); - - expect(await controller.currentUrl(), 'https://flutter.io'); - }); - - testWidgets('Current URL', (WidgetTester tester) async { - WebViewController controller; - await tester.pumpWidget( - WebView( - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - expect(controller, isNotNull); - - // Test a WebView without an explicitly set first URL. - expect(await controller.currentUrl(), isNull); - - await controller.loadUrl('https://youtube.com'); - expect(await controller.currentUrl(), 'https://youtube.com'); - - await controller.loadUrl('https://flutter.io'); - expect(await controller.currentUrl(), 'https://flutter.io'); - - await controller.goBack(); - expect(await controller.currentUrl(), 'https://youtube.com'); - }); - - testWidgets('Reload url', (WidgetTester tester) async { - WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; - - expect(platformWebView.currentUrl, 'https://flutter.io'); - expect(platformWebView.amountOfReloadsOnCurrentUrl, 0); - - await controller.reload(); - - expect(platformWebView.currentUrl, 'https://flutter.io'); - expect(platformWebView.amountOfReloadsOnCurrentUrl, 1); - - await controller.loadUrl('https://youtube.com'); - - expect(platformWebView.amountOfReloadsOnCurrentUrl, 0); - }); - - testWidgets('evaluate Javascript', (WidgetTester tester) async { - WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - javascriptMode: JavascriptMode.unrestricted, - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - expect( - await controller.evaluateJavascript("fake js string"), "fake js string", - reason: 'should get the argument'); - expect( - () => controller.evaluateJavascript(null), - throwsA(anything), - ); - }); - - testWidgets('evaluate Javascript with JavascriptMode disabled', - (WidgetTester tester) async { - WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://flutter.io', - javascriptMode: JavascriptMode.disabled, - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - expect( - () => controller.evaluateJavascript('fake js string'), - throwsA(anything), - ); - expect( - () => controller.evaluateJavascript(null), - throwsA(anything), - ); - }); - - testWidgets('Cookies can be cleared once', (WidgetTester tester) async { - await tester.pumpWidget( - const WebView( - initialUrl: 'https://flutter.io', - ), - ); - final CookieManager cookieManager = CookieManager(); - final bool hasCookies = await cookieManager.clearCookies(); - expect(hasCookies, true); - }); - - testWidgets('Second cookie clear does not have cookies', - (WidgetTester tester) async { - await tester.pumpWidget( - const WebView( - initialUrl: 'https://flutter.io', - ), - ); - final CookieManager cookieManager = CookieManager(); - final bool hasCookies = await cookieManager.clearCookies(); - expect(hasCookies, true); - final bool hasCookiesSecond = await cookieManager.clearCookies(); - expect(hasCookiesSecond, false); - }); - - testWidgets('Initial JavaScript channels', (WidgetTester tester) async { - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // ignore: prefer_collection_literals - javascriptChannels: [ - JavascriptChannel( - name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), - JavascriptChannel( - name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), - ].toSet(), - ), - ); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; - - expect(platformWebView.javascriptChannelNames, - unorderedEquals(['Tts', 'Alarm'])); - }); - - test('Only valid JavaScript channel names are allowed', () { - final JavascriptMessageHandler noOp = (JavascriptMessage msg) {}; - JavascriptChannel(name: 'Tts1', onMessageReceived: noOp); - JavascriptChannel(name: '_Alarm', onMessageReceived: noOp); - JavascriptChannel(name: 'foo_bar_', onMessageReceived: noOp); - - VoidCallback createChannel(String name) { - return () { - JavascriptChannel(name: name, onMessageReceived: noOp); - }; - } - - expect(createChannel('1Alarm'), throwsAssertionError); - expect(createChannel('foo.bar'), throwsAssertionError); - expect(createChannel(''), throwsAssertionError); - }); - - testWidgets('Unique JavaScript channel names are required', - (WidgetTester tester) async { - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // ignore: prefer_collection_literals - javascriptChannels: [ - JavascriptChannel( - name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), - JavascriptChannel( - name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), - ].toSet(), - ), - ); - expect(tester.takeException(), isNot(null)); - }); - - testWidgets('JavaScript channels update', (WidgetTester tester) async { - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // ignore: prefer_collection_literals - javascriptChannels: [ - JavascriptChannel( - name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), - JavascriptChannel( - name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), - ].toSet(), - ), - ); - - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // ignore: prefer_collection_literals - javascriptChannels: [ - JavascriptChannel( - name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), - JavascriptChannel( - name: 'Alarm2', onMessageReceived: (JavascriptMessage msg) {}), - JavascriptChannel( - name: 'Alarm3', onMessageReceived: (JavascriptMessage msg) {}), - ].toSet(), - ), - ); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; - - expect(platformWebView.javascriptChannelNames, - unorderedEquals(['Tts', 'Alarm2', 'Alarm3'])); - }); - - testWidgets('Remove all JavaScript channels and then add', - (WidgetTester tester) async { - // This covers a specific bug we had where after updating javascriptChannels to null, - // updating it again with a subset of the previously registered channels fails as the - // widget's cache of current channel wasn't properly updated when updating javascriptChannels to - // null. - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // ignore: prefer_collection_literals - javascriptChannels: [ - JavascriptChannel( - name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), - ].toSet(), - ), - ); - - await tester.pumpWidget( - const WebView( - initialUrl: 'https://youtube.com', - ), - ); - - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // ignore: prefer_collection_literals - javascriptChannels: [ - JavascriptChannel( - name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), - ].toSet(), - ), - ); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; - - expect(platformWebView.javascriptChannelNames, - unorderedEquals(['Tts'])); - }); - - testWidgets('JavaScript channel messages', (WidgetTester tester) async { - final List ttsMessagesReceived = []; - final List alarmMessagesReceived = []; - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // ignore: prefer_collection_literals - javascriptChannels: [ - JavascriptChannel( - name: 'Tts', - onMessageReceived: (JavascriptMessage msg) { - ttsMessagesReceived.add(msg.message); - }), - JavascriptChannel( - name: 'Alarm', - onMessageReceived: (JavascriptMessage msg) { - alarmMessagesReceived.add(msg.message); - }), - ].toSet(), - ), - ); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; - - expect(ttsMessagesReceived, isEmpty); - expect(alarmMessagesReceived, isEmpty); - - platformWebView.fakeJavascriptPostMessage('Tts', 'Hello'); - platformWebView.fakeJavascriptPostMessage('Tts', 'World'); - - expect(ttsMessagesReceived, ['Hello', 'World']); - }); - - group('$PageStartedCallback', () { - testWidgets('onPageStarted is not null', (WidgetTester tester) async { - String returnedUrl; - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - onPageStarted: (String url) { - returnedUrl = url; - }, - )); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; - - platformWebView.fakeOnPageStartedCallback(); - - expect(platformWebView.currentUrl, returnedUrl); - }); - - testWidgets('onPageStarted is null', (WidgetTester tester) async { - await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', - onPageStarted: null, - )); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; - - // The platform side will always invoke a call for onPageStarted. This is - // to test that it does not crash on a null callback. - platformWebView.fakeOnPageStartedCallback(); - }); - - testWidgets('onPageStarted changed', (WidgetTester tester) async { - String returnedUrl; - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - onPageStarted: (String url) {}, - )); - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - onPageStarted: (String url) { - returnedUrl = url; - }, - )); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; - - platformWebView.fakeOnPageStartedCallback(); - - expect(platformWebView.currentUrl, returnedUrl); - }); - }); - - group('$PageFinishedCallback', () { - testWidgets('onPageFinished is not null', (WidgetTester tester) async { - String returnedUrl; - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - onPageFinished: (String url) { - returnedUrl = url; - }, - )); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; - - platformWebView.fakeOnPageFinishedCallback(); - - expect(platformWebView.currentUrl, returnedUrl); - }); - - testWidgets('onPageFinished is null', (WidgetTester tester) async { - await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', - onPageFinished: null, - )); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; - - // The platform side will always invoke a call for onPageFinished. This is - // to test that it does not crash on a null callback. - platformWebView.fakeOnPageFinishedCallback(); - }); - - testWidgets('onPageFinished changed', (WidgetTester tester) async { - String returnedUrl; - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - onPageFinished: (String url) {}, - )); - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - onPageFinished: (String url) { - returnedUrl = url; - }, - )); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; - - platformWebView.fakeOnPageFinishedCallback(); - - expect(platformWebView.currentUrl, returnedUrl); - }); - }); - - group('navigationDelegate', () { - testWidgets('hasNavigationDelegate', (WidgetTester tester) async { - await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', - )); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; - - expect(platformWebView.hasNavigationDelegate, false); - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - navigationDelegate: (NavigationRequest r) => null, - )); - - expect(platformWebView.hasNavigationDelegate, true); - }); - - testWidgets('Block navigation', (WidgetTester tester) async { - final List navigationRequests = []; - - await tester.pumpWidget(WebView( - initialUrl: 'https://youtube.com', - navigationDelegate: (NavigationRequest request) { - navigationRequests.add(request); - // Only allow navigating to https://flutter.dev - return request.url == 'https://flutter.dev' - ? NavigationDecision.navigate - : NavigationDecision.prevent; - })); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; - - expect(platformWebView.hasNavigationDelegate, true); - - platformWebView.fakeNavigate('https://www.google.com'); - // The navigation delegate only allows navigation to https://flutter.dev - // so we should still be in https://youtube.com. - expect(platformWebView.currentUrl, 'https://youtube.com'); - expect(navigationRequests.length, 1); - expect(navigationRequests[0].url, 'https://www.google.com'); - expect(navigationRequests[0].isForMainFrame, true); - - platformWebView.fakeNavigate('https://flutter.dev'); - await tester.pump(); - expect(platformWebView.currentUrl, 'https://flutter.dev'); - }); - }); - - group('debuggingEnabled', () { - testWidgets('enable debugging', (WidgetTester tester) async { - await tester.pumpWidget(const WebView( - debuggingEnabled: true, - )); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; - - expect(platformWebView.debuggingEnabled, true); - }); - - testWidgets('defaults to false', (WidgetTester tester) async { - await tester.pumpWidget(const WebView()); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; - - expect(platformWebView.debuggingEnabled, false); - }); - - testWidgets('can be changed', (WidgetTester tester) async { - final GlobalKey key = GlobalKey(); - await tester.pumpWidget(WebView(key: key)); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; - - await tester.pumpWidget(WebView( - key: key, - debuggingEnabled: true, - )); - - expect(platformWebView.debuggingEnabled, true); - - await tester.pumpWidget(WebView( - key: key, - debuggingEnabled: false, - )); - - expect(platformWebView.debuggingEnabled, false); - }); - }); - - group('Custom platform implementation', () { - setUpAll(() { - WebView.platform = MyWebViewPlatform(); - }); - tearDownAll(() { - WebView.platform = null; - }); - - testWidgets('creation', (WidgetTester tester) async { - await tester.pumpWidget( - const WebView( - initialUrl: 'https://youtube.com', - gestureNavigationEnabled: true, - ), - ); - - final MyWebViewPlatform builder = WebView.platform; - final MyWebViewPlatformController platform = builder.lastPlatformBuilt; - - expect( - platform.creationParams, - MatchesCreationParams(CreationParams( - initialUrl: 'https://youtube.com', - webSettings: WebSettings( - javascriptMode: JavascriptMode.disabled, - hasNavigationDelegate: false, - debuggingEnabled: false, - userAgent: WebSetting.of(null), - gestureNavigationEnabled: true, - ), - // TODO(iskakaushik): Remove this when collection literals makes it to stable. - // ignore: prefer_collection_literals - javascriptChannelNames: Set(), - ))); - }); - - testWidgets('loadUrl', (WidgetTester tester) async { - WebViewController controller; - await tester.pumpWidget( - WebView( - initialUrl: 'https://youtube.com', - onWebViewCreated: (WebViewController webViewController) { - controller = webViewController; - }, - ), - ); - - final MyWebViewPlatform builder = WebView.platform; - final MyWebViewPlatformController platform = builder.lastPlatformBuilt; - - final Map headers = { - 'header': 'value', - }; - - await controller.loadUrl('https://google.com', headers: headers); - - expect(platform.lastUrlLoaded, 'https://google.com'); - expect(platform.lastRequestHeaders, headers); - }); - }); - testWidgets('Set UserAgent', (WidgetTester tester) async { - await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', - javascriptMode: JavascriptMode.unrestricted, - )); - - final FakePlatformWebView platformWebView = - fakePlatformViewsController.lastCreatedView; - - expect(platformWebView.userAgent, isNull); - - await tester.pumpWidget(const WebView( - initialUrl: 'https://youtube.com', - javascriptMode: JavascriptMode.unrestricted, - userAgent: 'UA', - )); - - expect(platformWebView.userAgent, 'UA'); - }); -} - -class FakePlatformWebView { - FakePlatformWebView(int id, Map params) { - if (params.containsKey('initialUrl')) { - final String initialUrl = params['initialUrl']; - if (initialUrl != null) { - history.add(initialUrl); - currentPosition++; - } - } - if (params.containsKey('javascriptChannelNames')) { - javascriptChannelNames = - List.from(params['javascriptChannelNames']); - } - javascriptMode = JavascriptMode.values[params['settings']['jsMode']]; - hasNavigationDelegate = - params['settings']['hasNavigationDelegate'] ?? false; - debuggingEnabled = params['settings']['debuggingEnabled']; - userAgent = params['settings']['userAgent']; - channel = MethodChannel( - 'plugins.flutter.io/webview_$id', const StandardMethodCodec()); - channel.setMockMethodCallHandler(onMethodCall); - } - - MethodChannel channel; - - List history = []; - int currentPosition = -1; - int amountOfReloadsOnCurrentUrl = 0; - bool hasCache = true; - - String get currentUrl => history.isEmpty ? null : history[currentPosition]; - JavascriptMode javascriptMode; - List javascriptChannelNames; - - bool hasNavigationDelegate; - bool debuggingEnabled; - String userAgent; - - Future onMethodCall(MethodCall call) { - switch (call.method) { - case 'loadUrl': - final Map request = call.arguments; - _loadUrl(request['url']); - return Future.sync(() {}); - case 'updateSettings': - if (call.arguments['jsMode'] != null) { - javascriptMode = JavascriptMode.values[call.arguments['jsMode']]; - } - if (call.arguments['hasNavigationDelegate'] != null) { - hasNavigationDelegate = call.arguments['hasNavigationDelegate']; - } - if (call.arguments['debuggingEnabled'] != null) { - debuggingEnabled = call.arguments['debuggingEnabled']; - } - userAgent = call.arguments['userAgent']; - break; - case 'canGoBack': - return Future.sync(() => currentPosition > 0); - break; - case 'canGoForward': - return Future.sync(() => currentPosition < history.length - 1); - break; - case 'goBack': - currentPosition = max(-1, currentPosition - 1); - return Future.sync(() {}); - break; - case 'goForward': - currentPosition = min(history.length - 1, currentPosition + 1); - return Future.sync(() {}); - case 'reload': - amountOfReloadsOnCurrentUrl++; - return Future.sync(() {}); - break; - case 'currentUrl': - return Future.value(currentUrl); - break; - case 'evaluateJavascript': - return Future.value(call.arguments); - break; - case 'addJavascriptChannels': - final List channelNames = List.from(call.arguments); - javascriptChannelNames.addAll(channelNames); - break; - case 'removeJavascriptChannels': - final List channelNames = List.from(call.arguments); - javascriptChannelNames - .removeWhere((String channel) => channelNames.contains(channel)); - break; - case 'clearCache': - hasCache = false; - return Future.sync(() {}); - } - return Future.sync(() {}); - } - - void fakeJavascriptPostMessage(String jsChannel, String message) { - final StandardMethodCodec codec = const StandardMethodCodec(); - final Map arguments = { - 'channel': jsChannel, - 'message': message - }; - final ByteData data = codec - .encodeMethodCall(MethodCall('javascriptChannelMessage', arguments)); - ServicesBinding.instance.defaultBinaryMessenger - .handlePlatformMessage(channel.name, data, (ByteData data) {}); - } - - // Fakes a main frame navigation that was initiated by the webview, e.g when - // the user clicks a link in the currently loaded page. - void fakeNavigate(String url) { - if (!hasNavigationDelegate) { - print('no navigation delegate'); - _loadUrl(url); - return; - } - final StandardMethodCodec codec = const StandardMethodCodec(); - final Map arguments = { - 'url': url, - 'isForMainFrame': true - }; - final ByteData data = - codec.encodeMethodCall(MethodCall('navigationRequest', arguments)); - ServicesBinding.instance.defaultBinaryMessenger - .handlePlatformMessage(channel.name, data, (ByteData data) { - final bool allow = codec.decodeEnvelope(data); - if (allow) { - _loadUrl(url); - } - }); - } - - void fakeOnPageStartedCallback() { - final StandardMethodCodec codec = const StandardMethodCodec(); - - final ByteData data = codec.encodeMethodCall(MethodCall( - 'onPageStarted', - {'url': currentUrl}, - )); - - ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage( - channel.name, - data, - (ByteData data) {}, - ); - } - - void fakeOnPageFinishedCallback() { - final StandardMethodCodec codec = const StandardMethodCodec(); - - final ByteData data = codec.encodeMethodCall(MethodCall( - 'onPageFinished', - {'url': currentUrl}, - )); - - ServicesBinding.instance.defaultBinaryMessenger.handlePlatformMessage( - channel.name, - data, - (ByteData data) {}, - ); - } - - void _loadUrl(String url) { - history = history.sublist(0, currentPosition + 1); - history.add(url); - currentPosition++; - amountOfReloadsOnCurrentUrl = 0; - } -} - -class _FakePlatformViewsController { - FakePlatformWebView lastCreatedView; - - Future fakePlatformViewsMethodHandler(MethodCall call) { - switch (call.method) { - case 'create': - final Map args = call.arguments; - final Map params = _decodeParams(args['params']); - lastCreatedView = FakePlatformWebView( - args['id'], - params, - ); - return Future.sync(() => 1); - default: - return Future.sync(() {}); - } - } - - void reset() { - lastCreatedView = null; - } -} - -Map _decodeParams(Uint8List paramsMessage) { - final ByteBuffer buffer = paramsMessage.buffer; - final ByteData messageBytes = buffer.asByteData( - paramsMessage.offsetInBytes, - paramsMessage.lengthInBytes, - ); - return const StandardMessageCodec().decodeMessage(messageBytes); -} - -class _FakeCookieManager { - _FakeCookieManager() { - final MethodChannel channel = const MethodChannel( - 'plugins.flutter.io/cookie_manager', - StandardMethodCodec(), - ); - channel.setMockMethodCallHandler(onMethodCall); - } - - bool hasCookies = true; - - Future onMethodCall(MethodCall call) { - switch (call.method) { - case 'clearCookies': - bool hadCookies = false; - if (hasCookies) { - hadCookies = true; - hasCookies = false; - } - return Future.sync(() { - return hadCookies; - }); - break; - } - return Future.sync(() => null); - } - - void reset() { - hasCookies = true; - } -} - -class MyWebViewPlatform implements WebViewPlatform { - MyWebViewPlatformController lastPlatformBuilt; - - @override - Widget build({ - BuildContext context, - CreationParams creationParams, - @required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, - @required WebViewPlatformCreatedCallback onWebViewPlatformCreated, - Set> gestureRecognizers, - }) { - assert(onWebViewPlatformCreated != null); - lastPlatformBuilt = MyWebViewPlatformController( - creationParams, gestureRecognizers, webViewPlatformCallbacksHandler); - onWebViewPlatformCreated(lastPlatformBuilt); - return Container(); - } - - @override - Future clearCookies() { - return Future.sync(() => null); - } -} - -class MyWebViewPlatformController extends WebViewPlatformController { - MyWebViewPlatformController(this.creationParams, this.gestureRecognizers, - WebViewPlatformCallbacksHandler platformHandler) - : super(platformHandler); - - CreationParams creationParams; - Set> gestureRecognizers; - - String lastUrlLoaded; - Map lastRequestHeaders; - - @override - Future loadUrl(String url, Map headers) { - equals(1, 1); - lastUrlLoaded = url; - lastRequestHeaders = headers; - return null; - } -} - -class MatchesWebSettings extends Matcher { - MatchesWebSettings(this._webSettings); - - final WebSettings _webSettings; - - @override - Description describe(Description description) => - description.add('$_webSettings'); - - @override - bool matches( - covariant WebSettings webSettings, Map matchState) { - return _webSettings.javascriptMode == webSettings.javascriptMode && - _webSettings.hasNavigationDelegate == - webSettings.hasNavigationDelegate && - _webSettings.debuggingEnabled == webSettings.debuggingEnabled && - _webSettings.gestureNavigationEnabled == - webSettings.gestureNavigationEnabled && - _webSettings.userAgent == webSettings.userAgent; - } -} - -class MatchesCreationParams extends Matcher { - MatchesCreationParams(this._creationParams); - - final CreationParams _creationParams; - - @override - Description describe(Description description) => - description.add('$_creationParams'); - - @override - bool matches(covariant CreationParams creationParams, - Map matchState) { - return _creationParams.initialUrl == creationParams.initialUrl && - MatchesWebSettings(_creationParams.webSettings) - .matches(creationParams.webSettings, matchState) && - orderedEquals(_creationParams.javascriptChannelNames) - .matches(creationParams.javascriptChannelNames, matchState); - } -} diff --git a/packages/webview_flutter/webview_flutter/AUTHORS b/packages/webview_flutter/webview_flutter/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/webview_flutter/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/webview_flutter/CHANGELOG.md b/packages/webview_flutter/webview_flutter/CHANGELOG.md new file mode 100644 index 000000000000..6724b43476ff --- /dev/null +++ b/packages/webview_flutter/webview_flutter/CHANGELOG.md @@ -0,0 +1,473 @@ +## 2.1.1 + +* Fixed `_CastError` that was thrown when running the example App. + +## 2.1.0 + +* Migrated to fully federated architecture. + +## 2.0.14 + +* Update minimum Flutter SDK to 2.5 and iOS deployment target to 9.0. + +## 2.0.13 + +* Send URL of File to download to the NavigationDelegate on Android just like it is already done on iOS. +* Updated Android lint settings. + +## 2.0.12 + +* Improved the documentation on using the different Android Platform View modes. +* So that Android and iOS behave the same, `onWebResourceError` is now only called for the main + page. + +## 2.0.11 + +* Remove references to the Android V1 embedding. + +## 2.0.10 + +* Fix keyboard issues link in the README. + +## 2.0.9 + +* Add iOS UI integration test target. +* Suppress deprecation warning for iOS APIs deprecated in iOS 9. + +## 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 +performing better on iOS. Flutter 1.22 no longer requires adding the +`io.flutter.embedded_views_preview` flag to `Info.plist`. + +* Added support for Hybrid Composition on Android (see opt-in instructions in [README](https://github.com/flutter/plugins/blob/master/packages/webview_flutter/README.md#android)) + * Lowered the required Android API to 19 (was previously 20): [#23728](https://github.com/flutter/flutter/issues/23728). + * Fixed the following issues: + * 🎹 Keyboard: [#41089](https://github.com/flutter/flutter/issues/41089), [#36478](https://github.com/flutter/flutter/issues/36478), [#51254](https://github.com/flutter/flutter/issues/51254), [#50716](https://github.com/flutter/flutter/issues/50716), [#55724](https://github.com/flutter/flutter/issues/55724), [#56513](https://github.com/flutter/flutter/issues/56513), [#56515](https://github.com/flutter/flutter/issues/56515), [#61085](https://github.com/flutter/flutter/issues/61085), [#62205](https://github.com/flutter/flutter/issues/62205), [#62547](https://github.com/flutter/flutter/issues/62547), [#58943](https://github.com/flutter/flutter/issues/58943), [#56361](https://github.com/flutter/flutter/issues/56361), [#56361](https://github.com/flutter/flutter/issues/42902), [#40716](https://github.com/flutter/flutter/issues/40716), [#37989](https://github.com/flutter/flutter/issues/37989), [#27924](https://github.com/flutter/flutter/issues/27924). + * ♿️ Accessibility: [#50716](https://github.com/flutter/flutter/issues/50716). + * ⚡️ Performance: [#61280](https://github.com/flutter/flutter/issues/61280), [#31243](https://github.com/flutter/flutter/issues/31243), [#52211](https://github.com/flutter/flutter/issues/52211). + * 📹 Video: [#5191](https://github.com/flutter/flutter/issues/5191). + +## 0.3.24 + +* Keep handling deprecated Android v1 classes for backward compatibility. + +## 0.3.23 + +* Handle WebView multi-window support. + +## 0.3.22+2 + +* Update package:e2e reference to use the local version in the flutter/plugins + repository. + +## 0.3.22+1 + +* Update the `setAndGetScrollPosition` to use hard coded values and add a `pumpAndSettle` call. + +## 0.3.22 + +* Add support for passing a failing url. + +## 0.3.21 + +* Enable programmatic scrolling using Android's WebView.scrollTo & iOS WKWebView.scrollView.contentOffset. + +## 0.3.20+2 + +* Fix CocoaPods podspec lint warnings. + +## 0.3.20+1 + +* OCMock module import -> #import, unit tests compile generated as library. +* Fix select drop down crash on old Android tablets (https://github.com/flutter/flutter/issues/54164). + +## 0.3.20 + +* Added support for receiving web resource loading errors. See `WebView.onWebResourceError`. + +## 0.3.19+10 + +* Replace deprecated `getFlutterEngine` call on Android. + +## 0.3.19+9 + +* Remove example app's iOS workspace settings. + +## 0.3.19+8 + +* Make the pedantic dev_dependency explicit. + +## 0.3.19+7 + +* Remove the Flutter SDK constraint upper bound. + +## 0.3.19+6 + +* Enable opening links that target the "_blank" window (links open in same window). + +## 0.3.19+5 + +* On iOS, always keep contentInsets of the WebView to be 0. +* Fix XCTest case to follow XCTest naming convention. + +## 0.3.19+4 + +* On iOS, fix the scroll view content inset is automatically adjusted. After the fix, the content position of the WebView is customizable by Flutter. +* Fix an iOS 13 bug where the scroll indicator shows at random location. + +## 0.3.19+3 + +* Setup XCTests. + +## 0.3.19+2 + +* Migrate from deprecated BinaryMessages to ServicesBinding.instance.defaultBinaryMessenger. + +## 0.3.19+1 + +* Raise min Flutter SDK requirement to the latest stable. v2 embedding apps no + longer need to special case their Flutter SDK requirement like they have + since v0.3.15+3. + +## 0.3.19 + +* Add setting for iOS to allow gesture based navigation. + +## 0.3.18+1 + +* Be explicit that keyboard is not ready for production in README.md. + +## 0.3.18 + +* Add support for onPageStarted event. +* Remove the deprecated `author:` field from pubspec.yaml +* Migrate to the new pubspec platforms manifest. +* Require Flutter SDK 1.10.0 or greater. + +## 0.3.17 + +* Fix pedantic lint errors. Added missing documentation and awaited some futures + in tests and the example app. + +## 0.3.16 + +* Add support for async NavigationDelegates. Synchronous NavigationDelegates + should still continue to function without any change in behavior. + +## 0.3.15+3 + +* Re-land support for the v2 Android embedding. This correctly sets the minimum + SDK to the latest stable and avoid any compile errors. *WARNING:* the V2 + embedding itself still requires the current Flutter master channel + (flutter/flutter@1d4d63a) for text input to work properly on all Android + versions. + +## 0.3.15+2 + +* Remove AndroidX warnings. + +## 0.3.15+1 + +* Revert the prior embedding support add since it requires an API that hasn't + rolled to stable. + +## 0.3.15 + +* Add support for the v2 Android embedding. This shouldn't affect existing + functionality. Plugin authors who use the V2 embedding can now register the + plugin and expect that it correctly responds to app lifecycle changes. + +## 0.3.14+2 + +* Define clang module for iOS. + +## 0.3.14+1 + +* Allow underscores anywhere for Javascript Channel name. + +## 0.3.14 + +* Added a getTitle getter to WebViewController. + +## 0.3.13 + +* Add an optional `userAgent` property to set a custom User Agent. + +## 0.3.12+1 + +* Temporarily revert getTitle (doing this as a patch bump shortly after publishing). + +## 0.3.12 + +* Added a getTitle getter to WebViewController. + +## 0.3.11+6 + +* Calling destroy on Android webview when flutter webview is getting disposed. + +## 0.3.11+5 + +* Reduce compiler warnings regarding iOS9 compatibility by moving a single + method back into a `@available` block. + +## 0.3.11+4 + +* Removed noisy log messages on iOS. + +## 0.3.11+3 + +* Apply the display listeners workaround that was shipped in 0.3.11+1 on + all Android versions prior to P. + +## 0.3.11+2 + +* Add fix for input connection being dropped after a screen resize on certain + Android devices. + +## 0.3.11+1 + +* Work around a bug in old Android WebView versions that was causing a crash + when resizing the webview on old devices. + +## 0.3.11 + +* Add an initialAutoMediaPlaybackPolicy setting for controlling how auto media + playback is restricted. + +## 0.3.10+5 + +* Add dependency on `androidx.annotation:annotation:1.0.0`. + +## 0.3.10+4 + +* Add keyboard text to README. + +## 0.3.10+3 + +* Don't log an unknown setting key error for 'debuggingEnabled' on iOS. + +## 0.3.10+2 + +* Fix InputConnection being lost when combined with route transitions. + +## 0.3.10+1 + +* Add support for simultaenous Flutter `TextInput` and WebView text fields. + +## 0.3.10 + +* Add partial WebView keyboard support for Android versions prior to N. Support + for UIs that also have Flutter `TextInput` fields is still pending. This basic + support currently only works with Flutter `master`. The keyboard will still + appear when it previously did not when run with older versions of Flutter. But + if the WebView is resized while showing the keyboard the text field will need + to be focused multiple times for any input to be registered. + +## 0.3.9+2 + +* Update Dart code to conform to current Dart formatter. + +## 0.3.9+1 + +* Add missing template type parameter to `invokeMethod` calls. +* Bump minimum Flutter version to 1.5.0. +* Replace invokeMethod with invokeMapMethod wherever necessary. + +## 0.3.9 + +* Allow external packages to provide webview implementations for new platforms. + +## 0.3.8+1 + +* Suppress deprecation warning for BinaryMessages. See: https://github.com/flutter/flutter/issues/33446 + +## 0.3.8 + +* Add `debuggingEnabled` property. + +## 0.3.7+1 + +* Fix an issue where JavaScriptChannel messages weren't sent from the platform thread on Android. + +## 0.3.7 + +* Fix loadUrlWithHeaders flaky test. + +## 0.3.6+1 + +* Remove un-used method params in webview\_flutter + +## 0.3.6 + +* Add an optional `headers` field to the controller. + +## 0.3.5+5 + +* Fixed error in documentation of `javascriptChannels`. + +## 0.3.5+4 + +* Fix bugs in the example app by updating it to use a `StatefulWidget`. + +## 0.3.5+3 + +* Make sure to post javascript channel messages from the platform thread. + +## 0.3.5+2 + +* Fix crash from `NavigationDelegate` on later versions of Android. + +## 0.3.5+1 + +* Fix a bug where updates to onPageFinished were ignored. + +## 0.3.5 + +* Added an onPageFinished callback. + +## 0.3.4 + +* Support specifying navigation delegates that can prevent navigations from being executed. + +## 0.3.3+2 + +* Exclude LongPress handler from semantics tree since it does nothing. + +## 0.3.3+1 + +* Fixed a memory leak on Android - the WebView was not properly disposed. + +## 0.3.3 + +* Add clearCache method to WebView controller. + +## 0.3.2+1 + +* Log a more detailed warning at build time about the previous AndroidX + migration. + +## 0.3.2 + +* Added CookieManager to interface with WebView cookies. Currently has the ability to clear cookies. + +## 0.3.1 + +* Added JavaScript channels to facilitate message passing from JavaScript code running inside + the WebView to the Flutter app's Dart code. + +## 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.0 + +* Added a evaluateJavascript method to WebView controller. +* (BREAKING CHANGE) Renamed the `JavaScriptMode` enum to `JavascriptMode`, and the WebView `javasScriptMode` parameter to `javascriptMode`. + +## 0.1.2 + +* Added a reload method to the WebView controller. + +## 0.1.1 + +* Added a `currentUrl` accessor for the WebView controller to look up what URL + is being displayed. + +## 0.1.0+1 + +* Fix null crash when initialUrl is unset on iOS. + +## 0.1.0 + +* Add goBack, goForward, canGoBack, and canGoForward methods to the WebView controller. + +## 0.0.1+1 + +* Fix case for "FLTWebViewFlutterPlugin" (iOS was failing to buld on case-sensitive file systems). + +## 0.0.1 + +* Initial release. diff --git a/packages/webview_flutter/webview_flutter/LICENSE b/packages/webview_flutter/webview_flutter/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/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/webview_flutter/webview_flutter/README.md b/packages/webview_flutter/webview_flutter/README.md new file mode 100644 index 000000000000..a1a98901affb --- /dev/null +++ b/packages/webview_flutter/webview_flutter/README.md @@ -0,0 +1,94 @@ +# WebView for 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. + +On iOS the WebView widget is backed by a [WKWebView](https://developer.apple.com/documentation/webkit/wkwebview); +On Android the WebView widget is backed by a [WebView](https://developer.android.com/reference/android/webkit/WebView). + +## Usage +Add `webview_flutter` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/platform-integration/platform-channels). If you are targeting Android, make sure to read the *Android Platform Views* section below to choose the platform view mode that best suits your needs. + +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 +the Android’s webview within the Flutter app. It supports two modes: *Virtual displays* (the current default) and *Hybrid composition*. + +Here are some points to consider when choosing between the two: + +* *Hybrid composition* mode has a built-in keyboard support while *Virtual displays* mode has multiple +[keyboard issues](https://github.com/flutter/flutter/issues?q=is%3Aopen+label%3Avd-only+label%3A%22p%3A+webview-keyboard%22) +* *Hybrid composition* mode requires Android SKD 19+ while *Virtual displays* mode requires Android SDK 20+ +* *Hybrid composition* mode has [performence limitations](https://flutter.dev/docs/development/platform-integration/platform-views#performance) when working on Android versions prior to Android 10 while *Virtual displays* is performant on all supported Android versions + +| | Hybrid composition | Virtual displays | +| --------------------------- | ------------------- | ---------------- | +| **Full keyboard supoport** | yes | no | +| **Android SDK support** | 19+ | 20+ | +| **Full performance** | Android 10+ | always | +| **The default** | no | yes | + +### Using Virtual displays + +The mode is currently enabled by default. You should however make sure to set the correct `minSdkVersion` in `android/app/build.gradle` (if it was previously lower than 20): + +```groovy +android { + defaultConfig { + minSdkVersion 20 + } +} +``` + + +### Using Hybrid Composition + +1. Set the correct `minSdkVersion` in `android/app/build.gradle` (if it was previously lower than 19): + + ```groovy + android { + defaultConfig { + minSdkVersion 19 + } + } + ``` + +2. Set `WebView.platform = SurfaceAndroidWebView();` in `initState()`. + For example: + + ```dart + import 'dart:io'; + + import 'package:webview_flutter/webview_flutter.dart'; + + class WebViewExample extends StatefulWidget { + @override + WebViewExampleState createState() => WebViewExampleState(); + } + + class WebViewExampleState extends State { + @override + void initState() { + super.initState(); + // Enable hybrid composition. + if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView(); + } + + @override + Widget build(BuildContext context) { + return WebView( + initialUrl: 'https://flutter.dev', + ); + } + } + ``` + +### Enable Material Components for Android + +To use Material Components when the user interacts with input elements in the WebView, +follow the steps described in the [Enabling Material Components instructions](https://flutter.dev/docs/deployment/android#enabling-material-components). diff --git a/packages/webview_flutter/example/.metadata b/packages/webview_flutter/webview_flutter/example/.metadata similarity index 100% rename from packages/webview_flutter/example/.metadata rename to packages/webview_flutter/webview_flutter/example/.metadata diff --git a/packages/webview_flutter/webview_flutter/example/README.md b/packages/webview_flutter/webview_flutter/example/README.md new file mode 100644 index 000000000000..850ee74397a9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/README.md @@ -0,0 +1,8 @@ +# webview_flutter_example + +Demonstrates how to use the webview_flutter plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.dev/). diff --git a/packages/webview_flutter/webview_flutter/example/android/app/build.gradle b/packages/webview_flutter/webview_flutter/example/android/app/build.gradle new file mode 100644 index 000000000000..9a43699afb2b --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/android/app/build.gradle @@ -0,0 +1,62 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 29 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "io.flutter.plugins.webviewflutterexample" + minSdkVersion 19 + 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' + api 'androidx.test:core:1.2.0' +} diff --git a/packages/quick_actions/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/webview_flutter/webview_flutter/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/webview_flutter/webview_flutter/example/android/app/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java b/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java new file mode 100644 index 000000000000..a32aaebb0ecd --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutterexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class MainActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/WebViewTest.java b/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/WebViewTest.java new file mode 100644 index 000000000000..0b3eeef9b6b7 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/WebViewTest.java @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutterexample; + +import static org.junit.Assert.assertTrue; + +import androidx.test.core.app.ActivityScenario; +import io.flutter.plugins.webviewflutter.WebViewFlutterPlugin; +import org.junit.Test; + +public class WebViewTest { + @Test + public void webViewPluginIsAdded() { + final ActivityScenario scenario = + ActivityScenario.launch(WebViewTestActivity.class); + scenario.onActivity( + activity -> { + assertTrue(activity.engine.getPlugins().has(WebViewFlutterPlugin.class)); + }); + } +} diff --git a/packages/webview_flutter/webview_flutter/example/android/app/src/debug/AndroidManifest.xml b/packages/webview_flutter/webview_flutter/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..28792201bc36 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/packages/webview_flutter/webview_flutter/example/android/app/src/main/AndroidManifest.xml b/packages/webview_flutter/webview_flutter/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..b8c8d38d45a5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/packages/webview_flutter/webview_flutter/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/WebViewTestActivity.java b/packages/webview_flutter/webview_flutter/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/WebViewTestActivity.java new file mode 100644 index 000000000000..cb53a7a0dbf5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/WebViewTestActivity.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.webviewflutterexample; + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; + +// Extends FlutterActivity to make the FlutterEngine accessible for testing. +public class WebViewTestActivity extends FlutterActivity { + public FlutterEngine engine; + + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + engine = flutterEngine; + } +} diff --git a/packages/ios_platform_images/example/android/app/src/main/res/drawable/launch_background.xml b/packages/webview_flutter/webview_flutter/example/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from packages/ios_platform_images/example/android/app/src/main/res/drawable/launch_background.xml rename to packages/webview_flutter/webview_flutter/example/android/app/src/main/res/drawable/launch_background.xml diff --git a/packages/ios_platform_images/example/android/app/bin/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from packages/ios_platform_images/example/android/app/bin/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/webview_flutter/webview_flutter/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/ios_platform_images/example/android/app/bin/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from packages/ios_platform_images/example/android/app/bin/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/webview_flutter/webview_flutter/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/ios_platform_images/example/android/app/bin/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from packages/ios_platform_images/example/android/app/bin/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/webview_flutter/webview_flutter/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/ios_platform_images/example/android/app/bin/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from packages/ios_platform_images/example/android/app/bin/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/webview_flutter/webview_flutter/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/ios_platform_images/example/android/app/bin/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from packages/ios_platform_images/example/android/app/bin/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/webview_flutter/webview_flutter/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/packages/ios_platform_images/example/android/app/bin/src/main/res/values/styles.xml b/packages/webview_flutter/webview_flutter/example/android/app/src/main/res/values/styles.xml similarity index 100% rename from packages/ios_platform_images/example/android/app/bin/src/main/res/values/styles.xml rename to packages/webview_flutter/webview_flutter/example/android/app/src/main/res/values/styles.xml diff --git a/packages/webview_flutter/webview_flutter/example/android/build.gradle b/packages/webview_flutter/webview_flutter/example/android/build.gradle new file mode 100644 index 000000000000..e101ac08df55 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/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/connectivity/connectivity_macos/example/android/gradle.properties b/packages/webview_flutter/webview_flutter/example/android/gradle.properties similarity index 100% rename from packages/connectivity/connectivity_macos/example/android/gradle.properties rename to packages/webview_flutter/webview_flutter/example/android/gradle.properties diff --git a/packages/e2e/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/webview_flutter/webview_flutter/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/e2e/example/android/gradle/wrapper/gradle-wrapper.properties rename to packages/webview_flutter/webview_flutter/example/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/ios_platform_images/example/android/settings.gradle b/packages/webview_flutter/webview_flutter/example/android/settings.gradle similarity index 100% rename from packages/ios_platform_images/example/android/settings.gradle rename to packages/webview_flutter/webview_flutter/example/android/settings.gradle diff --git a/packages/webview_flutter/example/assets/sample_audio.ogg b/packages/webview_flutter/webview_flutter/example/assets/sample_audio.ogg similarity index 100% rename from packages/webview_flutter/example/assets/sample_audio.ogg rename to packages/webview_flutter/webview_flutter/example/assets/sample_audio.ogg diff --git a/packages/webview_flutter/webview_flutter/example/assets/sample_video.mp4 b/packages/webview_flutter/webview_flutter/example/assets/sample_video.mp4 new file mode 100644 index 000000000000..a203d0cdf13e Binary files /dev/null and b/packages/webview_flutter/webview_flutter/example/assets/sample_video.mp4 differ diff --git a/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart new file mode 100644 index 000000000000..3379bafa2346 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/integration_test/webview_flutter_test.dart @@ -0,0 +1,1473 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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 'dart:typed_data'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter/platform_interface.dart'; +import 'package:webview_flutter/webview_flutter.dart'; +import 'package:integration_test/integration_test.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // URLs to navigate to in tests. These need to be URLs that we are confident will + // always be accessible, and won't do redirection. (E.g., just + // 'https://www.google.com/' will sometimes redirect traffic that looks + // like it's coming from a bot, which is true of these tests). + const String primaryUrl = 'https://flutter.dev/'; + const String secondaryUrl = 'https://www.google.com/robots.txt'; + + const bool _skipDueToIssue86757 = true; + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('initialUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); + }, skip: _skipDueToIssue86757); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('loadUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await controller.loadUrl(secondaryUrl); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); + }, skip: _skipDueToIssue86757); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('loadUrl with headers', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageStarts = StreamController(); + final StreamController pageLoads = StreamController(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarts.add(url); + }, + onPageFinished: (String url) { + pageLoads.add(url); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final Map headers = { + 'test_header': 'flutter_test_header' + }; + await controller.loadUrl('https://flutter-header-echo.herokuapp.com/', + headers: headers); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://flutter-header-echo.herokuapp.com/'); + + await pageStarts.stream.firstWhere((String url) => url == currentUrl); + await pageLoads.stream.firstWhere((String url) => url == currentUrl); + + final String content = await controller + .evaluateJavascript('document.documentElement.innerText'); + expect(content.contains('flutter_test_header'), isTrue); + }, skip: Platform.isAndroid && _skipDueToIssue86757); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('JavaScriptChannel', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final List messagesReceived = []; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + // This is the data URL for: '' + initialUrl: + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'Echo', + onMessageReceived: (JavascriptMessage message) { + messagesReceived.add(message.message); + }, + ), + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + expect(messagesReceived, isEmpty); + // Append a return value "1" in the end will prevent an iOS platform exception. + // See: https://github.com/flutter/flutter/issues/66318#issuecomment-701105380 + // TODO(cyanglaz): remove the workaround "1" in the end when the below issue is fixed. + // https://github.com/flutter/flutter/issues/66318 + await controller.evaluateJavascript('Echo.postMessage("hello");1;'); + expect(messagesReceived, equals(['hello'])); + }, skip: Platform.isAndroid && _skipDueToIssue86757); + + testWidgets('resize webview', (WidgetTester tester) async { + final String resizeTest = ''' + + Resize test + + + + + + '''; + final String resizeTestBase64 = + base64Encode(const Utf8Encoder().convert(resizeTest)); + final Completer resizeCompleter = Completer(); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + final GlobalKey key = GlobalKey(); + + final WebView webView = WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$resizeTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptChannels: { + JavascriptChannel( + name: 'Resize', + onMessageReceived: (JavascriptMessage message) { + resizeCompleter.complete(true); + }, + ), + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + javascriptMode: JavascriptMode.unrestricted, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: 200, + height: 200, + child: webView, + ), + ], + ), + ), + ); + + await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + expect(resizeCompleter.isCompleted, false); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: 400, + height: 400, + child: webView, + ), + ], + ), + ), + ); + + await resizeCompleter.future; + }); + + testWidgets('set custom userAgent', (WidgetTester tester) async { + final Completer controllerCompleter1 = + Completer(); + final GlobalKey _globalKey = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent1', + onWebViewCreated: (WebViewController controller) { + controllerCompleter1.complete(controller); + }, + ), + ), + ); + final WebViewController controller1 = await controllerCompleter1.future; + final String customUserAgent1 = await _getUserAgent(controller1); + expect(customUserAgent1, 'Custom_User_Agent1'); + // rebuild the WebView with a different user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent2', + ), + ), + ); + + final String customUserAgent2 = await _getUserAgent(controller1); + expect(customUserAgent2, 'Custom_User_Agent2'); + }); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('use default platform userAgent after webView is rebuilt', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final GlobalKey _globalKey = GlobalKey(); + // Build the webView with no user agent to get the default platform user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: primaryUrl, + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String defaultPlatformUserAgent = await _getUserAgent(controller); + // rebuild the WebView with a custom user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent', + ), + ), + ); + final String customUserAgent = await _getUserAgent(controller); + expect(customUserAgent, 'Custom_User_Agent'); + // rebuilds the WebView with no user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ); + + final String customUserAgent2 = await _getUserAgent(controller); + expect(customUserAgent2, defaultPlatformUserAgent); + }, skip: Platform.isAndroid && _skipDueToIssue86757); + + group('Video playback policy', () { + late String videoTestBase64; + setUpAll(() async { + final ByteData videoData = + await rootBundle.load('assets/sample_video.mp4'); + final String base64VideoData = + base64Encode(Uint8List.view(videoData.buffer)); + final String videoTest = ''' + + Video auto play + + + + + + + '''; + videoTestBase64 = base64Encode(const Utf8Encoder().convert(videoTest)); + }); + + testWidgets('Auto media playback', (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + controllerCompleter = Completer(); + pageLoaded = Completer(); + + // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + controller = await controllerCompleter.future; + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(true)); + }); + + testWidgets('Changes to initialMediaPlaybackPolicy are ignored', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + await controller.reload(); + + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + }); + + testWidgets('Video plays inline when allowsInlineMediaPlayback is true', + (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + Completer videoPlaying = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'VideoTestTime', + onMessageReceived: (JavascriptMessage message) { + final double currentTime = double.parse(message.message); + // Let it play for at least 1 second to make sure the related video's properties are set. + if (currentTime > 1) { + videoPlaying.complete(null); + } + }, + ), + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + allowsInlineMediaPlayback: true, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + // Pump once to trigger the video play. + await tester.pump(); + + // Makes sure we get the correct event that indicates the video is actually playing. + await videoPlaying.future; + + String fullScreen = + await controller.evaluateJavascript('isFullScreen();'); + expect(fullScreen, _webviewBool(false)); + }); + + // allowsInlineMediaPlayback is a noop on Android, so it is skipped. + testWidgets( + 'Video plays full screen when allowsInlineMediaPlayback is false', + (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + Completer videoPlaying = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'VideoTestTime', + onMessageReceived: (JavascriptMessage message) { + final double currentTime = double.parse(message.message); + // Let it play for at least 1 second to make sure the related video's properties are set. + if (currentTime > 1) { + videoPlaying.complete(null); + } + }, + ), + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + allowsInlineMediaPlayback: false, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + // Pump once to trigger the video play. + await tester.pump(); + + // Makes sure we get the correct event that indicates the video is actually playing. + await videoPlaying.future; + + String fullScreen = + await controller.evaluateJavascript('isFullScreen();'); + expect(fullScreen, _webviewBool(true)); + }, skip: Platform.isAndroid); + }); + + group('Audio playback policy', () { + late String audioTestBase64; + setUpAll(() async { + final ByteData audioData = + await rootBundle.load('assets/sample_audio.ogg'); + final String base64AudioData = + base64Encode(Uint8List.view(audioData.buffer)); + final String audioTest = ''' + + Audio auto play + + + + + + + '''; + audioTestBase64 = base64Encode(const Utf8Encoder().convert(audioTest)); + }); + + testWidgets('Auto media playback', (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageStarted = Completer(); + Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + controllerCompleter = Completer(); + pageStarted = Completer(); + pageLoaded = Completer(); + + // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(true)); + }); + + testWidgets('Changes to initialMediaPlaybackPolocy are ignored', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageStarted = Completer(); + Completer pageLoaded = Completer(); + + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + pageStarted = Completer(); + pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + await controller.reload(); + + await pageStarted.future; + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + }); + }); + + testWidgets('getTitle', (WidgetTester tester) async { + final String getTitleTest = ''' + + Some title + + + + + '''; + final String getTitleTestBase64 = + base64Encode(const Utf8Encoder().convert(getTitleTest)); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + final String? title = await controller.getTitle(); + expect(title, 'Some title'); + }); + + group('Programmatic Scroll', () { + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { + final String scrollTestPage = ''' + + + + + + +
      + + + '''; + + final String scrollTestPageBase64 = + base64Encode(const Utf8Encoder().convert(scrollTestPage)); + + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + await tester.pumpAndSettle(Duration(seconds: 3)); + + int scrollPosX = await controller.getScrollX(); + int scrollPosY = await controller.getScrollY(); + + // Check scrollTo() + const int X_SCROLL = 123; + const int Y_SCROLL = 321; + // Get the initial position; this ensures that scrollTo is actually + // changing something, but also gives the native view's scroll position + // time to settle. + expect(scrollPosX, isNot(X_SCROLL)); + expect(scrollPosX, isNot(Y_SCROLL)); + + await controller.scrollTo(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(scrollPosX, X_SCROLL); + expect(scrollPosY, Y_SCROLL); + + // Check scrollBy() (on top of scrollTo()) + await controller.scrollBy(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(scrollPosX, X_SCROLL * 2); + expect(scrollPosY, Y_SCROLL * 2); + }, skip: Platform.isAndroid && _skipDueToIssue86757); + }); + + group('SurfaceAndroidWebView', () { + setUpAll(() { + WebView.platform = SurfaceAndroidWebView(); + }); + + tearDownAll(() { + WebView.platform = null; + }); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { + final String scrollTestPage = ''' + + + + + + +
      + + + '''; + + final String scrollTestPageBase64 = + base64Encode(const Utf8Encoder().convert(scrollTestPage)); + + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + await tester.pumpAndSettle(Duration(seconds: 3)); + + // Check scrollTo() + const int X_SCROLL = 123; + const int Y_SCROLL = 321; + + await controller.scrollTo(X_SCROLL, Y_SCROLL); + int scrollPosX = await controller.getScrollX(); + int scrollPosY = await controller.getScrollY(); + expect(X_SCROLL, scrollPosX); + expect(Y_SCROLL, scrollPosY); + + // Check scrollBy() (on top of scrollTo()) + await controller.scrollBy(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(X_SCROLL * 2, scrollPosX); + expect(Y_SCROLL * 2, scrollPosY); + }, skip: !Platform.isAndroid || _skipDueToIssue86757); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('inputs are scrolled into view when focused', + (WidgetTester tester) async { + final String scrollTestPage = ''' + + + + + + +
      + + + + '''; + + final String scrollTestPageBase64 = + base64Encode(const Utf8Encoder().convert(scrollTestPage)); + + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.runAsync(() async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: SizedBox( + width: 200, + height: 200, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ), + ); + await Future.delayed(Duration(milliseconds: 20)); + await tester.pump(); + }); + + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + final String viewportRectJSON = await _evaluateJavascript( + controller, 'JSON.stringify(viewport.getBoundingClientRect())'); + final Map viewportRectRelativeToViewport = + jsonDecode(viewportRectJSON); + + // Check that the input is originally outside of the viewport. + + final String initialInputClientRectJSON = await _evaluateJavascript( + controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); + final Map initialInputClientRectRelativeToViewport = + jsonDecode(initialInputClientRectJSON); + + expect( + initialInputClientRectRelativeToViewport['bottom'] <= + viewportRectRelativeToViewport['bottom'], + isFalse); + + await controller.evaluateJavascript('inputEl.focus()'); + + // Check that focusing the input brought it into view. + + final String lastInputClientRectJSON = await _evaluateJavascript( + controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); + final Map lastInputClientRectRelativeToViewport = + jsonDecode(lastInputClientRectJSON); + + expect( + lastInputClientRectRelativeToViewport['top'] >= + viewportRectRelativeToViewport['top'], + isTrue); + expect( + lastInputClientRectRelativeToViewport['bottom'] <= + viewportRectRelativeToViewport['bottom'], + isTrue); + + expect( + lastInputClientRectRelativeToViewport['left'] >= + viewportRectRelativeToViewport['left'], + isTrue); + expect( + lastInputClientRectRelativeToViewport['right'] <= + viewportRectRelativeToViewport['right'], + isTrue); + }, skip: !Platform.isAndroid || _skipDueToIssue86757); + }); + + group('NavigationDelegate', () { + final String blankPage = ""; + final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + + base64Encode(const Utf8Encoder().convert(blankPage)); + + testWidgets('can allow requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller.evaluateJavascript('location.href = "$secondaryUrl"'); + + await pageLoads.stream.first; // Wait for the next page load. + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); + }); + + testWidgets('onWebResourceError', (WidgetTester tester) async { + final Completer errorCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://www.notawebsite..com', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + ), + ), + ); + + final WebResourceError error = await errorCompleter.future; + expect(error, isNotNull); + + if (Platform.isIOS) { + expect(error.domain, isNotNull); + expect(error.failingUrl, isNull); + } else if (Platform.isAndroid) { + expect(error.errorType, isNotNull); + expect(error.failingUrl?.startsWith('https://www.notawebsite..com'), + isTrue); + } + }); + + testWidgets('onWebResourceError is not called with valid url', + (WidgetTester tester) async { + final Completer errorCompleter = + Completer(); + final Completer pageFinishCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + onPageFinished: (_) => pageFinishCompleter.complete(), + ), + ), + ); + + expect(errorCompleter.future, doesNotComplete); + await pageFinishCompleter.future; + }); + + testWidgets( + 'onWebResourceError only called for main frame', + (WidgetTester tester) async { + final String iframeTest = ''' + + + + WebResourceError test + + + + + + '''; + final String iframeTestBase64 = + base64Encode(const Utf8Encoder().convert(iframeTest)); + + final Completer errorCompleter = + Completer(); + final Completer pageFinishCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: + 'data:text/html;charset=utf-8;base64,$iframeTestBase64', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + onPageFinished: (_) => pageFinishCompleter.complete(), + ), + ), + ); + + expect(errorCompleter.future, doesNotComplete); + await pageFinishCompleter.future; + }, + ); + + testWidgets('can block requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller + .evaluateJavascript('location.href = "https://www.youtube.com/"'); + + // There should never be any second page load, since our new URL is + // blocked. Still wait for a potential page change for some time in order + // to give the test a chance to fail. + await pageLoads.stream.first + .timeout(const Duration(milliseconds: 500), onTimeout: () => ''); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, isNot(contains('youtube.com'))); + }); + + testWidgets('supports asynchronous decisions', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) async { + NavigationDecision decision = NavigationDecision.prevent; + decision = await Future.delayed( + const Duration(milliseconds: 10), + () => NavigationDecision.navigate); + return decision; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller.evaluateJavascript('location.href = "$secondaryUrl"'); + + await pageLoads.stream.first; // Wait for second page to load. + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); + }); + }); + + testWidgets('launches with gestureNavigationEnabled on iOS', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: SizedBox( + width: 400, + height: 300, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + gestureNavigationEnabled: true, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); + }); + + testWidgets('target _blank opens in same window', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageLoaded = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await controller.evaluateJavascript('window.open("$primaryUrl", "_blank")'); + await pageLoaded.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); + }, + // Flaky on Android: https://github.com/flutter/flutter/issues/86757 + skip: Platform.isAndroid && _skipDueToIssue86757); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets( + 'can open new window and go back', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(); + }, + initialUrl: primaryUrl, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + expect(controller.currentUrl(), completion(primaryUrl)); + await pageLoaded.future; + pageLoaded = Completer(); + + await controller.evaluateJavascript('window.open("$secondaryUrl")'); + await pageLoaded.future; + pageLoaded = Completer(); + expect(controller.currentUrl(), completion(secondaryUrl)); + + expect(controller.canGoBack(), completion(true)); + await controller.goBack(); + await pageLoaded.future; + expect(controller.currentUrl(), completion(primaryUrl)); + }, + skip: _skipDueToIssue86757, + ); + + testWidgets( + 'javascript does not run in parent window', + (WidgetTester tester) async { + final String iframe = ''' + + + '''; + final String iframeTestBase64 = + base64Encode(const Utf8Encoder().convert(iframe)); + + final String openWindowTest = ''' + + + + XSS test + + + + + + '''; + final String openWindowTestBase64 = + base64Encode(const Utf8Encoder().convert(openWindowTest)); + final Completer controllerCompleter = + Completer(); + final Completer pageLoadCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + initialUrl: + 'data:text/html;charset=utf-8;base64,$openWindowTestBase64', + onPageFinished: (String url) { + pageLoadCompleter.complete(); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageLoadCompleter.future; + + expect(controller.evaluateJavascript('iframeLoaded'), completion('true')); + expect( + controller.evaluateJavascript( + 'document.querySelector("p") && document.querySelector("p").textContent'), + completion('null'), + ); + }, + skip: !Platform.isAndroid, + ); +} + +// JavaScript booleans evaluate to different string values on Android and iOS. +// This utility method returns the string boolean value of the current platform. +String _webviewBool(bool value) { + if (defaultTargetPlatform == TargetPlatform.iOS) { + return value ? '1' : '0'; + } + return value ? 'true' : 'false'; +} + +/// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. +Future _getUserAgent(WebViewController controller) async { + return _evaluateJavascript(controller, 'navigator.userAgent;'); +} + +Future _evaluateJavascript( + WebViewController controller, String js) async { + if (defaultTargetPlatform == TargetPlatform.iOS) { + return await controller.evaluateJavascript(js); + } + return jsonDecode(await controller.evaluateJavascript(js)); +} diff --git a/packages/webview_flutter/webview_flutter/example/ios/Flutter/AppFrameworkInfo.plist b/packages/webview_flutter/webview_flutter/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..8d4492f977ad --- /dev/null +++ b/packages/webview_flutter/webview_flutter/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 + 9.0 + + diff --git a/packages/device_info/example/ios/Flutter/Debug.xcconfig b/packages/webview_flutter/webview_flutter/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/device_info/example/ios/Flutter/Debug.xcconfig rename to packages/webview_flutter/webview_flutter/example/ios/Flutter/Debug.xcconfig diff --git a/packages/device_info/example/ios/Flutter/Release.xcconfig b/packages/webview_flutter/webview_flutter/example/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/device_info/example/ios/Flutter/Release.xcconfig rename to packages/webview_flutter/webview_flutter/example/ios/Flutter/Release.xcconfig diff --git a/packages/webview_flutter/webview_flutter/example/ios/Podfile b/packages/webview_flutter/webview_flutter/example/ios/Podfile new file mode 100644 index 000000000000..66509fcae284 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/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 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/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj b/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..62428d041adf --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,722 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 334734012669319100DCC49E /* FLTWebViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */; }; + 334734022669319400DCC49E /* FLTWKNavigationDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 9D26F6F82D91F92CC095EBA9 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */; }; + D9A9D48F1A75E5C682944DDD /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 27CC950C9005575711528C12 /* libPods-RunnerTests.a */; }; + F7151F77266057800028CB91 /* FLTWebViewUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F76266057800028CB91 /* FLTWebViewUITests.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 68BDCAEE23C3F7CB00D9C032 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; + F7151F79266057800028CB91 /* 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 */ + 127772EEA7782174BE0D74B5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 27CC950C9005575711528C12 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTWKNavigationDelegateTests.m; sourceTree = ""; }; + 68BDCAE923C3F7CB00D9C032 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 68BDCAED23C3F7CB00D9C032 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTWebViewTests.m; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; + C475C484BD510DD9CB2E403C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + F7151F74266057800028CB91 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F7151F76266057800028CB91 /* FLTWebViewUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTWebViewUITests.m; sourceTree = ""; }; + F7151F78266057800028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 68BDCAE623C3F7CB00D9C032 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D9A9D48F1A75E5C682944DDD /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9D26F6F82D91F92CC095EBA9 /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F71266057800028CB91 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 68BDCAEA23C3F7CB00D9C032 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */, + 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */, + 68BDCAED23C3F7CB00D9C032 /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 68BDCAEA23C3F7CB00D9C032 /* RunnerTests */, + F7151F75266057800028CB91 /* RunnerUITests */, + 97C146EF1CF9000F007C117D /* Products */, + C6FFB52F5C2B8A41A7E39DE2 /* Pods */, + B6736FC417BDCCDA377E779D /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 68BDCAE923C3F7CB00D9C032 /* RunnerTests.xctest */, + F7151F74266057800028CB91 /* 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 = ""; + }; + B6736FC417BDCCDA377E779D /* Frameworks */ = { + isa = PBXGroup; + children = ( + 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */, + 27CC950C9005575711528C12 /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + C6FFB52F5C2B8A41A7E39DE2 /* Pods */ = { + isa = PBXGroup; + children = ( + 127772EEA7782174BE0D74B5 /* Pods-Runner.debug.xcconfig */, + C475C484BD510DD9CB2E403C /* Pods-Runner.release.xcconfig */, + F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */, + E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + F7151F75266057800028CB91 /* RunnerUITests */ = { + isa = PBXGroup; + children = ( + F7151F76266057800028CB91 /* FLTWebViewUITests.m */, + F7151F78266057800028CB91 /* Info.plist */, + ); + path = RunnerUITests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 68BDCAE823C3F7CB00D9C032 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 68BDCAF223C3F7CB00D9C032 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 53FD4CBDD9756D74B5A3B4C1 /* [CP] Check Pods Manifest.lock */, + 68BDCAE523C3F7CB00D9C032 /* Sources */, + 68BDCAE623C3F7CB00D9C032 /* Frameworks */, + 68BDCAE723C3F7CB00D9C032 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 68BDCAEF23C3F7CB00D9C032 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = webview_flutter_exampleTests; + productReference = 68BDCAE923C3F7CB00D9C032 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + B71376B4FB8384EF9D5F3F84 /* [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"; + }; + F7151F73266057800028CB91 /* RunnerUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F7151F7B266057800028CB91 /* Build configuration list for PBXNativeTarget "RunnerUITests" */; + buildPhases = ( + F7151F70266057800028CB91 /* Sources */, + F7151F71266057800028CB91 /* Frameworks */, + F7151F72266057800028CB91 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F7151F7A266057800028CB91 /* PBXTargetDependency */, + ); + name = RunnerUITests; + productName = RunnerUITests; + productReference = F7151F74266057800028CB91 /* RunnerUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + DefaultBuildSystemTypeForWorkspace = Original; + LastUpgradeCheck = 1030; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 68BDCAE823C3F7CB00D9C032 = { + ProvisioningStyle = Automatic; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + F7151F73266057800028CB91 = { + CreatedOnToolsVersion = 12.5; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 68BDCAE823C3F7CB00D9C032 /* RunnerTests */, + F7151F73266057800028CB91 /* RunnerUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 68BDCAE723C3F7CB00D9C032 /* 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; + }; + F7151F72266057800028CB91 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed\n/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin\n"; + }; + 53FD4CBDD9756D74B5A3B4C1 /* [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\n"; + }; + B71376B4FB8384EF9D5F3F84 /* [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 */ + 68BDCAE523C3F7CB00D9C032 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 334734012669319100DCC49E /* FLTWebViewTests.m in Sources */, + 334734022669319400DCC49E /* FLTWKNavigationDelegateTests.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; + }; + F7151F70266057800028CB91 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F7151F77266057800028CB91 /* FLTWebViewUITests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 68BDCAEF23C3F7CB00D9C032 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 68BDCAEE23C3F7CB00D9C032 /* PBXContainerItemProxy */; + }; + F7151F7A266057800028CB91 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = F7151F79266057800028CB91 /* 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 */ + 68BDCAF023C3F7CB00D9C032 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F674B2A05DAC369B4FF27850 /* 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; + }; + 68BDCAF123C3F7CB00D9C032 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E14113434CCE6D3186B5CBC3 /* 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; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(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.webviewFlutterExample; + 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.webviewFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + F7151F7C266057800028CB91 /* 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; + }; + F7151F7D266057800028CB91 /* 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 */ + 68BDCAF223C3F7CB00D9C032 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 68BDCAF023C3F7CB00D9C032 /* Debug */, + 68BDCAF123C3F7CB00D9C032 /* 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; + }; + F7151F7B266057800028CB91 /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F7151F7C266057800028CB91 /* Debug */, + F7151F7D266057800028CB91 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..d7453a8ce862 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/path_provider/path_provider_macos/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/webview_flutter/webview_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/path_provider/path_provider_macos/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/webview_flutter/webview_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/webview_flutter/webview_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/webview_flutter/webview_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/webview_flutter/webview_flutter/example/ios/Runner/AppDelegate.h b/packages/webview_flutter/webview_flutter/example/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..0681d288bb70 --- /dev/null +++ b/packages/webview_flutter/webview_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/webview_flutter/webview_flutter/example/ios/Runner/AppDelegate.m b/packages/webview_flutter/webview_flutter/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..30b87969f44a --- /dev/null +++ b/packages/webview_flutter/webview_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. + +#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/webview_flutter/webview_flutter/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/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/webview_flutter/webview_flutter/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/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from packages/e2e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/packages/flutter_plugin_android_lifecycle/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/webview_flutter/webview_flutter/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/webview_flutter/webview_flutter/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/webview_flutter/webview_flutter/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/webview_flutter/webview_flutter/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/webview_flutter/webview_flutter/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/webview_flutter/webview_flutter/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/webview_flutter/webview_flutter/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/webview_flutter/webview_flutter/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/webview_flutter/webview_flutter/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/webview_flutter/webview_flutter/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/packages/espresso/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/webview_flutter/webview_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from packages/espresso/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/e2e/example/ios/Runner/Base.lproj/Main.storyboard b/packages/webview_flutter/webview_flutter/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/e2e/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/webview_flutter/webview_flutter/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/packages/webview_flutter/webview_flutter/example/ios/Runner/Info.plist b/packages/webview_flutter/webview_flutter/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..a810c5a172c0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/ios/Runner/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + webview_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 + + + diff --git a/packages/webview_flutter/webview_flutter/example/ios/Runner/main.m b/packages/webview_flutter/webview_flutter/example/ios/Runner/main.m new file mode 100644 index 000000000000..f97b9ef5c8a1 --- /dev/null +++ b/packages/webview_flutter/webview_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/webview_flutter/webview_flutter/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m b/packages/webview_flutter/webview_flutter/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m new file mode 100644 index 000000000000..08c2e8b60832 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m @@ -0,0 +1,38 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import Flutter; +@import XCTest; +@import webview_flutter; + +// OCMock library doesn't generate a valid modulemap. +#import + +@interface FLTWKNavigationDelegateTests : XCTestCase + +@property(strong, nonatomic) FlutterMethodChannel *mockMethodChannel; +@property(strong, nonatomic) FLTWKNavigationDelegate *navigationDelegate; + +@end + +@implementation FLTWKNavigationDelegateTests + +- (void)setUp { + self.mockMethodChannel = OCMClassMock(FlutterMethodChannel.class); + self.navigationDelegate = + [[FLTWKNavigationDelegate alloc] initWithChannel:self.mockMethodChannel]; +} + +- (void)testWebViewWebContentProcessDidTerminateCallsRecourseErrorChannel { + WKWebView *webview = OCMClassMock(WKWebView.class); + [self.navigationDelegate webViewWebContentProcessDidTerminate:webview]; + OCMVerify([self.mockMethodChannel invokeMethod:@"onWebResourceError" + arguments:[OCMArg checkWithBlock:^BOOL(NSDictionary *args) { + XCTAssertEqualObjects(args[@"errorType"], + @"webContentProcessTerminated"); + return true; + }]]); +} + +@end diff --git a/packages/webview_flutter/webview_flutter/example/ios/RunnerTests/FLTWebViewTests.m b/packages/webview_flutter/webview_flutter/example/ios/RunnerTests/FLTWebViewTests.m new file mode 100644 index 000000000000..f8229935cbe6 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/ios/RunnerTests/FLTWebViewTests.m @@ -0,0 +1,91 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import Flutter; +@import XCTest; +@import webview_flutter; + +// OCMock library doesn't generate a valid modulemap. +#import + +static bool feq(CGFloat a, CGFloat b) { return fabs(b - a) < FLT_EPSILON; } + +@interface FLTWebViewTests : XCTestCase + +@property(strong, nonatomic) NSObject *mockBinaryMessenger; + +@end + +@implementation FLTWebViewTests + +- (void)setUp { + [super setUp]; + self.mockBinaryMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); +} + +- (void)testCanInitFLTWebViewController { + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTAssertNotNil(controller); +} + +- (void)testCanInitFLTWebViewFactory { + FLTWebViewFactory *factory = + [[FLTWebViewFactory alloc] initWithMessenger:self.mockBinaryMessenger]; + XCTAssertNotNil(factory); +} + +- (void)webViewContentInsetBehaviorShouldBeNeverOnIOS11 { + if (@available(iOS 11, *)) { + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + UIView *view = controller.view; + XCTAssertTrue([view isKindOfClass:WKWebView.class]); + WKWebView *webView = (WKWebView *)view; + XCTAssertEqual(webView.scrollView.contentInsetAdjustmentBehavior, + UIScrollViewContentInsetAdjustmentNever); + } +} + +- (void)testWebViewScrollIndicatorAticautomaticallyAdjustsScrollIndicatorInsetsShouldbeNoOnIOS13 { + if (@available(iOS 13, *)) { + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + UIView *view = controller.view; + XCTAssertTrue([view isKindOfClass:WKWebView.class]); + WKWebView *webView = (WKWebView *)view; + XCTAssertFalse(webView.scrollView.automaticallyAdjustsScrollIndicatorInsets); + } +} + +- (void)testContentInsetsSumAlwaysZeroAfterSetFrame { + FLTWKWebView *webView = [[FLTWKWebView alloc] initWithFrame:CGRectMake(0, 0, 300, 400)]; + webView.scrollView.contentInset = UIEdgeInsetsMake(0, 0, 300, 0); + XCTAssertFalse(UIEdgeInsetsEqualToEdgeInsets(webView.scrollView.contentInset, UIEdgeInsetsZero)); + webView.frame = CGRectMake(0, 0, 300, 200); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(webView.scrollView.contentInset, UIEdgeInsetsZero)); + XCTAssertTrue(CGRectEqualToRect(webView.frame, CGRectMake(0, 0, 300, 200))); + + if (@available(iOS 11, *)) { + // After iOS 11, we need to make sure the contentInset compensates the adjustedContentInset. + UIScrollView *partialMockScrollView = OCMPartialMock(webView.scrollView); + UIEdgeInsets insetToAdjust = UIEdgeInsetsMake(0, 0, 300, 0); + OCMStub(partialMockScrollView.adjustedContentInset).andReturn(insetToAdjust); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(webView.scrollView.contentInset, UIEdgeInsetsZero)); + webView.frame = CGRectMake(0, 0, 300, 100); + XCTAssertTrue(feq(webView.scrollView.contentInset.bottom, -insetToAdjust.bottom)); + XCTAssertTrue(CGRectEqualToRect(webView.frame, CGRectMake(0, 0, 300, 100))); + } +} + +@end diff --git a/packages/webview_flutter/webview_flutter/example/ios/RunnerTests/Info.plist b/packages/webview_flutter/webview_flutter/example/ios/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/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/webview_flutter/webview_flutter/example/ios/RunnerUITests/FLTWebViewUITests.m b/packages/webview_flutter/webview_flutter/example/ios/RunnerUITests/FLTWebViewUITests.m new file mode 100644 index 000000000000..d193be745972 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/ios/RunnerUITests/FLTWebViewUITests.m @@ -0,0 +1,101 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import XCTest; +@import os.log; + +@interface FLTWebViewUITests : XCTestCase +@property(nonatomic, strong) XCUIApplication* app; +@end + +@implementation FLTWebViewUITests + +- (void)setUp { + self.continueAfterFailure = NO; + + self.app = [[XCUIApplication alloc] init]; + [self.app launch]; +} + +- (void)testUserAgent { + XCUIApplication* app = self.app; + XCUIElement* menu = app.buttons[@"Show menu"]; + if (![menu waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find menu"); + } + [menu tap]; + + XCUIElement* userAgent = app.buttons[@"Show user agent"]; + if (![userAgent waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find Show user agent"); + } + NSPredicate* userAgentPredicate = + [NSPredicate predicateWithFormat:@"label BEGINSWITH 'User Agent: Mozilla/5.0 (iPhone; '"]; + XCUIElement* userAgentPopUp = [app.otherElements elementMatchingPredicate:userAgentPredicate]; + XCTAssertFalse(userAgentPopUp.exists); + [userAgent tap]; + if (![userAgentPopUp waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find user agent pop up"); + } +} + +- (void)testCache { + XCUIApplication* app = self.app; + XCUIElement* menu = app.buttons[@"Show menu"]; + if (![menu waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find menu"); + } + [menu tap]; + + XCUIElement* clearCache = app.buttons[@"Clear cache"]; + if (![clearCache waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find Clear cache"); + } + [clearCache tap]; + + [menu tap]; + + XCUIElement* listCache = app.buttons[@"List cache"]; + if (![listCache waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find List cache"); + } + [listCache tap]; + + XCUIElement* emptyCachePopup = app.otherElements[@"{\"cacheKeys\":[],\"localStorage\":{}}"]; + if (![emptyCachePopup waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find empty cache pop up"); + } + + [menu tap]; + XCUIElement* addCache = app.buttons[@"Add to cache"]; + if (![addCache waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find Add to cache"); + } + [addCache tap]; + [menu tap]; + + if (![listCache waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find List cache"); + } + [listCache tap]; + + XCUIElement* cachePopup = + app.otherElements[@"{\"cacheKeys\":[\"test_caches_entry\"],\"localStorage\":{\"test_" + @"localStorage\":\"dummy_entry\"}}"]; + if (![cachePopup waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find cache pop up"); + } +} + +@end diff --git a/packages/webview_flutter/webview_flutter/example/ios/RunnerUITests/Info.plist b/packages/webview_flutter/webview_flutter/example/ios/RunnerUITests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/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/webview_flutter/webview_flutter/example/lib/main.dart b/packages/webview_flutter/webview_flutter/example/lib/main.dart new file mode 100644 index 000000000000..c456a9691455 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/lib/main.dart @@ -0,0 +1,354 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +void main() => runApp(MaterialApp(home: WebViewExample())); + +const String kNavigationExamplePage = ''' + +Navigation Delegate Example + +

      +The navigation delegate is set to block navigation to the youtube website. +

      + + + +'''; + +class WebViewExample extends StatefulWidget { + @override + _WebViewExampleState createState() => _WebViewExampleState(); +} + +class _WebViewExampleState extends State { + final Completer _controller = + Completer(); + + @override + void initState() { + super.initState(); + if (Platform.isAndroid) WebView.platform = SurfaceAndroidWebView(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Flutter WebView example'), + // This drop down menu demonstrates that Flutter widgets can be shown over the web view. + actions: [ + NavigationControls(_controller.future), + SampleMenu(_controller.future), + ], + ), + // We're using a Builder here so we have a context that is below the Scaffold + // to allow calling Scaffold.of(context) so we can show a snackbar. + body: Builder(builder: (BuildContext context) { + return WebView( + initialUrl: 'https://flutter.dev', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController webViewController) { + _controller.complete(webViewController); + }, + onProgress: (int progress) { + print("WebView is loading (progress : $progress%)"); + }, + javascriptChannels: { + _toasterJavascriptChannel(context), + }, + navigationDelegate: (NavigationRequest request) { + if (request.url.startsWith('https://www.youtube.com/')) { + print('blocking navigation to $request}'); + return NavigationDecision.prevent; + } + print('allowing navigation to $request'); + return NavigationDecision.navigate; + }, + onPageStarted: (String url) { + print('Page started loading: $url'); + }, + onPageFinished: (String url) { + print('Page finished loading: $url'); + }, + gestureNavigationEnabled: true, + ); + }), + floatingActionButton: favoriteButton(), + ); + } + + JavascriptChannel _toasterJavascriptChannel(BuildContext context) { + return JavascriptChannel( + name: 'Toaster', + onMessageReceived: (JavascriptMessage message) { + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + SnackBar(content: Text(message.message)), + ); + }); + } + + Widget favoriteButton() { + return FutureBuilder( + future: _controller.future, + builder: (BuildContext context, + AsyncSnapshot controller) { + if (controller.hasData) { + return FloatingActionButton( + onPressed: () async { + final String url = (await controller.data!.currentUrl())!; + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + SnackBar(content: Text('Favorited $url')), + ); + }, + child: const Icon(Icons.favorite), + ); + } + return Container(); + }); + } +} + +enum MenuOptions { + showUserAgent, + listCookies, + clearCookies, + addToCache, + listCache, + clearCache, + navigationDelegate, +} + +class SampleMenu extends StatelessWidget { + SampleMenu(this.controller); + + final Future controller; + final CookieManager cookieManager = CookieManager(); + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: controller, + builder: + (BuildContext context, AsyncSnapshot controller) { + return PopupMenuButton( + onSelected: (MenuOptions value) { + switch (value) { + case MenuOptions.showUserAgent: + _onShowUserAgent(controller.data!, context); + break; + case MenuOptions.listCookies: + _onListCookies(controller.data!, context); + break; + case MenuOptions.clearCookies: + _onClearCookies(context); + break; + case MenuOptions.addToCache: + _onAddToCache(controller.data!, context); + break; + case MenuOptions.listCache: + _onListCache(controller.data!, context); + break; + case MenuOptions.clearCache: + _onClearCache(controller.data!, context); + break; + case MenuOptions.navigationDelegate: + _onNavigationDelegateExample(controller.data!, context); + break; + } + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: MenuOptions.showUserAgent, + child: const Text('Show user agent'), + enabled: controller.hasData, + ), + const PopupMenuItem( + value: MenuOptions.listCookies, + child: Text('List cookies'), + ), + const PopupMenuItem( + value: MenuOptions.clearCookies, + child: Text('Clear cookies'), + ), + const PopupMenuItem( + value: MenuOptions.addToCache, + child: Text('Add to cache'), + ), + const PopupMenuItem( + value: MenuOptions.listCache, + child: Text('List cache'), + ), + const PopupMenuItem( + value: MenuOptions.clearCache, + child: Text('Clear cache'), + ), + const PopupMenuItem( + value: MenuOptions.navigationDelegate, + child: Text('Navigation Delegate example'), + ), + ], + ); + }, + ); + } + + void _onShowUserAgent( + WebViewController controller, BuildContext context) async { + // Send a message with the user agent string to the Toaster JavaScript channel we registered + // with the WebView. + await controller.evaluateJavascript( + 'Toaster.postMessage("User Agent: " + navigator.userAgent);'); + } + + void _onListCookies( + WebViewController controller, BuildContext context) async { + final String cookies = + await controller.evaluateJavascript('document.cookie'); + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar(SnackBar( + content: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Cookies:'), + _getCookieList(cookies), + ], + ), + )); + } + + void _onAddToCache(WebViewController controller, BuildContext context) async { + await controller.evaluateJavascript( + 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar(const SnackBar( + content: Text('Added a test entry to cache.'), + )); + } + + void _onListCache(WebViewController controller, BuildContext context) async { + await controller.evaluateJavascript('caches.keys()' + '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' + '.then((caches) => Toaster.postMessage(caches))'); + } + + void _onClearCache(WebViewController controller, BuildContext context) async { + await controller.clearCache(); + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar(const SnackBar( + content: Text("Cache cleared."), + )); + } + + void _onClearCookies(BuildContext context) async { + final bool hadCookies = await cookieManager.clearCookies(); + String message = 'There were cookies. Now, they are gone!'; + if (!hadCookies) { + message = 'There are no cookies.'; + } + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar(SnackBar( + content: Text(message), + )); + } + + void _onNavigationDelegateExample( + WebViewController controller, BuildContext context) async { + final String contentBase64 = + base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); + await controller.loadUrl('data:text/html;base64,$contentBase64'); + } + + Widget _getCookieList(String cookies) { + if (cookies == null || cookies == '""') { + return Container(); + } + final List cookieList = cookies.split(';'); + final Iterable cookieWidgets = + cookieList.map((String cookie) => Text(cookie)); + return Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: cookieWidgets.toList(), + ); + } +} + +class NavigationControls extends StatelessWidget { + const NavigationControls(this._webViewControllerFuture) + : assert(_webViewControllerFuture != null); + + final Future _webViewControllerFuture; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _webViewControllerFuture, + builder: + (BuildContext context, AsyncSnapshot snapshot) { + final bool webViewReady = + snapshot.connectionState == ConnectionState.done; + final WebViewController? controller = snapshot.data; + return Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: !webViewReady + ? null + : () async { + if (await controller!.canGoBack()) { + await controller.goBack(); + } else { + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + const SnackBar(content: Text("No back history item")), + ); + return; + } + }, + ), + IconButton( + icon: const Icon(Icons.arrow_forward_ios), + onPressed: !webViewReady + ? null + : () async { + if (await controller!.canGoForward()) { + await controller.goForward(); + } else { + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + const SnackBar( + content: Text("No forward history item")), + ); + return; + } + }, + ), + IconButton( + icon: const Icon(Icons.replay), + onPressed: !webViewReady + ? null + : () { + controller!.reload(); + }, + ), + ], + ); + }, + ); + } +} diff --git a/packages/webview_flutter/webview_flutter/example/pubspec.yaml b/packages/webview_flutter/webview_flutter/example/pubspec.yaml new file mode 100644 index 000000000000..6b668eb96af3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/example/pubspec.yaml @@ -0,0 +1,34 @@ +name: webview_flutter_example +description: Demonstrates how to use the webview_flutter plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" + +dependencies: + flutter: + sdk: flutter + webview_flutter: + # When depending on this package from a real application you should use: + # webview_flutter: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + +dev_dependencies: + espresso: ^0.1.0+2 + flutter_test: + sdk: flutter + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + pedantic: ^1.10.0 + +flutter: + uses-material-design: true + assets: + - assets/sample_audio.ogg + - assets/sample_video.mp4 diff --git a/packages/webview_flutter/webview_flutter/example/test_driver/integration_test.dart b/packages/webview_flutter/webview_flutter/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/webview_flutter/webview_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/webview_flutter/webview_flutter/lib/platform_interface.dart b/packages/webview_flutter/webview_flutter/lib/platform_interface.dart new file mode 100644 index 000000000000..aa7b3a0931e8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/lib/platform_interface.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. + +/// Re-export the classes from the webview_flutter_platform_interface through +/// the `platform_interface.dart` file so we don't accidentally break any +/// non-endorsed existing implementations of the interface. +export 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart' + show + AutoMediaPlaybackPolicy, + CreationParams, + JavascriptChannel, + JavascriptChannelRegistry, + JavascriptMessage, + JavascriptMode, + JavascriptMessageHandler, + WebViewPlatform, + WebViewPlatformCallbacksHandler, + WebViewPlatformController, + WebViewPlatformCreatedCallback, + WebSetting, + WebSettings, + WebResourceError, + WebResourceErrorType; diff --git a/packages/webview_flutter/webview_flutter/lib/src/webview.dart b/packages/webview_flutter/webview_flutter/lib/src/webview.dart new file mode 100644 index 000000000000..7699cc46c5d3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/lib/src/webview.dart @@ -0,0 +1,681 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; +import 'package:webview_flutter_android/webview_android.dart'; +import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; + +import '../platform_interface.dart'; + +/// Optional callback invoked when a web view is first created. [controller] is +/// the [WebViewController] for the created web view. +typedef void WebViewCreatedCallback(WebViewController controller); + +/// Information about a navigation action that is about to be executed. +class NavigationRequest { + NavigationRequest._({required this.url, required this.isForMainFrame}); + + /// The URL that will be loaded if the navigation is executed. + final String url; + + /// Whether the navigation request is to be loaded as the main frame. + final bool isForMainFrame; + + @override + String toString() { + return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)'; + } +} + +/// A decision on how to handle a navigation request. +enum NavigationDecision { + /// Prevent the navigation from taking place. + prevent, + + /// Allow the navigation to take place. + navigate, +} + +/// Decides how to handle a specific navigation request. +/// +/// The returned [NavigationDecision] determines how the navigation described by +/// `navigation` should be handled. +/// +/// See also: [WebView.navigationDelegate]. +typedef FutureOr NavigationDelegate( + NavigationRequest navigation); + +/// Signature for when a [WebView] has started loading a page. +typedef void PageStartedCallback(String url); + +/// Signature for when a [WebView] has finished loading a page. +typedef void PageFinishedCallback(String url); + +/// Signature for when a [WebView] is loading a page. +typedef void PageLoadingCallback(int progress); + +/// Signature for when a [WebView] has failed to load a resource. +typedef void WebResourceErrorCallback(WebResourceError error); + +/// A web view widget for showing html content. +/// +/// There is a known issue that on iOS 13.4 and 13.5, other flutter widgets covering +/// the `WebView` is not able to block the `WebView` from receiving touch events. +/// See https://github.com/flutter/flutter/issues/53490. +class WebView extends StatefulWidget { + /// Creates a new web view. + /// + /// The web view can be controlled using a `WebViewController` that is passed to the + /// `onWebViewCreated` callback once the web view is created. + /// + /// The `javascriptMode` and `autoMediaPlaybackPolicy` parameters must not be null. + const WebView({ + Key? key, + this.onWebViewCreated, + this.initialUrl, + this.javascriptMode = JavascriptMode.disabled, + this.javascriptChannels, + this.navigationDelegate, + this.gestureRecognizers, + this.onPageStarted, + this.onPageFinished, + this.onProgress, + this.onWebResourceError, + this.debuggingEnabled = false, + this.gestureNavigationEnabled = false, + this.userAgent, + this.initialMediaPlaybackPolicy = + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + this.allowsInlineMediaPlayback = false, + }) : assert(javascriptMode != null), + assert(initialMediaPlaybackPolicy != null), + assert(allowsInlineMediaPlayback != null), + super(key: key); + + static WebViewPlatform? _platform; + + /// Sets a custom [WebViewPlatform]. + /// + /// This property can be set to use a custom platform implementation for WebViews. + /// + /// Setting `platform` doesn't affect [WebView]s that were already created. + /// + /// The default value is [AndroidWebView] on Android and [CupertinoWebView] on iOS. + static set platform(WebViewPlatform? platform) { + _platform = platform; + } + + /// The WebView platform that's used by this WebView. + /// + /// The default value is [AndroidWebView] on Android and [CupertinoWebView] on iOS. + static WebViewPlatform get platform { + if (_platform == null) { + switch (defaultTargetPlatform) { + case TargetPlatform.android: + _platform = AndroidWebView(); + break; + case TargetPlatform.iOS: + _platform = CupertinoWebView(); + break; + default: + throw UnsupportedError( + "Trying to use the default webview implementation for $defaultTargetPlatform but there isn't a default one"); + } + } + return _platform!; + } + + /// If not null invoked once the web view is created. + final WebViewCreatedCallback? onWebViewCreated; + + /// Which gestures should be consumed by the web view. + /// + /// It is possible for other gesture recognizers to be competing with the web view on pointer + /// events, e.g if the web view is inside a [ListView] the [ListView] will want to handle + /// vertical drags. The web view will claim gestures that are recognized by any of the + /// recognizers on this list. + /// + /// When this set is empty or null, the web view will only handle pointer events for gestures that + /// were not claimed by any other gesture recognizer. + final Set>? gestureRecognizers; + + /// The initial URL to load. + final String? initialUrl; + + /// Whether Javascript execution is enabled. + final JavascriptMode javascriptMode; + + /// The set of [JavascriptChannel]s available to JavaScript code running in the web view. + /// + /// For each [JavascriptChannel] in the set, a channel object is made available for the + /// JavaScript code in a window property named [JavascriptChannel.name]. + /// The JavaScript code can then call `postMessage` on that object to send a message that will be + /// passed to [JavascriptChannel.onMessageReceived]. + /// + /// For example for the following JavascriptChannel: + /// + /// ```dart + /// JavascriptChannel(name: 'Print', onMessageReceived: (JavascriptMessage message) { print(message.message); }); + /// ``` + /// + /// JavaScript code can call: + /// + /// ```javascript + /// Print.postMessage('Hello'); + /// ``` + /// + /// To asynchronously invoke the message handler which will print the message to standard output. + /// + /// Adding a new JavaScript channel only takes affect after the next page is loaded. + /// + /// Set values must not be null. A [JavascriptChannel.name] cannot be the same for multiple + /// channels in the list. + /// + /// A null value is equivalent to an empty set. + final Set? javascriptChannels; + + /// A delegate function that decides how to handle navigation actions. + /// + /// When a navigation is initiated by the WebView (e.g when a user clicks a link) + /// this delegate is called and has to decide how to proceed with the navigation. + /// + /// See [NavigationDecision] for possible decisions the delegate can take. + /// + /// When null all navigation actions are allowed. + /// + /// Caveats on Android: + /// + /// * Navigation actions targeted to the main frame can be intercepted, + /// navigation actions targeted to subframes are allowed regardless of the value + /// returned by this delegate. + /// * Setting a navigationDelegate makes the WebView treat all navigations as if they were + /// triggered by a user gesture, this disables some of Chromium's security mechanisms. + /// A navigationDelegate should only be set when loading trusted content. + /// * On Android WebView versions earlier than 67(most devices running at least Android L+ should have + /// a later version): + /// * When a navigationDelegate is set pages with frames are not properly handled by the + /// webview, and frames will be opened in the main frame. + /// * When a navigationDelegate is set HTTP requests do not include the HTTP referer header. + final NavigationDelegate? navigationDelegate; + + /// Controls whether inline playback of HTML5 videos is allowed on iOS. + /// + /// This field is ignored on Android because Android allows it by default. + /// + /// By default `allowsInlineMediaPlayback` is false. + final bool allowsInlineMediaPlayback; + + /// Invoked when a page starts loading. + final PageStartedCallback? onPageStarted; + + /// Invoked when a page has finished loading. + /// + /// This is invoked only for the main frame. + /// + /// When [onPageFinished] is invoked on Android, the page being rendered may + /// not be updated yet. + /// + /// When invoked on iOS or Android, any Javascript code that is embedded + /// directly in the HTML has been loaded and code injected with + /// [WebViewController.evaluateJavascript] can assume this. + final PageFinishedCallback? onPageFinished; + + /// Invoked when a page is loading. + final PageLoadingCallback? onProgress; + + /// Invoked when a web resource has failed to load. + /// + /// This callback is only called for the main page. + final WebResourceErrorCallback? onWebResourceError; + + /// Controls whether WebView debugging is enabled. + /// + /// Setting this to true enables [WebView debugging on Android](https://developers.google.com/web/tools/chrome-devtools/remote-debugging/). + /// + /// WebView debugging is enabled by default in dev builds on iOS. + /// + /// To debug WebViews on iOS: + /// - Enable developer options (Open Safari, go to Preferences -> Advanced and make sure "Show Develop Menu in Menubar" is on.) + /// - From the Menu-bar (of Safari) select Develop -> iPhone Simulator -> + /// + /// By default `debuggingEnabled` is false. + final bool debuggingEnabled; + + /// A Boolean value indicating whether horizontal swipe gestures will trigger back-forward list navigations. + /// + /// This only works on iOS. + /// + /// By default `gestureNavigationEnabled` is false. + final bool gestureNavigationEnabled; + + /// The value used for the HTTP User-Agent: request header. + /// + /// When null the platform's webview default is used for the User-Agent header. + /// + /// When the [WebView] is rebuilt with a different `userAgent`, the page reloads and the request uses the new User Agent. + /// + /// When [WebViewController.goBack] is called after changing `userAgent` the previous `userAgent` value is used until the page is reloaded. + /// + /// This field is ignored on iOS versions prior to 9 as the platform does not support a custom + /// user agent. + /// + /// By default `userAgent` is null. + final String? userAgent; + + /// Which restrictions apply on automatic media playback. + /// + /// This initial value is applied to the platform's webview upon creation. Any following + /// changes to this parameter are ignored (as long as the state of the [WebView] is preserved). + /// + /// The default policy is [AutoMediaPlaybackPolicy.require_user_action_for_all_media_types]. + final AutoMediaPlaybackPolicy initialMediaPlaybackPolicy; + + @override + State createState() => _WebViewState(); +} + +class _WebViewState extends State { + final Completer _controller = + Completer(); + + late JavascriptChannelRegistry _javascriptChannelRegistry; + late _PlatformCallbacksHandler _platformCallbacksHandler; + + @override + Widget build(BuildContext context) { + return WebView.platform.build( + context: context, + onWebViewPlatformCreated: _onWebViewPlatformCreated, + webViewPlatformCallbacksHandler: _platformCallbacksHandler, + javascriptChannelRegistry: _javascriptChannelRegistry, + gestureRecognizers: widget.gestureRecognizers, + creationParams: _creationParamsfromWidget(widget), + ); + } + + @override + void initState() { + super.initState(); + _assertJavascriptChannelNamesAreUnique(); + _platformCallbacksHandler = _PlatformCallbacksHandler(widget); + _javascriptChannelRegistry = + JavascriptChannelRegistry(widget.javascriptChannels); + } + + @override + void didUpdateWidget(WebView oldWidget) { + super.didUpdateWidget(oldWidget); + _assertJavascriptChannelNamesAreUnique(); + _controller.future.then((WebViewController controller) { + _platformCallbacksHandler._widget = widget; + controller._updateWidget(widget); + }); + } + + void _onWebViewPlatformCreated(WebViewPlatformController? webViewPlatform) { + final WebViewController controller = WebViewController._( + widget, + webViewPlatform!, + _javascriptChannelRegistry, + ); + _controller.complete(controller); + if (widget.onWebViewCreated != null) { + widget.onWebViewCreated!(controller); + } + } + + void _assertJavascriptChannelNamesAreUnique() { + if (widget.javascriptChannels == null || + widget.javascriptChannels!.isEmpty) { + return; + } + assert(_extractChannelNames(widget.javascriptChannels).length == + widget.javascriptChannels!.length); + } +} + +CreationParams _creationParamsfromWidget(WebView widget) { + return CreationParams( + initialUrl: widget.initialUrl, + webSettings: _webSettingsFromWidget(widget), + javascriptChannelNames: _extractChannelNames(widget.javascriptChannels), + userAgent: widget.userAgent, + autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, + ); +} + +WebSettings _webSettingsFromWidget(WebView widget) { + return WebSettings( + javascriptMode: widget.javascriptMode, + hasNavigationDelegate: widget.navigationDelegate != null, + hasProgressTracking: widget.onProgress != null, + debuggingEnabled: widget.debuggingEnabled, + gestureNavigationEnabled: widget.gestureNavigationEnabled, + allowsInlineMediaPlayback: widget.allowsInlineMediaPlayback, + userAgent: WebSetting.of(widget.userAgent), + ); +} + +// This method assumes that no fields in `currentValue` are null. +WebSettings _clearUnchangedWebSettings( + WebSettings currentValue, WebSettings newValue) { + assert(currentValue.javascriptMode != null); + assert(currentValue.hasNavigationDelegate != null); + assert(currentValue.hasProgressTracking != null); + assert(currentValue.debuggingEnabled != null); + assert(currentValue.userAgent != null); + assert(newValue.javascriptMode != null); + assert(newValue.hasNavigationDelegate != null); + assert(newValue.debuggingEnabled != null); + assert(newValue.userAgent != null); + + JavascriptMode? javascriptMode; + bool? hasNavigationDelegate; + bool? hasProgressTracking; + bool? debuggingEnabled; + WebSetting userAgent = WebSetting.absent(); + if (currentValue.javascriptMode != newValue.javascriptMode) { + javascriptMode = newValue.javascriptMode; + } + if (currentValue.hasNavigationDelegate != newValue.hasNavigationDelegate) { + hasNavigationDelegate = newValue.hasNavigationDelegate; + } + if (currentValue.hasProgressTracking != newValue.hasProgressTracking) { + hasProgressTracking = newValue.hasProgressTracking; + } + if (currentValue.debuggingEnabled != newValue.debuggingEnabled) { + debuggingEnabled = newValue.debuggingEnabled; + } + if (currentValue.userAgent != newValue.userAgent) { + userAgent = newValue.userAgent; + } + + return WebSettings( + javascriptMode: javascriptMode, + hasNavigationDelegate: hasNavigationDelegate, + hasProgressTracking: hasProgressTracking, + debuggingEnabled: debuggingEnabled, + userAgent: userAgent, + ); +} + +Set _extractChannelNames(Set? channels) { + final Set channelNames = channels == null + ? {} + : channels.map((JavascriptChannel channel) => channel.name).toSet(); + return channelNames; +} + +class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { + _PlatformCallbacksHandler(this._widget); + + WebView _widget; + + @override + FutureOr onNavigationRequest({ + required String url, + required bool isForMainFrame, + }) async { + final NavigationRequest request = + NavigationRequest._(url: url, isForMainFrame: isForMainFrame); + final bool allowNavigation = _widget.navigationDelegate == null || + await _widget.navigationDelegate!(request) == + NavigationDecision.navigate; + return allowNavigation; + } + + @override + void onPageStarted(String url) { + if (_widget.onPageStarted != null) { + _widget.onPageStarted!(url); + } + } + + @override + void onPageFinished(String url) { + if (_widget.onPageFinished != null) { + _widget.onPageFinished!(url); + } + } + + @override + void onProgress(int progress) { + if (_widget.onProgress != null) { + _widget.onProgress!(progress); + } + } + + void onWebResourceError(WebResourceError error) { + if (_widget.onWebResourceError != null) { + _widget.onWebResourceError!(error); + } + } +} + +/// Controls a [WebView]. +/// +/// A [WebViewController] instance can be obtained by setting the [WebView.onWebViewCreated] +/// callback for a [WebView] widget. +class WebViewController { + WebViewController._( + this._widget, + this._webViewPlatformController, + this._javascriptChannelRegistry, + ) : assert(_webViewPlatformController != null) { + _settings = _webSettingsFromWidget(_widget); + } + + final WebViewPlatformController _webViewPlatformController; + final JavascriptChannelRegistry _javascriptChannelRegistry; + + late WebSettings _settings; + + WebView _widget; + + /// Loads the specified URL. + /// + /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will + /// be added as key value pairs of HTTP headers for the request. + /// + /// `url` must not be null. + /// + /// Throws an ArgumentError if `url` is not a valid URL string. + Future loadUrl( + String url, { + Map? headers, + }) async { + assert(url != null); + _validateUrlString(url); + return _webViewPlatformController.loadUrl(url, headers); + } + + /// Accessor to the current URL that the WebView is displaying. + /// + /// If [WebView.initialUrl] was never specified, returns `null`. + /// Note that this operation is asynchronous, and it is possible that the + /// current URL changes again by the time this function returns (in other + /// words, by the time this future completes, the WebView may be displaying a + /// different URL). + Future currentUrl() { + return _webViewPlatformController.currentUrl(); + } + + /// Checks whether there's a back history item. + /// + /// Note that this operation is asynchronous, and it is possible that the "canGoBack" state has + /// changed by the time the future completed. + Future canGoBack() { + return _webViewPlatformController.canGoBack(); + } + + /// Checks whether there's a forward history item. + /// + /// Note that this operation is asynchronous, and it is possible that the "canGoForward" state has + /// changed by the time the future completed. + Future canGoForward() { + return _webViewPlatformController.canGoForward(); + } + + /// Goes back in the history of this WebView. + /// + /// If there is no back history item this is a no-op. + Future goBack() { + return _webViewPlatformController.goBack(); + } + + /// Goes forward in the history of this WebView. + /// + /// If there is no forward history item this is a no-op. + Future goForward() { + return _webViewPlatformController.goForward(); + } + + /// Reloads the current URL. + Future reload() { + return _webViewPlatformController.reload(); + } + + /// Clears all caches used by the [WebView]. + /// + /// The following caches are cleared: + /// 1. Browser HTTP Cache. + /// 2. [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api) caches. + /// These are not yet supported in iOS WkWebView. Service workers tend to use this cache. + /// 3. Application cache. + /// 4. Local Storage. + /// + /// Note: Calling this method also triggers a reload. + Future clearCache() async { + await _webViewPlatformController.clearCache(); + return reload(); + } + + Future _updateJavascriptChannels( + Set? newChannels) async { + final Set currentChannels = + _javascriptChannelRegistry.channels.keys.toSet(); + final Set newChannelNames = _extractChannelNames(newChannels); + final Set channelsToAdd = + newChannelNames.difference(currentChannels); + final Set channelsToRemove = + currentChannels.difference(newChannelNames); + if (channelsToRemove.isNotEmpty) { + await _webViewPlatformController + .removeJavascriptChannels(channelsToRemove); + } + if (channelsToAdd.isNotEmpty) { + await _webViewPlatformController.addJavascriptChannels(channelsToAdd); + } + _javascriptChannelRegistry.updateJavascriptChannelsFromSet(newChannels); + } + + Future _updateWidget(WebView widget) async { + _widget = widget; + await _updateSettings(_webSettingsFromWidget(widget)); + await _updateJavascriptChannels(widget.javascriptChannels); + } + + Future _updateSettings(WebSettings newSettings) { + final WebSettings update = + _clearUnchangedWebSettings(_settings, newSettings); + _settings = newSettings; + return _webViewPlatformController.updateSettings(update); + } + + /// Evaluates a JavaScript expression in the context of the current page. + /// + /// On Android returns the evaluation result as a JSON formatted string. + /// + /// On iOS depending on the value type the return value would be one of: + /// + /// - For primitive JavaScript types: the value string formatted (e.g JavaScript 100 returns '100'). + /// - For JavaScript arrays of supported types: a string formatted NSArray(e.g '(1,2,3), note that the string for NSArray is formatted and might contain newlines and extra spaces.'). + /// - Other non-primitive types are not supported on iOS and will complete the Future with an error. + /// + /// The Future completes with an error if a JavaScript error occurred, or on iOS, if the type of the + /// evaluated expression is not supported as described above. + /// + /// When evaluating Javascript in a [WebView], it is best practice to wait for + /// the [WebView.onPageFinished] callback. This guarantees all the Javascript + /// embedded in the main frame HTML has been loaded. + Future evaluateJavascript(String javascriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); + } + // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. + // https://github.com/flutter/flutter/issues/26431 + // ignore: strong_mode_implicit_dynamic_method + return _webViewPlatformController.evaluateJavascript(javascriptString); + } + + /// Returns the title of the currently loaded page. + Future getTitle() { + return _webViewPlatformController.getTitle(); + } + + /// Sets the WebView's content scroll position. + /// + /// The parameters `x` and `y` specify the scroll position in WebView pixels. + Future scrollTo(int x, int y) { + return _webViewPlatformController.scrollTo(x, y); + } + + /// Move the scrolled position of this view. + /// + /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by horizontally and vertically respectively. + Future scrollBy(int x, int y) { + return _webViewPlatformController.scrollBy(x, y); + } + + /// Return the horizontal scroll position, in WebView pixels, of this view. + /// + /// Scroll position is measured from left. + Future getScrollX() { + return _webViewPlatformController.getScrollX(); + } + + /// Return the vertical scroll position, in WebView pixels, of this view. + /// + /// Scroll position is measured from top. + Future getScrollY() { + return _webViewPlatformController.getScrollY(); + } +} + +/// Manages cookies pertaining to all [WebView]s. +class CookieManager { + /// Creates a [CookieManager] -- returns the instance if it's already been called. + factory CookieManager() { + return _instance ??= CookieManager._(); + } + + CookieManager._(); + + static CookieManager? _instance; + + /// Clears all cookies for all [WebView] instances. + /// + /// This is a no op on iOS version smaller than 9. + /// + /// Returns true if cookies were present before clearing, else false. + Future clearCookies() => WebView.platform.clearCookies(); +} + +// Throws an ArgumentError if `url` is not a valid URL string. +void _validateUrlString(String url) { + try { + final Uri uri = Uri.parse(url); + if (uri.scheme.isEmpty) { + throw ArgumentError('Missing scheme in URL string: "$url"'); + } + } on FormatException catch (e) { + throw ArgumentError(e); + } +} diff --git a/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart new file mode 100644 index 000000000000..ba38771e5107 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/lib/webview_flutter.dart @@ -0,0 +1,10 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'package:webview_flutter_android/webview_android.dart'; +export 'package:webview_flutter_android/webview_surface_android.dart'; +export 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; + +export 'platform_interface.dart'; +export 'src/webview.dart'; diff --git a/packages/webview_flutter/webview_flutter/pubspec.yaml b/packages/webview_flutter/webview_flutter/pubspec.yaml new file mode 100644 index 000000000000..4206789cb1b3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter/pubspec.yaml @@ -0,0 +1,31 @@ +name: webview_flutter +description: A Flutter plugin that provides a WebView widget on Android and iOS. +repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 +version: 2.1.1 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" + +flutter: + plugin: + platforms: + android: + default_package: webview_flutter_android + ios: + default_package: webview_flutter_wkwebview + +dependencies: + flutter: + sdk: flutter + webview_flutter_platform_interface: ^1.0.0 + webview_flutter_android: ^2.0.13 + webview_flutter_wkwebview: ^2.0.13 + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + pedantic: ^1.10.0 diff --git a/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart new file mode 100644 index 000000000000..f7d09266a64b --- /dev/null +++ b/packages/webview_flutter/webview_flutter/test/webview_flutter_test.dart @@ -0,0 +1,1261 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:flutter/services.dart'; +import 'package:flutter/src/foundation/basic_types.dart'; +import 'package:flutter/src/gestures/recognizer.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +typedef void VoidCallback(); + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final _FakePlatformViewsController fakePlatformViewsController = + _FakePlatformViewsController(); + + final _FakeCookieManager _fakeCookieManager = _FakeCookieManager(); + + setUpAll(() { + SystemChannels.platform_views.setMockMethodCallHandler( + fakePlatformViewsController.fakePlatformViewsMethodHandler); + SystemChannels.platform + .setMockMethodCallHandler(_fakeCookieManager.onMethodCall); + }); + + setUp(() { + fakePlatformViewsController.reset(); + _fakeCookieManager.reset(); + }); + + testWidgets('Create WebView', (WidgetTester tester) async { + await tester.pumpWidget(const WebView()); + }); + + testWidgets('Initial url', (WidgetTester tester) async { + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(await controller.currentUrl(), 'https://youtube.com'); + }); + + testWidgets('Javascript mode', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + javascriptMode: JavascriptMode.unrestricted, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + expect(platformWebView.javascriptMode, JavascriptMode.unrestricted); + + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + javascriptMode: JavascriptMode.disabled, + )); + expect(platformWebView.javascriptMode, JavascriptMode.disabled); + }); + + testWidgets('Load url', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + await controller!.loadUrl('https://flutter.io'); + + expect(await controller!.currentUrl(), 'https://flutter.io'); + }); + + testWidgets('Invalid urls', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + expect(await controller!.currentUrl(), isNull); + + expect(() => controller!.loadUrl(''), throwsA(anything)); + expect(await controller!.currentUrl(), isNull); + + // Missing schema. + expect(() => controller!.loadUrl('flutter.io'), throwsA(anything)); + expect(await controller!.currentUrl(), isNull); + }); + + testWidgets('Headers in loadUrl', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + final Map headers = { + 'CACHE-CONTROL': 'ABC' + }; + await controller!.loadUrl('https://flutter.io', headers: headers); + expect(await controller!.currentUrl(), equals('https://flutter.io')); + }); + + testWidgets("Can't go back before loading a page", + (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + final bool canGoBackNoPageLoaded = await controller!.canGoBack(); + + expect(canGoBackNoPageLoaded, false); + }); + + testWidgets("Clear Cache", (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + expect(fakePlatformViewsController.lastCreatedView!.hasCache, true); + + await controller!.clearCache(); + + expect(fakePlatformViewsController.lastCreatedView!.hasCache, false); + }); + + testWidgets("Can't go back with no history", (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + final bool canGoBackFirstPageLoaded = await controller!.canGoBack(); + + expect(canGoBackFirstPageLoaded, false); + }); + + testWidgets('Can go back', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + await controller!.loadUrl('https://www.google.com'); + final bool canGoBackSecondPageLoaded = await controller!.canGoBack(); + + expect(canGoBackSecondPageLoaded, true); + }); + + testWidgets("Can't go forward before loading a page", + (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + final bool canGoForwardNoPageLoaded = await controller!.canGoForward(); + + expect(canGoForwardNoPageLoaded, false); + }); + + testWidgets("Can't go forward with no history", (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + final bool canGoForwardFirstPageLoaded = await controller!.canGoForward(); + + expect(canGoForwardFirstPageLoaded, false); + }); + + testWidgets('Can go forward', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + await controller!.loadUrl('https://youtube.com'); + await controller!.goBack(); + final bool canGoForwardFirstPageBacked = await controller!.canGoForward(); + + expect(canGoForwardFirstPageBacked, true); + }); + + testWidgets('Go back', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + expect(await controller!.currentUrl(), 'https://youtube.com'); + + await controller!.loadUrl('https://flutter.io'); + + expect(await controller!.currentUrl(), 'https://flutter.io'); + + await controller!.goBack(); + + expect(await controller!.currentUrl(), 'https://youtube.com'); + }); + + testWidgets('Go forward', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + expect(await controller!.currentUrl(), 'https://youtube.com'); + + await controller!.loadUrl('https://flutter.io'); + + expect(await controller!.currentUrl(), 'https://flutter.io'); + + await controller!.goBack(); + + expect(await controller!.currentUrl(), 'https://youtube.com'); + + await controller!.goForward(); + + expect(await controller!.currentUrl(), 'https://flutter.io'); + }); + + testWidgets('Current URL', (WidgetTester tester) async { + WebViewController? controller; + await tester.pumpWidget( + WebView( + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + expect(controller, isNotNull); + + // Test a WebView without an explicitly set first URL. + expect(await controller!.currentUrl(), isNull); + + await controller!.loadUrl('https://youtube.com'); + expect(await controller!.currentUrl(), 'https://youtube.com'); + + await controller!.loadUrl('https://flutter.io'); + expect(await controller!.currentUrl(), 'https://flutter.io'); + + await controller!.goBack(); + expect(await controller!.currentUrl(), 'https://youtube.com'); + }); + + testWidgets('Reload url', (WidgetTester tester) async { + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + expect(platformWebView.currentUrl, 'https://flutter.io'); + expect(platformWebView.amountOfReloadsOnCurrentUrl, 0); + + await controller.reload(); + + expect(platformWebView.currentUrl, 'https://flutter.io'); + expect(platformWebView.amountOfReloadsOnCurrentUrl, 1); + + await controller.loadUrl('https://youtube.com'); + + expect(platformWebView.amountOfReloadsOnCurrentUrl, 0); + }); + + testWidgets('evaluate Javascript', (WidgetTester tester) async { + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + expect( + await controller.evaluateJavascript("fake js string"), "fake js string", + reason: 'should get the argument'); + }); + + testWidgets('evaluate Javascript with JavascriptMode disabled', + (WidgetTester tester) async { + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://flutter.io', + javascriptMode: JavascriptMode.disabled, + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + expect( + () => controller.evaluateJavascript('fake js string'), + throwsA(anything), + ); + }); + + testWidgets('Cookies can be cleared once', (WidgetTester tester) async { + await tester.pumpWidget( + const WebView( + initialUrl: 'https://flutter.io', + ), + ); + final CookieManager cookieManager = CookieManager(); + final bool hasCookies = await cookieManager.clearCookies(); + expect(hasCookies, true); + }); + + testWidgets('Second cookie clear does not have cookies', + (WidgetTester tester) async { + await tester.pumpWidget( + const WebView( + initialUrl: 'https://flutter.io', + ), + ); + final CookieManager cookieManager = CookieManager(); + final bool hasCookies = await cookieManager.clearCookies(); + expect(hasCookies, true); + final bool hasCookiesSecond = await cookieManager.clearCookies(); + expect(hasCookiesSecond, false); + }); + + testWidgets('Initial JavaScript channels', (WidgetTester tester) async { + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + javascriptChannels: { + JavascriptChannel( + name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), + }, + ), + ); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + expect(platformWebView.javascriptChannelNames, + unorderedEquals(['Tts', 'Alarm'])); + }); + + test('Only valid JavaScript channel names are allowed', () { + final JavascriptMessageHandler noOp = (JavascriptMessage msg) {}; + JavascriptChannel(name: 'Tts1', onMessageReceived: noOp); + JavascriptChannel(name: '_Alarm', onMessageReceived: noOp); + JavascriptChannel(name: 'foo_bar_', onMessageReceived: noOp); + + VoidCallback createChannel(String name) { + return () { + JavascriptChannel(name: name, onMessageReceived: noOp); + }; + } + + expect(createChannel('1Alarm'), throwsAssertionError); + expect(createChannel('foo.bar'), throwsAssertionError); + expect(createChannel(''), throwsAssertionError); + }); + + testWidgets('Unique JavaScript channel names are required', + (WidgetTester tester) async { + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + javascriptChannels: { + JavascriptChannel( + name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), + }, + ), + ); + expect(tester.takeException(), isNot(null)); + }); + + testWidgets('JavaScript channels update', (WidgetTester tester) async { + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + javascriptChannels: { + JavascriptChannel( + name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Alarm', onMessageReceived: (JavascriptMessage msg) {}), + }, + ), + ); + + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + javascriptChannels: { + JavascriptChannel( + name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Alarm2', onMessageReceived: (JavascriptMessage msg) {}), + JavascriptChannel( + name: 'Alarm3', onMessageReceived: (JavascriptMessage msg) {}), + }, + ), + ); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + expect(platformWebView.javascriptChannelNames, + unorderedEquals(['Tts', 'Alarm2', 'Alarm3'])); + }); + + testWidgets('Remove all JavaScript channels and then add', + (WidgetTester tester) async { + // This covers a specific bug we had where after updating javascriptChannels to null, + // updating it again with a subset of the previously registered channels fails as the + // widget's cache of current channel wasn't properly updated when updating javascriptChannels to + // null. + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + javascriptChannels: { + JavascriptChannel( + name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + }, + ), + ); + + await tester.pumpWidget( + const WebView( + initialUrl: 'https://youtube.com', + ), + ); + + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + javascriptChannels: { + JavascriptChannel( + name: 'Tts', onMessageReceived: (JavascriptMessage msg) {}), + }, + ), + ); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + expect(platformWebView.javascriptChannelNames, + unorderedEquals(['Tts'])); + }); + + testWidgets('JavaScript channel messages', (WidgetTester tester) async { + final List ttsMessagesReceived = []; + final List alarmMessagesReceived = []; + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + javascriptChannels: { + JavascriptChannel( + name: 'Tts', + onMessageReceived: (JavascriptMessage msg) { + ttsMessagesReceived.add(msg.message); + }), + JavascriptChannel( + name: 'Alarm', + onMessageReceived: (JavascriptMessage msg) { + alarmMessagesReceived.add(msg.message); + }), + }, + ), + ); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + expect(ttsMessagesReceived, isEmpty); + expect(alarmMessagesReceived, isEmpty); + + platformWebView.fakeJavascriptPostMessage('Tts', 'Hello'); + platformWebView.fakeJavascriptPostMessage('Tts', 'World'); + + expect(ttsMessagesReceived, ['Hello', 'World']); + }); + + group('$PageStartedCallback', () { + testWidgets('onPageStarted is not null', (WidgetTester tester) async { + String? returnedUrl; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageStarted: (String url) { + returnedUrl = url; + }, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + platformWebView.fakeOnPageStartedCallback(); + + expect(platformWebView.currentUrl, returnedUrl); + }); + + testWidgets('onPageStarted is null', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + onPageStarted: null, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + // The platform side will always invoke a call for onPageStarted. This is + // to test that it does not crash on a null callback. + platformWebView.fakeOnPageStartedCallback(); + }); + + testWidgets('onPageStarted changed', (WidgetTester tester) async { + String? returnedUrl; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageStarted: (String url) {}, + )); + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageStarted: (String url) { + returnedUrl = url; + }, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + platformWebView.fakeOnPageStartedCallback(); + + expect(platformWebView.currentUrl, returnedUrl); + }); + }); + + group('$PageFinishedCallback', () { + testWidgets('onPageFinished is not null', (WidgetTester tester) async { + String? returnedUrl; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageFinished: (String url) { + returnedUrl = url; + }, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + platformWebView.fakeOnPageFinishedCallback(); + + expect(platformWebView.currentUrl, returnedUrl); + }); + + testWidgets('onPageFinished is null', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + onPageFinished: null, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + // The platform side will always invoke a call for onPageFinished. This is + // to test that it does not crash on a null callback. + platformWebView.fakeOnPageFinishedCallback(); + }); + + testWidgets('onPageFinished changed', (WidgetTester tester) async { + String? returnedUrl; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageFinished: (String url) {}, + )); + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onPageFinished: (String url) { + returnedUrl = url; + }, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + platformWebView.fakeOnPageFinishedCallback(); + + expect(platformWebView.currentUrl, returnedUrl); + }); + }); + + group('$PageLoadingCallback', () { + testWidgets('onLoadingProgress is not null', (WidgetTester tester) async { + int? loadingProgress; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onProgress: (int progress) { + loadingProgress = progress; + }, + )); + + final FakePlatformWebView? platformWebView = + fakePlatformViewsController.lastCreatedView; + + platformWebView?.fakeOnProgressCallback(50); + + expect(loadingProgress, 50); + }); + + testWidgets('onLoadingProgress is null', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + onProgress: null, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + // This is to test that it does not crash on a null callback. + platformWebView.fakeOnProgressCallback(50); + }); + + testWidgets('onLoadingProgress changed', (WidgetTester tester) async { + int? loadingProgress; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onProgress: (int progress) {}, + )); + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + onProgress: (int progress) { + loadingProgress = progress; + }, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + platformWebView.fakeOnProgressCallback(50); + + expect(loadingProgress, 50); + }); + }); + + group('navigationDelegate', () { + testWidgets('hasNavigationDelegate', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + expect(platformWebView.hasNavigationDelegate, false); + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + navigationDelegate: (NavigationRequest r) => + NavigationDecision.navigate, + )); + + expect(platformWebView.hasNavigationDelegate, true); + }); + + testWidgets('Block navigation', (WidgetTester tester) async { + final List navigationRequests = []; + + await tester.pumpWidget(WebView( + initialUrl: 'https://youtube.com', + navigationDelegate: (NavigationRequest request) { + navigationRequests.add(request); + // Only allow navigating to https://flutter.dev + return request.url == 'https://flutter.dev' + ? NavigationDecision.navigate + : NavigationDecision.prevent; + })); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + expect(platformWebView.hasNavigationDelegate, true); + + platformWebView.fakeNavigate('https://www.google.com'); + // The navigation delegate only allows navigation to https://flutter.dev + // so we should still be in https://youtube.com. + expect(platformWebView.currentUrl, 'https://youtube.com'); + expect(navigationRequests.length, 1); + expect(navigationRequests[0].url, 'https://www.google.com'); + expect(navigationRequests[0].isForMainFrame, true); + + platformWebView.fakeNavigate('https://flutter.dev'); + await tester.pump(); + expect(platformWebView.currentUrl, 'https://flutter.dev'); + }); + }); + + group('debuggingEnabled', () { + testWidgets('enable debugging', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + debuggingEnabled: true, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + expect(platformWebView.debuggingEnabled, true); + }); + + testWidgets('defaults to false', (WidgetTester tester) async { + await tester.pumpWidget(const WebView()); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + expect(platformWebView.debuggingEnabled, false); + }); + + testWidgets('can be changed', (WidgetTester tester) async { + final GlobalKey key = GlobalKey(); + await tester.pumpWidget(WebView(key: key)); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + await tester.pumpWidget(WebView( + key: key, + debuggingEnabled: true, + )); + + expect(platformWebView.debuggingEnabled, true); + + await tester.pumpWidget(WebView( + key: key, + debuggingEnabled: false, + )); + + expect(platformWebView.debuggingEnabled, false); + }); + }); + + group('Custom platform implementation', () { + setUpAll(() { + WebView.platform = MyWebViewPlatform(); + }); + tearDownAll(() { + WebView.platform = null; + }); + + testWidgets('creation', (WidgetTester tester) async { + await tester.pumpWidget( + const WebView( + initialUrl: 'https://youtube.com', + gestureNavigationEnabled: true, + ), + ); + + final MyWebViewPlatform builder = WebView.platform as MyWebViewPlatform; + final MyWebViewPlatformController platform = builder.lastPlatformBuilt!; + + expect( + platform.creationParams, + MatchesCreationParams(CreationParams( + initialUrl: 'https://youtube.com', + webSettings: WebSettings( + javascriptMode: JavascriptMode.disabled, + hasNavigationDelegate: false, + debuggingEnabled: false, + userAgent: WebSetting.of(null), + gestureNavigationEnabled: true, + ), + ))); + }); + + testWidgets('loadUrl', (WidgetTester tester) async { + late WebViewController controller; + await tester.pumpWidget( + WebView( + initialUrl: 'https://youtube.com', + onWebViewCreated: (WebViewController webViewController) { + controller = webViewController; + }, + ), + ); + + final MyWebViewPlatform builder = WebView.platform as MyWebViewPlatform; + final MyWebViewPlatformController platform = builder.lastPlatformBuilt!; + + final Map headers = { + 'header': 'value', + }; + + await controller.loadUrl('https://google.com', headers: headers); + + expect(platform.lastUrlLoaded, 'https://google.com'); + expect(platform.lastRequestHeaders, headers); + }); + }); + testWidgets('Set UserAgent', (WidgetTester tester) async { + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + javascriptMode: JavascriptMode.unrestricted, + )); + + final FakePlatformWebView platformWebView = + fakePlatformViewsController.lastCreatedView!; + + expect(platformWebView.userAgent, isNull); + + await tester.pumpWidget(const WebView( + initialUrl: 'https://youtube.com', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'UA', + )); + + expect(platformWebView.userAgent, 'UA'); + }); +} + +class FakePlatformWebView { + FakePlatformWebView(int? id, Map params) { + if (params.containsKey('initialUrl')) { + final String? initialUrl = params['initialUrl']; + if (initialUrl != null) { + history.add(initialUrl); + currentPosition++; + } + } + if (params.containsKey('javascriptChannelNames')) { + javascriptChannelNames = + List.from(params['javascriptChannelNames']); + } + javascriptMode = JavascriptMode.values[params['settings']['jsMode']]; + hasNavigationDelegate = + params['settings']['hasNavigationDelegate'] ?? false; + debuggingEnabled = params['settings']['debuggingEnabled']; + userAgent = params['settings']['userAgent']; + channel = MethodChannel( + 'plugins.flutter.io/webview_$id', const StandardMethodCodec()); + channel.setMockMethodCallHandler(onMethodCall); + } + + late MethodChannel channel; + + List history = []; + int currentPosition = -1; + int amountOfReloadsOnCurrentUrl = 0; + bool hasCache = true; + + String? get currentUrl => history.isEmpty ? null : history[currentPosition]; + JavascriptMode? javascriptMode; + List? javascriptChannelNames; + + bool? hasNavigationDelegate; + bool? debuggingEnabled; + String? userAgent; + + Future onMethodCall(MethodCall call) { + switch (call.method) { + case 'loadUrl': + final Map request = call.arguments; + _loadUrl(request['url']); + return Future.sync(() {}); + case 'updateSettings': + if (call.arguments['jsMode'] != null) { + javascriptMode = JavascriptMode.values[call.arguments['jsMode']]; + } + if (call.arguments['hasNavigationDelegate'] != null) { + hasNavigationDelegate = call.arguments['hasNavigationDelegate']; + } + if (call.arguments['debuggingEnabled'] != null) { + debuggingEnabled = call.arguments['debuggingEnabled']; + } + userAgent = call.arguments['userAgent']; + break; + case 'canGoBack': + return Future.sync(() => currentPosition > 0); + case 'canGoForward': + return Future.sync(() => currentPosition < history.length - 1); + case 'goBack': + currentPosition = max(-1, currentPosition - 1); + return Future.sync(() {}); + case 'goForward': + currentPosition = min(history.length - 1, currentPosition + 1); + return Future.sync(() {}); + case 'reload': + amountOfReloadsOnCurrentUrl++; + return Future.sync(() {}); + case 'currentUrl': + return Future.value(currentUrl); + case 'evaluateJavascript': + return Future.value(call.arguments); + case 'addJavascriptChannels': + final List channelNames = List.from(call.arguments); + javascriptChannelNames!.addAll(channelNames); + break; + case 'removeJavascriptChannels': + final List channelNames = List.from(call.arguments); + javascriptChannelNames! + .removeWhere((String channel) => channelNames.contains(channel)); + break; + case 'clearCache': + hasCache = false; + return Future.sync(() {}); + } + return Future.sync(() {}); + } + + void fakeJavascriptPostMessage(String jsChannel, String message) { + final StandardMethodCodec codec = const StandardMethodCodec(); + final Map arguments = { + 'channel': jsChannel, + 'message': message + }; + final ByteData data = codec + .encodeMethodCall(MethodCall('javascriptChannelMessage', arguments)); + _ambiguate(ServicesBinding.instance)! + .defaultBinaryMessenger + .handlePlatformMessage(channel.name, data, (ByteData? data) {}); + } + + // Fakes a main frame navigation that was initiated by the webview, e.g when + // the user clicks a link in the currently loaded page. + void fakeNavigate(String url) { + if (!hasNavigationDelegate!) { + print('no navigation delegate'); + _loadUrl(url); + return; + } + final StandardMethodCodec codec = const StandardMethodCodec(); + final Map arguments = { + 'url': url, + 'isForMainFrame': true + }; + final ByteData data = + codec.encodeMethodCall(MethodCall('navigationRequest', arguments)); + _ambiguate(ServicesBinding.instance)! + .defaultBinaryMessenger + .handlePlatformMessage(channel.name, data, (ByteData? data) { + final bool allow = codec.decodeEnvelope(data!); + if (allow) { + _loadUrl(url); + } + }); + } + + void fakeOnPageStartedCallback() { + final StandardMethodCodec codec = const StandardMethodCodec(); + + final ByteData data = codec.encodeMethodCall(MethodCall( + 'onPageStarted', + {'url': currentUrl}, + )); + + _ambiguate(ServicesBinding.instance)! + .defaultBinaryMessenger + .handlePlatformMessage( + channel.name, + data, + (ByteData? data) {}, + ); + } + + void fakeOnPageFinishedCallback() { + final StandardMethodCodec codec = const StandardMethodCodec(); + + final ByteData data = codec.encodeMethodCall(MethodCall( + 'onPageFinished', + {'url': currentUrl}, + )); + + _ambiguate(ServicesBinding.instance)! + .defaultBinaryMessenger + .handlePlatformMessage( + channel.name, + data, + (ByteData? data) {}, + ); + } + + void fakeOnProgressCallback(int progress) { + final StandardMethodCodec codec = const StandardMethodCodec(); + + final ByteData data = codec.encodeMethodCall(MethodCall( + 'onProgress', + {'progress': progress}, + )); + + _ambiguate(ServicesBinding.instance)! + .defaultBinaryMessenger + .handlePlatformMessage(channel.name, data, (ByteData? data) {}); + } + + void _loadUrl(String? url) { + history = history.sublist(0, currentPosition + 1); + history.add(url); + currentPosition++; + amountOfReloadsOnCurrentUrl = 0; + } +} + +class _FakePlatformViewsController { + FakePlatformWebView? lastCreatedView; + + Future fakePlatformViewsMethodHandler(MethodCall call) { + switch (call.method) { + case 'create': + final Map args = call.arguments; + final Map params = _decodeParams(args['params'])!; + lastCreatedView = FakePlatformWebView( + args['id'], + params, + ); + return Future.sync(() => 1); + default: + return Future.sync(() {}); + } + } + + void reset() { + lastCreatedView = null; + } +} + +Map? _decodeParams(Uint8List paramsMessage) { + final ByteBuffer buffer = paramsMessage.buffer; + final ByteData messageBytes = buffer.asByteData( + paramsMessage.offsetInBytes, + paramsMessage.lengthInBytes, + ); + return const StandardMessageCodec().decodeMessage(messageBytes); +} + +class _FakeCookieManager { + _FakeCookieManager() { + final MethodChannel channel = const MethodChannel( + 'plugins.flutter.io/cookie_manager', + StandardMethodCodec(), + ); + channel.setMockMethodCallHandler(onMethodCall); + } + + bool hasCookies = true; + + Future onMethodCall(MethodCall call) { + switch (call.method) { + case 'clearCookies': + bool hadCookies = false; + if (hasCookies) { + hadCookies = true; + hasCookies = false; + } + return Future.sync(() { + return hadCookies; + }); + } + return Future.sync(() => true); + } + + void reset() { + hasCookies = true; + } +} + +class MyWebViewPlatform implements WebViewPlatform { + MyWebViewPlatformController? lastPlatformBuilt; + + @override + Widget build({ + BuildContext? context, + CreationParams? creationParams, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + required JavascriptChannelRegistry javascriptChannelRegistry, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, + }) { + assert(onWebViewPlatformCreated != null); + lastPlatformBuilt = MyWebViewPlatformController( + creationParams, gestureRecognizers, webViewPlatformCallbacksHandler); + onWebViewPlatformCreated!(lastPlatformBuilt); + return Container(); + } + + @override + Future clearCookies() { + return Future.sync(() => true); + } +} + +class MyWebViewPlatformController extends WebViewPlatformController { + MyWebViewPlatformController(this.creationParams, this.gestureRecognizers, + WebViewPlatformCallbacksHandler platformHandler) + : super(platformHandler); + + CreationParams? creationParams; + Set>? gestureRecognizers; + + String? lastUrlLoaded; + Map? lastRequestHeaders; + + @override + Future loadUrl(String url, Map? headers) async { + equals(1, 1); + lastUrlLoaded = url; + lastRequestHeaders = headers; + } +} + +class MatchesWebSettings extends Matcher { + MatchesWebSettings(this._webSettings); + + final WebSettings? _webSettings; + + @override + Description describe(Description description) => + description.add('$_webSettings'); + + @override + bool matches( + covariant WebSettings webSettings, Map matchState) { + return _webSettings!.javascriptMode == webSettings.javascriptMode && + _webSettings!.hasNavigationDelegate == + webSettings.hasNavigationDelegate && + _webSettings!.debuggingEnabled == webSettings.debuggingEnabled && + _webSettings!.gestureNavigationEnabled == + webSettings.gestureNavigationEnabled && + _webSettings!.userAgent == webSettings.userAgent; + } +} + +class MatchesCreationParams extends Matcher { + MatchesCreationParams(this._creationParams); + + final CreationParams _creationParams; + + @override + Description describe(Description description) => + description.add('$_creationParams'); + + @override + bool matches(covariant CreationParams creationParams, + Map matchState) { + return _creationParams.initialUrl == creationParams.initialUrl && + MatchesWebSettings(_creationParams.webSettings) + .matches(creationParams.webSettings!, matchState) && + orderedEquals(_creationParams.javascriptChannelNames) + .matches(creationParams.javascriptChannelNames, matchState); + } +} + +/// This allows a value of type T or T? to be treated as a value of type T?. +/// +/// We use this so that APIs that have become non-nullable can still be used +/// with `!` and `?` on the stable branch. +// TODO(ianh): Remove this once we roll stable in late 2021. +T? _ambiguate(T? value) => value; diff --git a/packages/webview_flutter/webview_flutter_android/AUTHORS b/packages/webview_flutter/webview_flutter_android/AUTHORS new file mode 100644 index 000000000000..4461b602a13b --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/AUTHORS @@ -0,0 +1,68 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Maurits van Beusekom + diff --git a/packages/webview_flutter/webview_flutter_android/CHANGELOG.md b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md new file mode 100644 index 000000000000..d4827a71e47d --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/CHANGELOG.md @@ -0,0 +1,12 @@ +## 2.0.15 + +* Added Overrides in FlutterWebView.java + +## 2.0.14 + +* Update example App so navigation menu loads immediatly but only becomes available when `WebViewController` is available (same behavior as example App in webview_flutter package). + +## 2.0.13 + +* Extract Android implementation from `webview_flutter`. + diff --git a/packages/webview_flutter/webview_flutter_android/LICENSE b/packages/webview_flutter/webview_flutter_android/LICENSE new file mode 100644 index 000000000000..77130909e474 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/LICENSE @@ -0,0 +1,26 @@ +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + diff --git a/packages/webview_flutter/webview_flutter_android/README.md b/packages/webview_flutter/webview_flutter_android/README.md new file mode 100644 index 000000000000..38838562d13c --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/README.md @@ -0,0 +1,12 @@ +# webview\_flutter\_android + +The Android implementation of [`webview_flutter`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `webview_flutter` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/webview_flutter +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin + diff --git a/packages/webview_flutter/webview_flutter_android/android/build.gradle b/packages/webview_flutter/webview_flutter_android/android/build.gradle new file mode 100644 index 000000000000..4a164317c60f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/build.gradle @@ -0,0 +1,57 @@ +group 'io.flutter.plugins.webviewflutter' +version '1.0-SNAPSHOT' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.3.0' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 29 + + defaultConfig { + minSdkVersion 19 + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + lintOptions { + disable 'InvalidPackage' + disable 'GradleDependency' + } + + dependencies { + implementation 'androidx.annotation:annotation:1.0.0' + implementation 'androidx.webkit:webkit:1.0.0' + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-inline:3.11.1' + testImplementation 'androidx.test:core:1.3.0' + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/packages/webview_flutter/android/settings.gradle b/packages/webview_flutter/webview_flutter_android/android/settings.gradle similarity index 100% rename from packages/webview_flutter/android/settings.gradle rename to packages/webview_flutter/webview_flutter_android/android/settings.gradle diff --git a/packages/webview_flutter/android/src/main/AndroidManifest.xml b/packages/webview_flutter/webview_flutter_android/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/webview_flutter/android/src/main/AndroidManifest.xml rename to packages/webview_flutter/webview_flutter_android/android/src/main/AndroidManifest.xml diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java similarity index 97% rename from packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/DisplayListenerProxy.java rename to packages/webview_flutter/webview_flutter_android/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/webview_flutter_android/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/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java similarity index 96% rename from packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterCookieManager.java rename to packages/webview_flutter/webview_flutter_android/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/webview_flutter_android/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/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java new file mode 100644 index 000000000000..cfad4e315514 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterDownloadListener.java @@ -0,0 +1,33 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.webkit.DownloadListener; +import android.webkit.WebView; + +/** DownloadListener to notify the {@link FlutterWebViewClient} of download starts */ +public class FlutterDownloadListener implements DownloadListener { + private final FlutterWebViewClient webViewClient; + private WebView webView; + + public FlutterDownloadListener(FlutterWebViewClient webViewClient) { + this.webViewClient = webViewClient; + } + + /** Sets the {@link WebView} that the result of the navigation delegate will be send to. */ + public void setWebView(WebView webView) { + this.webView = webView; + } + + @Override + public void onDownloadStart( + String url, + String userAgent, + String contentDisposition, + String mimetype, + long contentLength) { + webViewClient.notifyDownload(webView, url); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java new file mode 100644 index 000000000000..ff573c771960 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebView.java @@ -0,0 +1,478 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.annotation.TargetApi; +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.os.Build; +import android.os.Handler; +import android.os.Message; +import android.view.View; +import android.webkit.DownloadListener; +import android.webkit.WebChromeClient; +import android.webkit.WebResourceRequest; +import android.webkit.WebStorage; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugin.platform.PlatformView; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +public class FlutterWebView implements PlatformView, MethodCallHandler { + + private static final String JS_CHANNEL_NAMES_FIELD = "javascriptChannelNames"; + private final WebView webView; + private final MethodChannel methodChannel; + private final FlutterWebViewClient flutterWebViewClient; + private final Handler platformThreadHandler; + + // Verifies that a url opened by `Window.open` has a secure url. + private class FlutterWebChromeClient extends WebChromeClient { + + @Override + public boolean onCreateWindow( + final WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) { + final WebViewClient webViewClient = + new WebViewClient() { + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + @Override + public boolean shouldOverrideUrlLoading( + @NonNull WebView view, @NonNull WebResourceRequest request) { + final String url = request.getUrl().toString(); + if (!flutterWebViewClient.shouldOverrideUrlLoading( + FlutterWebView.this.webView, request)) { + webView.loadUrl(url); + } + return true; + } + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + if (!flutterWebViewClient.shouldOverrideUrlLoading( + FlutterWebView.this.webView, url)) { + webView.loadUrl(url); + } + return true; + } + }; + + final WebView newWebView = new WebView(view.getContext()); + newWebView.setWebViewClient(webViewClient); + + final WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj; + transport.setWebView(newWebView); + resultMsg.sendToTarget(); + + return true; + } + + @Override + public void onProgressChanged(WebView view, int progress) { + flutterWebViewClient.onLoadingProgress(progress); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + @SuppressWarnings("unchecked") + FlutterWebView( + final Context context, + MethodChannel methodChannel, + Map params, + View containerView) { + + DisplayListenerProxy displayListenerProxy = new DisplayListenerProxy(); + DisplayManager displayManager = + (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE); + displayListenerProxy.onPreWebViewInitialization(displayManager); + + this.methodChannel = methodChannel; + this.methodChannel.setMethodCallHandler(this); + + flutterWebViewClient = new FlutterWebViewClient(methodChannel); + + FlutterDownloadListener flutterDownloadListener = + new FlutterDownloadListener(flutterWebViewClient); + webView = + createWebView( + new WebViewBuilder(context, containerView), + params, + new FlutterWebChromeClient(), + flutterDownloadListener); + flutterDownloadListener.setWebView(webView); + + displayListenerProxy.onPostWebViewInitialization(displayManager); + + platformThreadHandler = new Handler(context.getMainLooper()); + + Map settings = (Map) params.get("settings"); + if (settings != null) { + applySettings(settings); + } + + if (params.containsKey(JS_CHANNEL_NAMES_FIELD)) { + List names = (List) params.get(JS_CHANNEL_NAMES_FIELD); + if (names != null) { + registerJavaScriptChannelNames(names); + } + } + + Integer autoMediaPlaybackPolicy = (Integer) params.get("autoMediaPlaybackPolicy"); + if (autoMediaPlaybackPolicy != null) { + updateAutoMediaPlaybackPolicy(autoMediaPlaybackPolicy); + } + if (params.containsKey("userAgent")) { + String userAgent = (String) params.get("userAgent"); + updateUserAgent(userAgent); + } + if (params.containsKey("initialUrl")) { + String url = (String) params.get("initialUrl"); + webView.loadUrl(url); + } + } + + /** + * Creates a {@link android.webkit.WebView} and configures it according to the supplied + * parameters. + * + *

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

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

      Important: This method is visible for testing purposes only and should + * never be called from outside this class. + * + * @param webViewBuilder a {@link WebViewBuilder} which is responsible for building the {@link + * WebView}. + * @param params creation parameters received over the method channel. + * @param webChromeClient an implementation of WebChromeClient This value may be null. + * @return The new {@link android.webkit.WebView} object. + */ + @VisibleForTesting + static WebView createWebView( + WebViewBuilder webViewBuilder, + Map params, + WebChromeClient webChromeClient, + @Nullable DownloadListener downloadListener) { + boolean usesHybridComposition = Boolean.TRUE.equals(params.get("usesHybridComposition")); + webViewBuilder + .setUsesHybridComposition(usesHybridComposition) + .setDomStorageEnabled(true) // Always enable DOM storage API. + .setJavaScriptCanOpenWindowsAutomatically( + true) // Always allow automatically opening of windows. + .setSupportMultipleWindows(true) // Always support multiple windows. + .setWebChromeClient(webChromeClient) + .setDownloadListener( + downloadListener); // Always use {@link FlutterWebChromeClient} as web Chrome client. + + return webViewBuilder.build(); + } + + @Override + public View getView() { + return webView; + } + + @Override + public void onInputConnectionUnlocked() { + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).unlockInputConnection(); + } + } + + @Override + public void onInputConnectionLocked() { + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).lockInputConnection(); + } + } + + @Override + public void onFlutterViewAttached(View flutterView) { + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).setContainerView(flutterView); + } + } + + @Override + public void onFlutterViewDetached() { + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).setContainerView(null); + } + } + + @Override + public void onMethodCall(MethodCall methodCall, Result result) { + switch (methodCall.method) { + case "loadUrl": + loadUrl(methodCall, result); + break; + case "updateSettings": + updateSettings(methodCall, result); + break; + case "canGoBack": + canGoBack(result); + break; + case "canGoForward": + canGoForward(result); + break; + case "goBack": + goBack(result); + break; + case "goForward": + goForward(result); + break; + case "reload": + reload(result); + break; + case "currentUrl": + currentUrl(result); + break; + case "evaluateJavascript": + evaluateJavaScript(methodCall, result); + break; + case "addJavascriptChannels": + addJavaScriptChannels(methodCall, result); + break; + case "removeJavascriptChannels": + removeJavaScriptChannels(methodCall, result); + break; + case "clearCache": + clearCache(result); + break; + case "getTitle": + getTitle(result); + break; + case "scrollTo": + scrollTo(methodCall, result); + break; + case "scrollBy": + scrollBy(methodCall, result); + break; + case "getScrollX": + getScrollX(result); + break; + case "getScrollY": + getScrollY(result); + break; + default: + result.notImplemented(); + } + } + + @SuppressWarnings("unchecked") + private void loadUrl(MethodCall methodCall, Result result) { + Map request = (Map) methodCall.arguments; + String url = (String) request.get("url"); + Map headers = (Map) request.get("headers"); + if (headers == null) { + headers = Collections.emptyMap(); + } + webView.loadUrl(url, headers); + result.success(null); + } + + private void canGoBack(Result result) { + result.success(webView.canGoBack()); + } + + private void canGoForward(Result result) { + result.success(webView.canGoForward()); + } + + private void goBack(Result result) { + if (webView.canGoBack()) { + webView.goBack(); + } + result.success(null); + } + + private void goForward(Result result) { + if (webView.canGoForward()) { + webView.goForward(); + } + result.success(null); + } + + private void reload(Result result) { + webView.reload(); + result.success(null); + } + + private void currentUrl(Result result) { + result.success(webView.getUrl()); + } + + @SuppressWarnings("unchecked") + private void updateSettings(MethodCall methodCall, Result result) { + applySettings((Map) methodCall.arguments); + result.success(null); + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + private void evaluateJavaScript(MethodCall methodCall, final Result result) { + String jsString = (String) methodCall.arguments; + if (jsString == null) { + throw new UnsupportedOperationException("JavaScript string cannot be null"); + } + webView.evaluateJavascript( + jsString, + new android.webkit.ValueCallback() { + @Override + public void onReceiveValue(String value) { + result.success(value); + } + }); + } + + @SuppressWarnings("unchecked") + private void addJavaScriptChannels(MethodCall methodCall, Result result) { + List channelNames = (List) methodCall.arguments; + registerJavaScriptChannelNames(channelNames); + result.success(null); + } + + @SuppressWarnings("unchecked") + private void removeJavaScriptChannels(MethodCall methodCall, Result result) { + List channelNames = (List) methodCall.arguments; + for (String channelName : channelNames) { + webView.removeJavascriptInterface(channelName); + } + result.success(null); + } + + private void clearCache(Result result) { + webView.clearCache(true); + WebStorage.getInstance().deleteAllData(); + result.success(null); + } + + private void getTitle(Result result) { + result.success(webView.getTitle()); + } + + private void scrollTo(MethodCall methodCall, Result result) { + Map request = methodCall.arguments(); + int x = (int) request.get("x"); + int y = (int) request.get("y"); + + webView.scrollTo(x, y); + + result.success(null); + } + + private void scrollBy(MethodCall methodCall, Result result) { + Map request = methodCall.arguments(); + int x = (int) request.get("x"); + int y = (int) request.get("y"); + + webView.scrollBy(x, y); + result.success(null); + } + + private void getScrollX(Result result) { + result.success(webView.getScrollX()); + } + + private void getScrollY(Result result) { + result.success(webView.getScrollY()); + } + + private void applySettings(Map settings) { + for (String key : settings.keySet()) { + switch (key) { + case "jsMode": + Integer mode = (Integer) settings.get(key); + if (mode != null) { + updateJsMode(mode); + } + break; + case "hasNavigationDelegate": + final boolean hasNavigationDelegate = (boolean) settings.get(key); + + final WebViewClient webViewClient = + flutterWebViewClient.createWebViewClient(hasNavigationDelegate); + + webView.setWebViewClient(webViewClient); + break; + case "debuggingEnabled": + final boolean debuggingEnabled = (boolean) settings.get(key); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + webView.setWebContentsDebuggingEnabled(debuggingEnabled); + } + break; + case "hasProgressTracking": + flutterWebViewClient.hasProgressTracking = (boolean) settings.get(key); + break; + case "gestureNavigationEnabled": + break; + case "userAgent": + updateUserAgent((String) settings.get(key)); + break; + case "allowsInlineMediaPlayback": + // no-op inline media playback is always allowed on Android. + break; + default: + throw new IllegalArgumentException("Unknown WebView setting: " + key); + } + } + } + + private void updateJsMode(int mode) { + switch (mode) { + case 0: // disabled + webView.getSettings().setJavaScriptEnabled(false); + break; + case 1: // unrestricted + webView.getSettings().setJavaScriptEnabled(true); + break; + default: + throw new IllegalArgumentException("Trying to set unknown JavaScript mode: " + mode); + } + } + + private void updateAutoMediaPlaybackPolicy(int mode) { + // This is the index of the AutoMediaPlaybackPolicy enum, index 1 is always_allow, for all + // other values we require a user gesture. + boolean requireUserGesture = mode != 1; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + webView.getSettings().setMediaPlaybackRequiresUserGesture(requireUserGesture); + } + } + + private void registerJavaScriptChannelNames(List channelNames) { + for (String channelName : channelNames) { + webView.addJavascriptInterface( + new JavaScriptChannel(methodChannel, channelName, platformThreadHandler), channelName); + } + } + + private void updateUserAgent(String userAgent) { + webView.getSettings().setUserAgentString(userAgent); + } + + @Override + public void dispose() { + methodChannel.setMethodCallHandler(null); + if (webView instanceof InputAwareWebView) { + ((InputAwareWebView) webView).dispose(); + } + webView.destroy(); + } +} diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java similarity index 84% rename from packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java rename to packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java index f61a9d39b85f..260ef8e8b15d 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewClient.java @@ -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. @@ -14,6 +14,8 @@ import android.webkit.WebResourceRequest; import android.webkit.WebView; import android.webkit.WebViewClient; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; import androidx.webkit.WebResourceErrorCompat; import androidx.webkit.WebViewClientCompat; import io.flutter.plugin.common.MethodChannel; @@ -29,12 +31,13 @@ class FlutterWebViewClient { private static final String TAG = "FlutterWebViewClient"; private final MethodChannel methodChannel; private boolean hasNavigationDelegate; + boolean hasProgressTracking; FlutterWebViewClient(MethodChannel methodChannel) { this.methodChannel = methodChannel; } - private static String errorCodeToString(int errorCode) { + static String errorCodeToString(int errorCode) { switch (errorCode) { case WebViewClient.ERROR_AUTHENTICATION: return "authentication"; @@ -76,7 +79,7 @@ private static String errorCodeToString(int errorCode) { } @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { if (!hasNavigationDelegate) { return false; } @@ -96,7 +99,7 @@ private boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest reques return request.isForMainFrame(); } - private boolean shouldOverrideUrlLoading(WebView view, String url) { + boolean shouldOverrideUrlLoading(WebView view, String url) { if (!hasNavigationDelegate) { return false; } @@ -112,6 +115,22 @@ private boolean shouldOverrideUrlLoading(WebView view, String url) { return true; } + /** + * Notifies the Flutter code that a download should start when a navigation delegate is set. + * + * @param view the webView the result of the navigation delegate will be send to. + * @param url the download url + * @return A boolean whether or not the request is forwarded to the Flutter code. + */ + boolean notifyDownload(WebView view, String url) { + if (!hasNavigationDelegate) { + return false; + } + + notifyOnNavigationRequest(url, null, view, true); + return true; + } + private void onPageStarted(WebView view, String url) { Map args = new HashMap<>(); args.put("url", url); @@ -124,11 +143,21 @@ private void onPageFinished(WebView view, String url) { methodChannel.invokeMethod("onPageFinished", args); } - private void onWebResourceError(final int errorCode, final String description) { + void onLoadingProgress(int progress) { + if (hasProgressTracking) { + Map args = new HashMap<>(); + args.put("progress", progress); + methodChannel.invokeMethod("onProgress", args); + } + } + + private void onWebResourceError( + final int errorCode, final String description, final String failingUrl) { final Map args = new HashMap<>(); args.put("errorCode", errorCode); args.put("description", description); args.put("errorType", FlutterWebViewClient.errorCodeToString(errorCode)); + args.put("failingUrl", failingUrl); methodChannel.invokeMethod("onWebResourceError", args); } @@ -180,14 +209,16 @@ public void onPageFinished(WebView view, String url) { @Override public void onReceivedError( WebView view, WebResourceRequest request, WebResourceError error) { - FlutterWebViewClient.this.onWebResourceError( - error.getErrorCode(), error.getDescription().toString()); + if (request.isForMainFrame()) { + FlutterWebViewClient.this.onWebResourceError( + error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); + } } @Override public void onReceivedError( WebView view, int errorCode, String description, String failingUrl) { - FlutterWebViewClient.this.onWebResourceError(errorCode, description); + FlutterWebViewClient.this.onWebResourceError(errorCode, description, failingUrl); } @Override @@ -223,18 +254,23 @@ public void onPageFinished(WebView view, String url) { // This method is only called when the WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR feature is // enabled. The deprecated method is called when a device doesn't support this. + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @SuppressLint("RequiresFeature") @Override public void onReceivedError( - WebView view, WebResourceRequest request, WebResourceErrorCompat error) { - FlutterWebViewClient.this.onWebResourceError( - error.getErrorCode(), error.getDescription().toString()); + @NonNull WebView view, + @NonNull WebResourceRequest request, + @NonNull WebResourceErrorCompat error) { + if (request.isForMainFrame()) { + FlutterWebViewClient.this.onWebResourceError( + error.getErrorCode(), error.getDescription().toString(), request.getUrl().toString()); + } } @Override public void onReceivedError( WebView view, int errorCode, String description, String failingUrl) { - FlutterWebViewClient.this.onWebResourceError(errorCode, description); + FlutterWebViewClient.this.onWebResourceError(errorCode, description, failingUrl); } @Override diff --git a/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java new file mode 100644 index 000000000000..8fe58104a0fb --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/FlutterWebViewFactory.java @@ -0,0 +1,33 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import android.content.Context; +import android.view.View; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.StandardMessageCodec; +import io.flutter.plugin.platform.PlatformView; +import io.flutter.plugin.platform.PlatformViewFactory; +import java.util.Map; + +public final class FlutterWebViewFactory extends PlatformViewFactory { + private final BinaryMessenger messenger; + private final View containerView; + + FlutterWebViewFactory(BinaryMessenger messenger, View containerView) { + super(StandardMessageCodec.INSTANCE); + this.messenger = messenger; + this.containerView = containerView; + } + + @SuppressWarnings("unchecked") + @Override + public PlatformView create(Context context, int id, Object args) { + Map params = (Map) args; + MethodChannel methodChannel = new MethodChannel(messenger, "plugins.flutter.io/webview_" + id); + return new FlutterWebView(context, methodChannel, params, containerView); + } +} diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java similarity index 96% rename from packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/InputAwareWebView.java rename to packages/webview_flutter/webview_flutter_android/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/webview_flutter_android/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/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java similarity index 97% rename from packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/JavaScriptChannel.java rename to packages/webview_flutter/webview_flutter_android/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/webview_flutter_android/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/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java similarity index 98% rename from packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/ThreadedInputConnectionProxyAdapterView.java rename to packages/webview_flutter/webview_flutter_android/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/webview_flutter_android/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/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java new file mode 100644 index 000000000000..d3cd1d57cdae --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewBuilder.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import android.content.Context; +import android.view.View; +import android.webkit.DownloadListener; +import android.webkit.WebChromeClient; +import android.webkit.WebSettings; +import android.webkit.WebView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** Builder used to create {@link android.webkit.WebView} objects. */ +public class WebViewBuilder { + + /** Factory used to create a new {@link android.webkit.WebView} instance. */ + static class WebViewFactory { + + /** + * Creates a new {@link android.webkit.WebView} instance. + * + * @param context an Activity Context to access application assets. This value cannot be null. + * @param usesHybridComposition If {@code false} a {@link InputAwareWebView} instance is + * returned. + * @param containerView must be supplied when the {@code useHybridComposition} parameter is set + * to {@code false}. Used to create an InputConnection on the WebView's dedicated input, or + * IME, thread (see also {@link InputAwareWebView}) + * @return A new instance of the {@link android.webkit.WebView} object. + */ + static WebView create(Context context, boolean usesHybridComposition, View containerView) { + return usesHybridComposition + ? new WebView(context) + : new InputAwareWebView(context, containerView); + } + } + + private final Context context; + private final View containerView; + + private boolean enableDomStorage; + private boolean javaScriptCanOpenWindowsAutomatically; + private boolean supportMultipleWindows; + private boolean usesHybridComposition; + private WebChromeClient webChromeClient; + private DownloadListener downloadListener; + + /** + * Constructs a new {@link WebViewBuilder} object with a custom implementation of the {@link + * WebViewFactory} object. + * + * @param context an Activity Context to access application assets. This value cannot be null. + * @param containerView must be supplied when the {@code useHybridComposition} parameter is set to + * {@code false}. Used to create an InputConnection on the WebView's dedicated input, or IME, + * thread (see also {@link InputAwareWebView}) + */ + WebViewBuilder(@NonNull final Context context, View containerView) { + this.context = context; + this.containerView = containerView; + } + + /** + * Sets whether the DOM storage API is enabled. The default value is {@code false}. + * + * @param flag {@code true} is {@link android.webkit.WebView} should use the DOM storage API. + * @return This builder. This value cannot be {@code null}. + */ + public WebViewBuilder setDomStorageEnabled(boolean flag) { + this.enableDomStorage = flag; + return this; + } + + /** + * Sets whether JavaScript is allowed to open windows automatically. This applies to the + * JavaScript function {@code window.open()}. The default value is {@code false}. + * + * @param flag {@code true} if JavaScript is allowed to open windows automatically. + * @return This builder. This value cannot be {@code null}. + */ + public WebViewBuilder setJavaScriptCanOpenWindowsAutomatically(boolean flag) { + this.javaScriptCanOpenWindowsAutomatically = flag; + return this; + } + + /** + * Sets whether the {@link WebView} supports multiple windows. If set to {@code true}, {@link + * WebChromeClient#onCreateWindow} must be implemented by the host application. The default is + * {@code false}. + * + * @param flag {@code true} if multiple windows are supported. + * @return This builder. This value cannot be {@code null}. + */ + public WebViewBuilder setSupportMultipleWindows(boolean flag) { + this.supportMultipleWindows = flag; + return this; + } + + /** + * Sets whether the hybrid composition should be used. + * + *

      If set to {@code true} a standard {@link WebView} is created. If set to {@code false} the + * {@link WebViewBuilder} will create a {@link InputAwareWebView} to workaround issues using the + * {@link WebView} on Android versions below N. + * + * @param flag {@code true} if uses hybrid composition. The default is {@code false}. + * @return This builder. This value cannot be {@code null} + */ + public WebViewBuilder setUsesHybridComposition(boolean flag) { + this.usesHybridComposition = flag; + return this; + } + + /** + * Sets the chrome handler. This is an implementation of WebChromeClient for use in handling + * JavaScript dialogs, favicons, titles, and the progress. This will replace the current handler. + * + * @param webChromeClient an implementation of WebChromeClient This value may be null. + * @return This builder. This value cannot be {@code null}. + */ + public WebViewBuilder setWebChromeClient(@Nullable WebChromeClient webChromeClient) { + this.webChromeClient = webChromeClient; + return this; + } + + /** + * Registers the interface to be used when content can not be handled by the rendering engine, and + * should be downloaded instead. This will replace the current handler. + * + * @param downloadListener an implementation of DownloadListener This value may be null. + * @return This builder. This value cannot be {@code null}. + */ + public WebViewBuilder setDownloadListener(@Nullable DownloadListener downloadListener) { + this.downloadListener = downloadListener; + return this; + } + + /** + * Build the {@link android.webkit.WebView} using the current settings. + * + * @return The {@link android.webkit.WebView} using the current settings. + */ + public WebView build() { + WebView webView = WebViewFactory.create(context, usesHybridComposition, containerView); + + WebSettings webSettings = webView.getSettings(); + webSettings.setDomStorageEnabled(enableDomStorage); + webSettings.setJavaScriptCanOpenWindowsAutomatically(javaScriptCanOpenWindowsAutomatically); + webSettings.setSupportMultipleWindows(supportMultipleWindows); + webView.setWebChromeClient(webChromeClient); + webView.setDownloadListener(downloadListener); + return webView; + } +} diff --git a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java similarity index 84% rename from packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java rename to packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java index a43128c680d5..268d35a1e04c 100644 --- a/packages/webview_flutter/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java +++ b/packages/webview_flutter/webview_flutter_android/android/src/main/java/io/flutter/plugins/webviewflutter/WebViewFlutterPlugin.java @@ -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. @@ -6,7 +6,6 @@ import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.PluginRegistry.Registrar; /** * Java platform implementation of the webview_flutter plugin. @@ -41,12 +40,13 @@ public WebViewFlutterPlugin() {} *

      Calling this automatically initializes the plugin. However plugins initialized this way * won't react to changes in activity or context, unlike {@link CameraPlugin}. */ - public static void registerWith(Registrar registrar) { + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { registrar .platformViewRegistry() .registerViewFactory( "plugins.flutter.io/webview", - new WebViewFactory(registrar.messenger(), registrar.view())); + new FlutterWebViewFactory(registrar.messenger(), registrar.view())); new FlutterCookieManager(registrar.messenger()); } @@ -54,11 +54,10 @@ public static void registerWith(Registrar registrar) { public void onAttachedToEngine(FlutterPluginBinding binding) { BinaryMessenger messenger = binding.getBinaryMessenger(); binding - .getFlutterEngine() - .getPlatformViewsController() - .getRegistry() + .getPlatformViewRegistry() .registerViewFactory( - "plugins.flutter.io/webview", new WebViewFactory(messenger, /*containerView=*/ null)); + "plugins.flutter.io/webview", + new FlutterWebViewFactory(messenger, /*containerView=*/ null)); flutterCookieManager = new FlutterCookieManager(messenger); } diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.java new file mode 100644 index 000000000000..2c918584ba83 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterDownloadListenerTest.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.webviewflutter; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import android.webkit.WebView; +import org.junit.Before; +import org.junit.Test; + +public class FlutterDownloadListenerTest { + private FlutterWebViewClient webViewClient; + private WebView webView; + + @Before + public void before() { + webViewClient = mock(FlutterWebViewClient.class); + webView = mock(WebView.class); + } + + @Test + public void onDownloadStart_should_notify_webViewClient() { + String url = "testurl.com"; + FlutterDownloadListener downloadListener = new FlutterDownloadListener(webViewClient); + downloadListener.onDownloadStart(url, "test", "inline", "data/text", 0); + verify(webViewClient).notifyDownload(nullable(WebView.class), eq(url)); + } + + @Test + public void onDownloadStart_should_pass_webView() { + FlutterDownloadListener downloadListener = new FlutterDownloadListener(webViewClient); + downloadListener.setWebView(webView); + downloadListener.onDownloadStart("testurl.com", "test", "inline", "data/text", 0); + verify(webViewClient).notifyDownload(eq(webView), anyString()); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.java new file mode 100644 index 000000000000..86346ac08f16 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewClientTest.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.webviewflutter; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +import android.webkit.WebView; +import io.flutter.plugin.common.MethodChannel; +import java.util.HashMap; +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; + +public class FlutterWebViewClientTest { + + MethodChannel mockMethodChannel; + WebView mockWebView; + + @Before + public void before() { + mockMethodChannel = mock(MethodChannel.class); + mockWebView = mock(WebView.class); + } + + @Test + public void notify_download_should_notifyOnNavigationRequest_when_navigationDelegate_is_set() { + final String url = "testurl.com"; + + FlutterWebViewClient client = new FlutterWebViewClient(mockMethodChannel); + client.createWebViewClient(true); + + client.notifyDownload(mockWebView, url); + ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Object.class); + verify(mockMethodChannel) + .invokeMethod( + eq("navigationRequest"), argumentCaptor.capture(), any(MethodChannel.Result.class)); + HashMap map = (HashMap) argumentCaptor.getValue(); + assertEquals(map.get("url"), url); + assertEquals(map.get("isForMainFrame"), true); + } + + @Test + public void + notify_download_should_not_notifyOnNavigationRequest_when_navigationDelegate_is_not_set() { + final String url = "testurl.com"; + + FlutterWebViewClient client = new FlutterWebViewClient(mockMethodChannel); + client.createWebViewClient(false); + + client.notifyDownload(mockWebView, url); + verifyNoInteractions(mockMethodChannel); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java new file mode 100644 index 000000000000..56d9db1ee493 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/FlutterWebViewTest.java @@ -0,0 +1,66 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutter; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.webkit.DownloadListener; +import android.webkit.WebChromeClient; +import android.webkit.WebView; +import java.util.HashMap; +import java.util.Map; +import org.junit.Before; +import org.junit.Test; + +public class FlutterWebViewTest { + private WebChromeClient mockWebChromeClient; + private DownloadListener mockDownloadListener; + private WebViewBuilder mockWebViewBuilder; + private WebView mockWebView; + + @Before + public void before() { + mockWebChromeClient = mock(WebChromeClient.class); + mockWebViewBuilder = mock(WebViewBuilder.class); + mockWebView = mock(WebView.class); + mockDownloadListener = mock(DownloadListener.class); + + when(mockWebViewBuilder.setDomStorageEnabled(anyBoolean())).thenReturn(mockWebViewBuilder); + when(mockWebViewBuilder.setJavaScriptCanOpenWindowsAutomatically(anyBoolean())) + .thenReturn(mockWebViewBuilder); + when(mockWebViewBuilder.setSupportMultipleWindows(anyBoolean())).thenReturn(mockWebViewBuilder); + when(mockWebViewBuilder.setUsesHybridComposition(anyBoolean())).thenReturn(mockWebViewBuilder); + when(mockWebViewBuilder.setWebChromeClient(any(WebChromeClient.class))) + .thenReturn(mockWebViewBuilder); + when(mockWebViewBuilder.setDownloadListener(any(DownloadListener.class))) + .thenReturn(mockWebViewBuilder); + + when(mockWebViewBuilder.build()).thenReturn(mockWebView); + } + + @Test + public void createWebView_should_create_webview_with_default_configuration() { + FlutterWebView.createWebView( + mockWebViewBuilder, createParameterMap(false), mockWebChromeClient, mockDownloadListener); + + verify(mockWebViewBuilder, times(1)).setDomStorageEnabled(true); + verify(mockWebViewBuilder, times(1)).setJavaScriptCanOpenWindowsAutomatically(true); + verify(mockWebViewBuilder, times(1)).setSupportMultipleWindows(true); + verify(mockWebViewBuilder, times(1)).setUsesHybridComposition(false); + verify(mockWebViewBuilder, times(1)).setWebChromeClient(mockWebChromeClient); + } + + private Map createParameterMap(boolean usesHybridComposition) { + Map params = new HashMap<>(); + params.put("usesHybridComposition", usesHybridComposition); + + return params; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java new file mode 100644 index 000000000000..423cb210c392 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewBuilderTest.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.*; + +import android.content.Context; +import android.view.View; +import android.webkit.DownloadListener; +import android.webkit.WebChromeClient; +import android.webkit.WebSettings; +import android.webkit.WebView; +import io.flutter.plugins.webviewflutter.WebViewBuilder.WebViewFactory; +import java.io.IOException; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.mockito.MockedStatic; +import org.mockito.MockedStatic.Verification; + +public class WebViewBuilderTest { + private Context mockContext; + private View mockContainerView; + private WebView mockWebView; + private MockedStatic mockedStaticWebViewFactory; + + @Before + public void before() { + mockContext = mock(Context.class); + mockContainerView = mock(View.class); + mockWebView = mock(WebView.class); + mockedStaticWebViewFactory = mockStatic(WebViewFactory.class); + + mockedStaticWebViewFactory + .when( + new Verification() { + @Override + public void apply() { + WebViewFactory.create(mockContext, false, mockContainerView); + } + }) + .thenReturn(mockWebView); + } + + @After + public void after() { + mockedStaticWebViewFactory.close(); + } + + @Test + public void ctor_test() { + WebViewBuilder builder = new WebViewBuilder(mockContext, mockContainerView); + + assertNotNull(builder); + } + + @Test + public void build_should_set_values() throws IOException { + WebSettings mockWebSettings = mock(WebSettings.class); + WebChromeClient mockWebChromeClient = mock(WebChromeClient.class); + DownloadListener mockDownloadListener = mock(DownloadListener.class); + + when(mockWebView.getSettings()).thenReturn(mockWebSettings); + + WebViewBuilder builder = + new WebViewBuilder(mockContext, mockContainerView) + .setDomStorageEnabled(true) + .setJavaScriptCanOpenWindowsAutomatically(true) + .setSupportMultipleWindows(true) + .setWebChromeClient(mockWebChromeClient) + .setDownloadListener(mockDownloadListener); + + WebView webView = builder.build(); + + assertNotNull(webView); + verify(mockWebSettings).setDomStorageEnabled(true); + verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(true); + verify(mockWebSettings).setSupportMultipleWindows(true); + verify(mockWebView).setWebChromeClient(mockWebChromeClient); + verify(mockWebView).setDownloadListener(mockDownloadListener); + } + + @Test + public void build_should_use_default_values() throws IOException { + WebSettings mockWebSettings = mock(WebSettings.class); + WebChromeClient mockWebChromeClient = mock(WebChromeClient.class); + + when(mockWebView.getSettings()).thenReturn(mockWebSettings); + + WebViewBuilder builder = new WebViewBuilder(mockContext, mockContainerView); + + WebView webView = builder.build(); + + assertNotNull(webView); + verify(mockWebSettings).setDomStorageEnabled(false); + verify(mockWebSettings).setJavaScriptCanOpenWindowsAutomatically(false); + verify(mockWebSettings).setSupportMultipleWindows(false); + verify(mockWebView).setWebChromeClient(null); + verify(mockWebView).setDownloadListener(null); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java new file mode 100644 index 000000000000..131a5a3eb53a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/android/src/test/java/io/flutter/plugins/webviewflutter/WebViewTest.java @@ -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. + +package io.flutter.plugins.webviewflutter; + +import static org.junit.Assert.assertEquals; + +import android.webkit.WebViewClient; +import org.junit.Test; + +public class WebViewTest { + @Test + public void errorCodes() { + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_AUTHENTICATION), + "authentication"); + assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_BAD_URL), "badUrl"); + assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_CONNECT), "connect"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FAILED_SSL_HANDSHAKE), + "failedSslHandshake"); + assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FILE), "file"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_FILE_NOT_FOUND), "fileNotFound"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_HOST_LOOKUP), "hostLookup"); + assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_IO), "io"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_PROXY_AUTHENTICATION), + "proxyAuthentication"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_REDIRECT_LOOP), "redirectLoop"); + assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_TIMEOUT), "timeout"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_TOO_MANY_REQUESTS), + "tooManyRequests"); + assertEquals(FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNKNOWN), "unknown"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSAFE_RESOURCE), + "unsafeResource"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSUPPORTED_AUTH_SCHEME), + "unsupportedAuthScheme"); + assertEquals( + FlutterWebViewClient.errorCodeToString(WebViewClient.ERROR_UNSUPPORTED_SCHEME), + "unsupportedScheme"); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/example/.metadata b/packages/webview_flutter/webview_flutter_android/example/.metadata new file mode 100644 index 000000000000..da83b1ada1bd --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/.metadata @@ -0,0 +1,8 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 1e5cb2d87f8542f9fbbd0f22d528823274be0acb + channel: master diff --git a/packages/webview_flutter/webview_flutter_android/example/README.md b/packages/webview_flutter/webview_flutter_android/example/README.md new file mode 100644 index 000000000000..850ee74397a9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/README.md @@ -0,0 +1,8 @@ +# webview_flutter_example + +Demonstrates how to use the webview_flutter plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.dev/). diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/build.gradle b/packages/webview_flutter/webview_flutter_android/example/android/app/build.gradle new file mode 100644 index 000000000000..1dcd363c9a44 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/build.gradle @@ -0,0 +1,62 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 29 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "io.flutter.plugins.webviewflutterandroidexample" + minSdkVersion 19 + 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' + api 'androidx.test:core:1.2.0' +} diff --git a/packages/url_launcher/url_launcher_macos/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/webview_flutter/webview_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/url_launcher/url_launcher_macos/example/android/app/gradle/wrapper/gradle-wrapper.properties rename to packages/webview_flutter/webview_flutter_android/example/android/app/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java new file mode 100644 index 000000000000..0f4298dca155 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java @@ -0,0 +1,14 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DartIntegrationTest {} diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java new file mode 100644 index 000000000000..a32aaebb0ecd --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/MainActivityTest.java @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutterexample; + +import androidx.test.rule.ActivityTestRule; +import dev.flutter.plugins.integration_test.FlutterTestRunner; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.plugins.DartIntegrationTest; +import org.junit.Rule; +import org.junit.runner.RunWith; + +@DartIntegrationTest +@RunWith(FlutterTestRunner.class) +public class MainActivityTest { + @Rule + public ActivityTestRule rule = new ActivityTestRule<>(FlutterActivity.class); +} diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/WebViewTest.java b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/WebViewTest.java new file mode 100644 index 000000000000..0b3eeef9b6b7 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/androidTest/java/io/flutter/plugins/webviewflutterexample/WebViewTest.java @@ -0,0 +1,23 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.webviewflutterexample; + +import static org.junit.Assert.assertTrue; + +import androidx.test.core.app.ActivityScenario; +import io.flutter.plugins.webviewflutter.WebViewFlutterPlugin; +import org.junit.Test; + +public class WebViewTest { + @Test + public void webViewPluginIsAdded() { + final ActivityScenario scenario = + ActivityScenario.launch(WebViewTestActivity.class); + scenario.onActivity( + activity -> { + assertTrue(activity.engine.getPlugins().has(WebViewFlutterPlugin.class)); + }); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml b/packages/webview_flutter/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..28792201bc36 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/AndroidManifest.xml b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..b8c8d38d45a5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/WebViewTestActivity.java b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/WebViewTestActivity.java new file mode 100644 index 000000000000..cb53a7a0dbf5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/java/io/flutter/plugins/webviewflutterexample/WebViewTestActivity.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.webviewflutterexample; + +import androidx.annotation.NonNull; +import io.flutter.embedding.android.FlutterActivity; +import io.flutter.embedding.engine.FlutterEngine; + +// Extends FlutterActivity to make the FlutterEngine accessible for testing. +public class WebViewTestActivity extends FlutterActivity { + public FlutterEngine engine; + + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + super.configureFlutterEngine(flutterEngine); + engine = flutterEngine; + } +} diff --git a/packages/webview_flutter/example/android/app/src/main/res/drawable/launch_background.xml b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from packages/webview_flutter/example/android/app/src/main/res/drawable/launch_background.xml rename to packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/drawable/launch_background.xml diff --git a/packages/ios_platform_images/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from packages/ios_platform_images/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/ios_platform_images/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from packages/ios_platform_images/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/ios_platform_images/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from packages/ios_platform_images/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/ios_platform_images/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from packages/ios_platform_images/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/ios_platform_images/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from packages/ios_platform_images/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/packages/ios_platform_images/example/android/app/src/main/res/values/styles.xml b/packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/values/styles.xml similarity index 100% rename from packages/ios_platform_images/example/android/app/src/main/res/values/styles.xml rename to packages/webview_flutter/webview_flutter_android/example/android/app/src/main/res/values/styles.xml diff --git a/packages/webview_flutter/webview_flutter_android/example/android/build.gradle b/packages/webview_flutter/webview_flutter_android/example/android/build.gradle new file mode 100644 index 000000000000..e101ac08df55 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_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/url_launcher/url_launcher_macos/example/android/gradle.properties b/packages/webview_flutter/webview_flutter_android/example/android/gradle.properties similarity index 100% rename from packages/url_launcher/url_launcher_macos/example/android/gradle.properties rename to packages/webview_flutter/webview_flutter_android/example/android/gradle.properties diff --git a/packages/in_app_purchase/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/webview_flutter/webview_flutter_android/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/webview_flutter/webview_flutter_android/example/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/webview_flutter/example/android/settings.gradle b/packages/webview_flutter/webview_flutter_android/example/android/settings.gradle similarity index 100% rename from packages/webview_flutter/example/android/settings.gradle rename to packages/webview_flutter/webview_flutter_android/example/android/settings.gradle diff --git a/packages/webview_flutter/webview_flutter_android/example/assets/sample_audio.ogg b/packages/webview_flutter/webview_flutter_android/example/assets/sample_audio.ogg new file mode 100644 index 000000000000..27e17104277b Binary files /dev/null and b/packages/webview_flutter/webview_flutter_android/example/assets/sample_audio.ogg differ diff --git a/packages/webview_flutter/webview_flutter_android/example/assets/sample_video.mp4 b/packages/webview_flutter/webview_flutter_android/example/assets/sample_video.mp4 new file mode 100644 index 000000000000..a203d0cdf13e Binary files /dev/null and b/packages/webview_flutter/webview_flutter_android/example/assets/sample_video.mp4 differ diff --git a/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart new file mode 100644 index 000000000000..c57d2bd55580 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/integration_test/webview_flutter_test.dart @@ -0,0 +1,1418 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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:typed_data'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:webview_flutter_android/webview_android.dart'; +import 'package:webview_flutter_android/webview_surface_android.dart'; +import 'package:webview_flutter_android_example/navigation_decision.dart'; +import 'package:webview_flutter_android_example/navigation_request.dart'; +import 'package:webview_flutter_android_example/web_view.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // URLs to navigate to in tests. These need to be URLs that we are confident will + // always be accessible, and won't do redirection. (E.g., just + // 'https://www.google.com/' will sometimes redirect traffic that looks + // like it's coming from a bot, which is true of these tests). + const String primaryUrl = 'https://flutter.dev/'; + const String secondaryUrl = 'https://www.google.com/robots.txt'; + + const bool _skipDueToIssue86757 = true; + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('initialUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + MaterialApp( + home: Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); + }, skip: _skipDueToIssue86757); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('loadUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await controller.loadUrl(secondaryUrl); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); + }, skip: _skipDueToIssue86757); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('loadUrl with headers', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageStarts = StreamController(); + final StreamController pageLoads = StreamController(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarts.add(url); + }, + onPageFinished: (String url) { + pageLoads.add(url); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final Map headers = { + 'test_header': 'flutter_test_header' + }; + await controller.loadUrl('https://flutter-header-echo.herokuapp.com/', + headers: headers); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://flutter-header-echo.herokuapp.com/'); + + await pageStarts.stream.firstWhere((String url) => url == currentUrl); + await pageLoads.stream.firstWhere((String url) => url == currentUrl); + + final String content = await controller + .evaluateJavascript('document.documentElement.innerText'); + expect(content.contains('flutter_test_header'), isTrue); + }, skip: _skipDueToIssue86757); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('JavaScriptChannel', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final List messagesReceived = []; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + // This is the data URL for: '' + initialUrl: + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'Echo', + onMessageReceived: (JavascriptMessage message) { + messagesReceived.add(message.message); + }, + ), + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + expect(messagesReceived, isEmpty); + // Append a return value "1" in the end will prevent an iOS platform exception. + // See: https://github.com/flutter/flutter/issues/66318#issuecomment-701105380 + // TODO(cyanglaz): remove the workaround "1" in the end when the below issue is fixed. + // https://github.com/flutter/flutter/issues/66318 + await controller.evaluateJavascript('Echo.postMessage("hello");1;'); + expect(messagesReceived, equals(['hello'])); + }, skip: _skipDueToIssue86757); + + testWidgets('resize webview', (WidgetTester tester) async { + final String resizeTest = ''' + + Resize test + + + + + + '''; + final String resizeTestBase64 = + base64Encode(const Utf8Encoder().convert(resizeTest)); + final Completer resizeCompleter = Completer(); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + final GlobalKey key = GlobalKey(); + + final WebView webView = WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$resizeTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptChannels: { + JavascriptChannel( + name: 'Resize', + onMessageReceived: (JavascriptMessage message) { + resizeCompleter.complete(true); + }, + ), + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + javascriptMode: JavascriptMode.unrestricted, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: 200, + height: 200, + child: webView, + ), + ], + ), + ), + ); + + await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + expect(resizeCompleter.isCompleted, false); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: 400, + height: 400, + child: webView, + ), + ], + ), + ), + ); + + await resizeCompleter.future; + }); + + testWidgets('set custom userAgent', (WidgetTester tester) async { + final Completer controllerCompleter1 = + Completer(); + final GlobalKey _globalKey = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent1', + onWebViewCreated: (WebViewController controller) { + controllerCompleter1.complete(controller); + }, + ), + ), + ); + final WebViewController controller1 = await controllerCompleter1.future; + final String customUserAgent1 = await _getUserAgent(controller1); + expect(customUserAgent1, 'Custom_User_Agent1'); + // rebuild the WebView with a different user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent2', + ), + ), + ); + + final String customUserAgent2 = await _getUserAgent(controller1); + expect(customUserAgent2, 'Custom_User_Agent2'); + }); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('use default platform userAgent after webView is rebuilt', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final GlobalKey _globalKey = GlobalKey(); + // Build the webView with no user agent to get the default platform user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: primaryUrl, + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String defaultPlatformUserAgent = await _getUserAgent(controller); + // rebuild the WebView with a custom user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent', + ), + ), + ); + final String customUserAgent = await _getUserAgent(controller); + expect(customUserAgent, 'Custom_User_Agent'); + // rebuilds the WebView with no user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ); + + final String customUserAgent2 = await _getUserAgent(controller); + expect(customUserAgent2, defaultPlatformUserAgent); + }, skip: _skipDueToIssue86757); + + group('Video playback policy', () { + late String videoTestBase64; + setUpAll(() async { + final ByteData videoData = + await rootBundle.load('assets/sample_video.mp4'); + final String base64VideoData = + base64Encode(Uint8List.view(videoData.buffer)); + final String videoTest = ''' + + Video auto play + + + + + + + '''; + videoTestBase64 = base64Encode(const Utf8Encoder().convert(videoTest)); + }); + + testWidgets('Auto media playback', (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + controllerCompleter = Completer(); + pageLoaded = Completer(); + + // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + controller = await controllerCompleter.future; + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(true)); + }); + + testWidgets('Changes to initialMediaPlaybackPolicy are ignored', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + await controller.reload(); + + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + }); + + testWidgets('Video plays inline when allowsInlineMediaPlayback is true', + (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + Completer videoPlaying = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'VideoTestTime', + onMessageReceived: (JavascriptMessage message) { + final double currentTime = double.parse(message.message); + // Let it play for at least 1 second to make sure the related video's properties are set. + if (currentTime > 1) { + videoPlaying.complete(null); + } + }, + ), + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + allowsInlineMediaPlayback: true, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + // Pump once to trigger the video play. + await tester.pump(); + + // Makes sure we get the correct event that indicates the video is actually playing. + await videoPlaying.future; + + String fullScreen = + await controller.evaluateJavascript('isFullScreen();'); + expect(fullScreen, _webviewBool(false)); + }); + }); + + group('Audio playback policy', () { + late String audioTestBase64; + setUpAll(() async { + final ByteData audioData = + await rootBundle.load('assets/sample_audio.ogg'); + final String base64AudioData = + base64Encode(Uint8List.view(audioData.buffer)); + final String audioTest = ''' + + Audio auto play + + + + + + + '''; + audioTestBase64 = base64Encode(const Utf8Encoder().convert(audioTest)); + }); + + testWidgets('Auto media playback', (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageStarted = Completer(); + Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + controllerCompleter = Completer(); + pageStarted = Completer(); + pageLoaded = Completer(); + + // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(true)); + }); + + testWidgets('Changes to initialMediaPlaybackPolocy are ignored', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageStarted = Completer(); + Completer pageLoaded = Completer(); + + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + pageStarted = Completer(); + pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + await controller.reload(); + + await pageStarted.future; + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + }); + }); + + testWidgets('getTitle', (WidgetTester tester) async { + final String getTitleTest = ''' + + Some title + + + + + '''; + final String getTitleTestBase64 = + base64Encode(const Utf8Encoder().convert(getTitleTest)); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + final String? title = await controller.getTitle(); + expect(title, 'Some title'); + }); + + group('Programmatic Scroll', () { + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { + final String scrollTestPage = ''' + + + + + + +
      + + + '''; + + final String scrollTestPageBase64 = + base64Encode(const Utf8Encoder().convert(scrollTestPage)); + + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + await tester.pumpAndSettle(Duration(seconds: 3)); + + int scrollPosX = await controller.getScrollX(); + int scrollPosY = await controller.getScrollY(); + + // Check scrollTo() + const int X_SCROLL = 123; + const int Y_SCROLL = 321; + // Get the initial position; this ensures that scrollTo is actually + // changing something, but also gives the native view's scroll position + // time to settle. + expect(scrollPosX, isNot(X_SCROLL)); + expect(scrollPosX, isNot(Y_SCROLL)); + + await controller.scrollTo(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(scrollPosX, X_SCROLL); + expect(scrollPosY, Y_SCROLL); + + // Check scrollBy() (on top of scrollTo()) + await controller.scrollBy(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(scrollPosX, X_SCROLL * 2); + expect(scrollPosY, Y_SCROLL * 2); + }, skip: _skipDueToIssue86757); + }); + + group('SurfaceAndroidWebView', () { + setUpAll(() { + WebView.platform = SurfaceAndroidWebView(); + }); + + tearDownAll(() { + WebView.platform = AndroidWebView(); + }); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { + final String scrollTestPage = ''' + + + + + + +
      + + + '''; + + final String scrollTestPageBase64 = + base64Encode(const Utf8Encoder().convert(scrollTestPage)); + + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + await tester.pumpAndSettle(Duration(seconds: 3)); + + // Check scrollTo() + const int X_SCROLL = 123; + const int Y_SCROLL = 321; + + await controller.scrollTo(X_SCROLL, Y_SCROLL); + int scrollPosX = await controller.getScrollX(); + int scrollPosY = await controller.getScrollY(); + expect(X_SCROLL, scrollPosX); + expect(Y_SCROLL, scrollPosY); + + // Check scrollBy() (on top of scrollTo()) + await controller.scrollBy(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(X_SCROLL * 2, scrollPosX); + expect(Y_SCROLL * 2, scrollPosY); + }, skip: _skipDueToIssue86757); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('inputs are scrolled into view when focused', + (WidgetTester tester) async { + final String scrollTestPage = ''' + + + + + + +
      + + + + '''; + + final String scrollTestPageBase64 = + base64Encode(const Utf8Encoder().convert(scrollTestPage)); + + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.runAsync(() async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: SizedBox( + width: 200, + height: 200, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ), + ); + await Future.delayed(Duration(milliseconds: 20)); + await tester.pump(); + }); + + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + final String viewportRectJSON = await _evaluateJavascript( + controller, 'JSON.stringify(viewport.getBoundingClientRect())'); + final Map viewportRectRelativeToViewport = + jsonDecode(viewportRectJSON); + + // Check that the input is originally outside of the viewport. + + final String initialInputClientRectJSON = await _evaluateJavascript( + controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); + final Map initialInputClientRectRelativeToViewport = + jsonDecode(initialInputClientRectJSON); + + expect( + initialInputClientRectRelativeToViewport['bottom'] <= + viewportRectRelativeToViewport['bottom'], + isFalse); + + await controller.evaluateJavascript('inputEl.focus()'); + + // Check that focusing the input brought it into view. + + final String lastInputClientRectJSON = await _evaluateJavascript( + controller, 'JSON.stringify(inputEl.getBoundingClientRect())'); + final Map lastInputClientRectRelativeToViewport = + jsonDecode(lastInputClientRectJSON); + + expect( + lastInputClientRectRelativeToViewport['top'] >= + viewportRectRelativeToViewport['top'], + isTrue); + expect( + lastInputClientRectRelativeToViewport['bottom'] <= + viewportRectRelativeToViewport['bottom'], + isTrue); + + expect( + lastInputClientRectRelativeToViewport['left'] >= + viewportRectRelativeToViewport['left'], + isTrue); + expect( + lastInputClientRectRelativeToViewport['right'] <= + viewportRectRelativeToViewport['right'], + isTrue); + }, skip: _skipDueToIssue86757); + }); + + group('NavigationDelegate', () { + final String blankPage = ""; + final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + + base64Encode(const Utf8Encoder().convert(blankPage)); + + testWidgets('can allow requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller.evaluateJavascript('location.href = "$secondaryUrl"'); + + await pageLoads.stream.first; // Wait for the next page load. + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); + }); + + testWidgets('onWebResourceError', (WidgetTester tester) async { + final Completer errorCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://www.notawebsite..com', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + ), + ), + ); + + final WebResourceError error = await errorCompleter.future; + expect(error, isNotNull); + + expect(error.errorType, isNotNull); + expect( + error.failingUrl?.startsWith('https://www.notawebsite..com'), isTrue); + }); + + testWidgets('onWebResourceError is not called with valid url', + (WidgetTester tester) async { + final Completer errorCompleter = + Completer(); + final Completer pageFinishCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + onPageFinished: (_) => pageFinishCompleter.complete(), + ), + ), + ); + + expect(errorCompleter.future, doesNotComplete); + await pageFinishCompleter.future; + }); + + testWidgets( + 'onWebResourceError only called for main frame', + (WidgetTester tester) async { + final String iframeTest = ''' + + + + WebResourceError test + + + + + + '''; + final String iframeTestBase64 = + base64Encode(const Utf8Encoder().convert(iframeTest)); + + final Completer errorCompleter = + Completer(); + final Completer pageFinishCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: + 'data:text/html;charset=utf-8;base64,$iframeTestBase64', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + onPageFinished: (_) => pageFinishCompleter.complete(), + ), + ), + ); + + expect(errorCompleter.future, doesNotComplete); + await pageFinishCompleter.future; + }, + ); + + testWidgets('can block requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller + .evaluateJavascript('location.href = "https://www.youtube.com/"'); + + // There should never be any second page load, since our new URL is + // blocked. Still wait for a potential page change for some time in order + // to give the test a chance to fail. + await pageLoads.stream.first + .timeout(const Duration(milliseconds: 500), onTimeout: () => ''); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, isNot(contains('youtube.com'))); + }); + + testWidgets('supports asynchronous decisions', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) async { + NavigationDecision decision = NavigationDecision.prevent; + decision = await Future.delayed( + const Duration(milliseconds: 10), + () => NavigationDecision.navigate); + return decision; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller.evaluateJavascript('location.href = "$secondaryUrl"'); + + await pageLoads.stream.first; // Wait for second page to load. + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); + }); + }); + + testWidgets('launches with gestureNavigationEnabled on iOS', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: SizedBox( + width: 400, + height: 300, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + gestureNavigationEnabled: true, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); + }); + + testWidgets('target _blank opens in same window', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageLoaded = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await controller.evaluateJavascript('window.open("$primaryUrl", "_blank")'); + await pageLoaded.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); + }, + // Flaky on Android: https://github.com/flutter/flutter/issues/86757 + skip: _skipDueToIssue86757); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets( + 'can open new window and go back', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(); + }, + initialUrl: primaryUrl, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + expect(controller.currentUrl(), completion(primaryUrl)); + await pageLoaded.future; + pageLoaded = Completer(); + + await controller.evaluateJavascript('window.open("$secondaryUrl")'); + await pageLoaded.future; + pageLoaded = Completer(); + expect(controller.currentUrl(), completion(secondaryUrl)); + + expect(controller.canGoBack(), completion(true)); + await controller.goBack(); + await pageLoaded.future; + expect(controller.currentUrl(), completion(primaryUrl)); + }, + skip: _skipDueToIssue86757, + ); + + testWidgets( + 'javascript does not run in parent window', + (WidgetTester tester) async { + final String iframe = ''' + + + '''; + final String iframeTestBase64 = + base64Encode(const Utf8Encoder().convert(iframe)); + + final String openWindowTest = ''' + + + + XSS test + + + + + + '''; + final String openWindowTestBase64 = + base64Encode(const Utf8Encoder().convert(openWindowTest)); + final Completer controllerCompleter = + Completer(); + final Completer pageLoadCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + initialUrl: + 'data:text/html;charset=utf-8;base64,$openWindowTestBase64', + onPageFinished: (String url) { + pageLoadCompleter.complete(); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageLoadCompleter.future; + + expect(controller.evaluateJavascript('iframeLoaded'), completion('true')); + expect( + controller.evaluateJavascript( + 'document.querySelector("p") && document.querySelector("p").textContent'), + completion('null'), + ); + }, + ); +} + +// JavaScript booleans evaluate to different string values on Android and iOS. +// This utility method returns the string boolean value of the current platform. +String _webviewBool(bool value) { + if (defaultTargetPlatform == TargetPlatform.iOS) { + return value ? '1' : '0'; + } + return value ? 'true' : 'false'; +} + +/// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. +Future _getUserAgent(WebViewController controller) async { + return _evaluateJavascript(controller, 'navigator.userAgent;'); +} + +Future _evaluateJavascript( + WebViewController controller, String js) async { + return jsonDecode(await controller.evaluateJavascript(js)); +} diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/main.dart b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart new file mode 100644 index 000000000000..65f49716aaac --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/lib/main.dart @@ -0,0 +1,344 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:webview_flutter_android/webview_surface_android.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'web_view.dart'; + +void main() { + // Configure the [WebView] to use the [SurfaceAndroidWebView] + // implementation instead of the default [AndroidWebView]. + WebView.platform = SurfaceAndroidWebView(); + + runApp(MaterialApp(home: _WebViewExample())); +} + +const String kNavigationExamplePage = ''' + +Navigation Delegate Example + +

      +The navigation delegate is set to block navigation to the youtube website. +

      + + + +'''; + +class _WebViewExample extends StatefulWidget { + const _WebViewExample({Key? key}) : super(key: key); + + @override + _WebViewExampleState createState() => _WebViewExampleState(); +} + +class _WebViewExampleState extends State<_WebViewExample> { + final Completer _controller = + Completer(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Flutter WebView example'), + // This drop down menu demonstrates that Flutter widgets can be shown over the web view. + actions: [ + _NavigationControls(_controller.future), + _SampleMenu(_controller.future), + ], + ), + // We're using a Builder here so we have a context that is below the Scaffold + // to allow calling Scaffold.of(context) so we can show a snackbar. + body: Builder(builder: (context) { + return WebView( + initialUrl: 'https://flutter.dev', + onWebViewCreated: (WebViewController controller) { + _controller.complete(controller); + }, + javascriptChannels: _createJavascriptChannels(context), + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent', + ); + }), + floatingActionButton: favoriteButton(), + ); + } + + Widget favoriteButton() { + return FutureBuilder( + future: _controller.future, + builder: (BuildContext context, + AsyncSnapshot controller) { + if (controller.hasData) { + return FloatingActionButton( + onPressed: () async { + final String url = (await controller.data!.currentUrl())!; + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + SnackBar(content: Text('Favorited $url')), + ); + }, + child: const Icon(Icons.favorite), + ); + } + return Container(); + }); + } +} + +Set _createJavascriptChannels(BuildContext context) { + return { + JavascriptChannel( + name: 'Snackbar', + onMessageReceived: (JavascriptMessage message) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(message.message))); + }), + }; +} + +enum _MenuOptions { + showUserAgent, + listCookies, + clearCookies, + addToCache, + listCache, + clearCache, + navigationDelegate, +} + +class _SampleMenu extends StatelessWidget { + _SampleMenu(this.controller); + + final Future controller; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: controller, + builder: + (BuildContext context, AsyncSnapshot controller) { + return PopupMenuButton<_MenuOptions>( + onSelected: (_MenuOptions value) { + switch (value) { + case _MenuOptions.showUserAgent: + _onShowUserAgent(controller.data!, context); + break; + case _MenuOptions.listCookies: + _onListCookies(controller.data!, context); + break; + case _MenuOptions.clearCookies: + _onClearCookies(controller.data!, context); + break; + case _MenuOptions.addToCache: + _onAddToCache(controller.data!, context); + break; + case _MenuOptions.listCache: + _onListCache(controller.data!, context); + break; + case _MenuOptions.clearCache: + _onClearCache(controller.data!, context); + break; + case _MenuOptions.navigationDelegate: + _onNavigationDelegateExample(controller.data!, context); + break; + } + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem<_MenuOptions>( + value: _MenuOptions.showUserAgent, + child: const Text('Show user agent'), + enabled: controller.hasData, + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.listCookies, + child: Text('List cookies'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.clearCookies, + child: Text('Clear cookies'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.addToCache, + child: Text('Add to cache'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.listCache, + child: Text('List cache'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.clearCache, + child: Text('Clear cache'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.navigationDelegate, + child: Text('Navigation Delegate example'), + ), + ], + ); + }, + ); + } + + void _onShowUserAgent( + WebViewController controller, BuildContext context) async { + // Send a message with the user agent string to the Snackbar JavaScript channel we registered + // with the WebView. + await controller.evaluateJavascript( + 'Snackbar.postMessage("User Agent: " + navigator.userAgent);'); + } + + void _onListCookies( + WebViewController controller, BuildContext context) async { + final String cookies = + await controller.evaluateJavascript('document.cookie'); + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar(SnackBar( + content: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Cookies:'), + _getCookieList(cookies), + ], + ), + )); + } + + void _onAddToCache(WebViewController controller, BuildContext context) async { + await controller.evaluateJavascript( + 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar(const SnackBar( + content: Text('Added a test entry to cache.'), + )); + } + + void _onListCache(WebViewController controller, BuildContext context) async { + await controller.evaluateJavascript('caches.keys()' + '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' + '.then((caches) => Snackbar.postMessage(caches))'); + } + + void _onClearCache(WebViewController controller, BuildContext context) async { + await controller.clearCache(); + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar(const SnackBar( + content: Text("Cache cleared."), + )); + } + + void _onClearCookies( + WebViewController controller, BuildContext context) async { + final bool hadCookies = await WebView.platform.clearCookies(); + String message = 'There were cookies. Now, they are gone!'; + if (!hadCookies) { + message = 'There are no cookies.'; + } + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar(SnackBar( + content: Text(message), + )); + } + + void _onNavigationDelegateExample( + WebViewController controller, BuildContext context) async { + final String contentBase64 = + base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); + await controller.loadUrl('data:text/html;base64,$contentBase64'); + } + + Widget _getCookieList(String cookies) { + if (cookies == null || cookies == '""') { + return Container(); + } + final List cookieList = cookies.split(';'); + final Iterable cookieWidgets = + cookieList.map((String cookie) => Text(cookie)); + return Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: cookieWidgets.toList(), + ); + } +} + +class _NavigationControls extends StatelessWidget { + const _NavigationControls(this._webViewControllerFuture) + : assert(_webViewControllerFuture != null); + + final Future _webViewControllerFuture; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _webViewControllerFuture, + builder: + (BuildContext context, AsyncSnapshot snapshot) { + final bool webViewReady = + snapshot.connectionState == ConnectionState.done; + final WebViewController? controller = snapshot.data; + + return Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: !webViewReady + ? null + : () async { + if (await controller!.canGoBack()) { + await controller.goBack(); + } else { + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + const SnackBar(content: Text("No back history item")), + ); + return; + } + }, + ), + IconButton( + icon: const Icon(Icons.arrow_forward_ios), + onPressed: !webViewReady + ? null + : () async { + if (await controller!.canGoForward()) { + await controller.goForward(); + } else { + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + const SnackBar( + content: Text("No forward history item")), + ); + return; + } + }, + ), + IconButton( + icon: const Icon(Icons.replay), + onPressed: !webViewReady + ? null + : () { + controller!.reload(); + }, + ), + ], + ); + }, + ); + } +} + +/// Callback type for handling messages sent from Javascript running in a web view. +typedef void JavascriptMessageHandler(JavascriptMessage message); diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/navigation_decision.dart b/packages/webview_flutter/webview_flutter_android/example/lib/navigation_decision.dart new file mode 100644 index 000000000000..d8178acd8096 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/lib/navigation_decision.dart @@ -0,0 +1,12 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// A decision on how to handle a navigation request. +enum NavigationDecision { + /// Prevent the navigation from taking place. + prevent, + + /// Allow the navigation to take place. + navigate, +} diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/navigation_request.dart b/packages/webview_flutter/webview_flutter_android/example/lib/navigation_request.dart new file mode 100644 index 000000000000..c1ff8dc5a690 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/lib/navigation_request.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. + +/// Information about a navigation action that is about to be executed. +class NavigationRequest { + NavigationRequest._({required this.url, required this.isForMainFrame}); + + /// The URL that will be loaded if the navigation is executed. + final String url; + + /// Whether the navigation request is to be loaded as the main frame. + final bool isForMainFrame; + + @override + String toString() { + return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)'; + } +} diff --git a/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart new file mode 100644 index 000000000000..33773f96cad8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/lib/web_view.dart @@ -0,0 +1,617 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:webview_flutter_android/webview_android.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'navigation_decision.dart'; +import 'navigation_request.dart'; + +/// Optional callback invoked when a web view is first created. [controller] is +/// the [WebViewController] for the created web view. +typedef void WebViewCreatedCallback(WebViewController controller); + +/// Decides how to handle a specific navigation request. +/// +/// The returned [NavigationDecision] determines how the navigation described by +/// `navigation` should be handled. +/// +/// See also: [WebView.navigationDelegate]. +typedef FutureOr NavigationDelegate( + NavigationRequest navigation); + +/// Signature for when a [WebView] has started loading a page. +typedef void PageStartedCallback(String url); + +/// Signature for when a [WebView] has finished loading a page. +typedef void PageFinishedCallback(String url); + +/// Signature for when a [WebView] is loading a page. +typedef void PageLoadingCallback(int progress); + +/// Signature for when a [WebView] has failed to load a resource. +typedef void WebResourceErrorCallback(WebResourceError error); + +/// A web view widget for showing html content. +/// +/// The [WebView] widget wraps around the [AndroidWebView] or +/// [SurfaceAndroidWebView] classes and acts like a facade which makes it easier +/// to inject a [AndroidWebView] or [SurfaceAndroidWebView] control into the +/// widget tree. +/// +/// The [WebView] widget is controlled using the [WebViewController] which is +/// provided through the `onWebViewCreated` callback. +/// +/// In this example project it's main purpose is to facilitate integration +/// testing of the `webview_flutter_android` package. +class WebView extends StatefulWidget { + /// Creates a new web view. + /// + /// The web view can be controlled using a `WebViewController` that is passed to the + /// `onWebViewCreated` callback once the web view is created. + /// + /// The `javascriptMode` and `autoMediaPlaybackPolicy` parameters must not be null. + const WebView({ + Key? key, + this.onWebViewCreated, + this.initialUrl, + this.javascriptMode = JavascriptMode.disabled, + this.javascriptChannels, + this.navigationDelegate, + this.gestureRecognizers, + this.onPageStarted, + this.onPageFinished, + this.onProgress, + this.onWebResourceError, + this.debuggingEnabled = false, + this.gestureNavigationEnabled = false, + this.userAgent, + this.initialMediaPlaybackPolicy = + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + this.allowsInlineMediaPlayback = false, + }) : assert(javascriptMode != null), + assert(initialMediaPlaybackPolicy != null), + assert(allowsInlineMediaPlayback != null), + super(key: key); + + static WebViewPlatform _platform = AndroidWebView(); + + /// The WebView platform that's used by this WebView. + /// + /// The default value is [AndroidWebView]. + static WebViewPlatform get platform => _platform; + + /// Sets a custom [WebViewPlatform]. + /// + /// This property can be set to use a custom platform implementation for WebViews. + /// + /// Setting `platform` doesn't affect [WebView]s that were already created. + /// + /// The default value is [AndroidWebView] on Android and [CupertinoWebView] on iOS. + static set platform(WebViewPlatform platform) { + _platform = platform; + } + + /// If not null invoked once the web view is created. + final WebViewCreatedCallback? onWebViewCreated; + + /// Which gestures should be consumed by the web view. + /// + /// It is possible for other gesture recognizers to be competing with the web view on pointer + /// events, e.g if the web view is inside a [ListView] the [ListView] will want to handle + /// vertical drags. The web view will claim gestures that are recognized by any of the + /// recognizers on this list. + /// + /// When this set is empty or null, the web view will only handle pointer events for gestures that + /// were not claimed by any other gesture recognizer. + final Set>? gestureRecognizers; + + /// The initial URL to load. + final String? initialUrl; + + /// Whether Javascript execution is enabled. + final JavascriptMode javascriptMode; + + /// The set of [JavascriptChannel]s available to JavaScript code running in the web view. + /// + /// For each [JavascriptChannel] in the set, a channel object is made available for the + /// JavaScript code in a window property named [JavascriptChannel.name]. + /// The JavaScript code can then call `postMessage` on that object to send a message that will be + /// passed to [JavascriptChannel.onMessageReceived]. + /// + /// For example for the following JavascriptChannel: + /// + /// ```dart + /// JavascriptChannel(name: 'Print', onMessageReceived: (JavascriptMessage message) { print(message.message); }); + /// ``` + /// + /// JavaScript code can call: + /// + /// ```javascript + /// Print.postMessage('Hello'); + /// ``` + /// + /// To asynchronously invoke the message handler which will print the message to standard output. + /// + /// Adding a new JavaScript channel only takes affect after the next page is loaded. + /// + /// Set values must not be null. A [JavascriptChannel.name] cannot be the same for multiple + /// channels in the list. + /// + /// A null value is equivalent to an empty set. + final Set? javascriptChannels; + + /// A delegate function that decides how to handle navigation actions. + /// + /// When a navigation is initiated by the WebView (e.g when a user clicks a link) + /// this delegate is called and has to decide how to proceed with the navigation. + /// + /// See [NavigationDecision] for possible decisions the delegate can take. + /// + /// When null all navigation actions are allowed. + /// + /// Caveats on Android: + /// + /// * Navigation actions targeted to the main frame can be intercepted, + /// navigation actions targeted to subframes are allowed regardless of the value + /// returned by this delegate. + /// * Setting a navigationDelegate makes the WebView treat all navigations as if they were + /// triggered by a user gesture, this disables some of Chromium's security mechanisms. + /// A navigationDelegate should only be set when loading trusted content. + /// * On Android WebView versions earlier than 67(most devices running at least Android L+ should have + /// a later version): + /// * When a navigationDelegate is set pages with frames are not properly handled by the + /// webview, and frames will be opened in the main frame. + /// * When a navigationDelegate is set HTTP requests do not include the HTTP referer header. + final NavigationDelegate? navigationDelegate; + + /// Controls whether inline playback of HTML5 videos is allowed on iOS. + /// + /// This field is ignored on Android because Android allows it by default. + /// + /// By default `allowsInlineMediaPlayback` is false. + final bool allowsInlineMediaPlayback; + + /// Invoked when a page starts loading. + final PageStartedCallback? onPageStarted; + + /// Invoked when a page has finished loading. + /// + /// This is invoked only for the main frame. + /// + /// When [onPageFinished] is invoked on Android, the page being rendered may + /// not be updated yet. + /// + /// When invoked on iOS or Android, any Javascript code that is embedded + /// directly in the HTML has been loaded and code injected with + /// [WebViewController.evaluateJavascript] can assume this. + final PageFinishedCallback? onPageFinished; + + /// Invoked when a page is loading. + final PageLoadingCallback? onProgress; + + /// Invoked when a web resource has failed to load. + /// + /// This callback is only called for the main page. + final WebResourceErrorCallback? onWebResourceError; + + /// Controls whether WebView debugging is enabled. + /// + /// Setting this to true enables [WebView debugging on Android](https://developers.google.com/web/tools/chrome-devtools/remote-debugging/). + /// + /// WebView debugging is enabled by default in dev builds on iOS. + /// + /// To debug WebViews on iOS: + /// - Enable developer options (Open Safari, go to Preferences -> Advanced and make sure "Show Develop Menu in Menubar" is on.) + /// - From the Menu-bar (of Safari) select Develop -> iPhone Simulator -> + /// + /// By default `debuggingEnabled` is false. + final bool debuggingEnabled; + + /// A Boolean value indicating whether horizontal swipe gestures will trigger back-forward list navigations. + /// + /// This only works on iOS. + /// + /// By default `gestureNavigationEnabled` is false. + final bool gestureNavigationEnabled; + + /// The value used for the HTTP User-Agent: request header. + /// + /// When null the platform's webview default is used for the User-Agent header. + /// + /// When the [WebView] is rebuilt with a different `userAgent`, the page reloads and the request uses the new User Agent. + /// + /// When [WebViewController.goBack] is called after changing `userAgent` the previous `userAgent` value is used until the page is reloaded. + /// + /// This field is ignored on iOS versions prior to 9 as the platform does not support a custom + /// user agent. + /// + /// By default `userAgent` is null. + final String? userAgent; + + /// Which restrictions apply on automatic media playback. + /// + /// This initial value is applied to the platform's webview upon creation. Any following + /// changes to this parameter are ignored (as long as the state of the [WebView] is preserved). + /// + /// The default policy is [AutoMediaPlaybackPolicy.require_user_action_for_all_media_types]. + final AutoMediaPlaybackPolicy initialMediaPlaybackPolicy; + + @override + _WebViewState createState() => _WebViewState(); +} + +class _WebViewState extends State { + final Completer _controller = + Completer(); + late final JavascriptChannelRegistry _javascriptChannelRegistry; + late final _PlatformCallbacksHandler _platformCallbacksHandler; + + @override + void initState() { + super.initState(); + _platformCallbacksHandler = _PlatformCallbacksHandler(widget); + _javascriptChannelRegistry = + JavascriptChannelRegistry(widget.javascriptChannels); + } + + @override + void didUpdateWidget(WebView oldWidget) { + super.didUpdateWidget(oldWidget); + _controller.future.then((WebViewController controller) { + controller.updateWidget(widget); + }); + } + + @override + Widget build(BuildContext context) { + return WebView.platform.build( + context: context, + onWebViewPlatformCreated: + (WebViewPlatformController? webViewPlatformController) { + WebViewController controller = WebViewController( + widget, + webViewPlatformController!, + _javascriptChannelRegistry, + ); + _controller.complete(controller); + + if (widget.onWebViewCreated != null) { + widget.onWebViewCreated!(controller); + } + }, + webViewPlatformCallbacksHandler: _platformCallbacksHandler, + creationParams: CreationParams( + initialUrl: widget.initialUrl, + webSettings: _webSettingsFromWidget(widget), + javascriptChannelNames: + _javascriptChannelRegistry.channels.keys.toSet(), + autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, + userAgent: widget.userAgent, + ), + javascriptChannelRegistry: _javascriptChannelRegistry, + ); + } +} + +class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { + _PlatformCallbacksHandler(this._webView); + + final WebView _webView; + + @override + FutureOr onNavigationRequest({ + required String url, + required bool isForMainFrame, + }) async { + if (url.startsWith('https://www.youtube.com/')) { + print('blocking navigation to $url'); + return false; + } + print('allowing navigation to $url'); + return true; + } + + @override + void onPageStarted(String url) { + if (_webView.onPageStarted != null) { + _webView.onPageStarted!(url); + } + } + + @override + void onPageFinished(String url) { + if (_webView.onPageFinished != null) { + _webView.onPageFinished!(url); + } + } + + @override + void onProgress(int progress) { + if (_webView.onProgress != null) { + _webView.onProgress!(progress); + } + } + + void onWebResourceError(WebResourceError error) { + if (_webView.onWebResourceError != null) { + _webView.onWebResourceError!(error); + } + } +} + +/// Controls a [WebView]. +/// +/// A [WebViewController] instance can be obtained by setting the [WebView.onWebViewCreated] +/// callback for a [WebView] widget. +class WebViewController { + /// Creates a [WebViewController] which can be used to control the provided + /// [WebView] widget. + WebViewController( + this._widget, + this._webViewPlatformController, + this._javascriptChannelRegistry, + ) : assert(_webViewPlatformController != null) { + _settings = _webSettingsFromWidget(_widget); + } + + final JavascriptChannelRegistry _javascriptChannelRegistry; + + final WebViewPlatformController _webViewPlatformController; + + late WebSettings _settings; + + WebView _widget; + + /// Loads the specified URL. + /// + /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will + /// be added as key value pairs of HTTP headers for the request. + /// + /// `url` must not be null. + /// + /// Throws an ArgumentError if `url` is not a valid URL string. + Future loadUrl( + String url, { + Map? headers, + }) async { + assert(url != null); + _validateUrlString(url); + return _webViewPlatformController.loadUrl(url, headers); + } + + /// Accessor to the current URL that the WebView is displaying. + /// + /// If [WebView.initialUrl] was never specified, returns `null`. + /// Note that this operation is asynchronous, and it is possible that the + /// current URL changes again by the time this function returns (in other + /// words, by the time this future completes, the WebView may be displaying a + /// different URL). + Future currentUrl() { + return _webViewPlatformController.currentUrl(); + } + + /// Checks whether there's a back history item. + /// + /// Note that this operation is asynchronous, and it is possible that the "canGoBack" state has + /// changed by the time the future completed. + Future canGoBack() { + return _webViewPlatformController.canGoBack(); + } + + /// Checks whether there's a forward history item. + /// + /// Note that this operation is asynchronous, and it is possible that the "canGoForward" state has + /// changed by the time the future completed. + Future canGoForward() { + return _webViewPlatformController.canGoForward(); + } + + /// Goes back in the history of this WebView. + /// + /// If there is no back history item this is a no-op. + Future goBack() { + return _webViewPlatformController.goBack(); + } + + /// Goes forward in the history of this WebView. + /// + /// If there is no forward history item this is a no-op. + Future goForward() { + return _webViewPlatformController.goForward(); + } + + /// Reloads the current URL. + Future reload() { + return _webViewPlatformController.reload(); + } + + /// Clears all caches used by the [WebView]. + /// + /// The following caches are cleared: + /// 1. Browser HTTP Cache. + /// 2. [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api) caches. + /// These are not yet supported in iOS WkWebView. Service workers tend to use this cache. + /// 3. Application cache. + /// 4. Local Storage. + /// + /// Note: Calling this method also triggers a reload. + Future clearCache() async { + await _webViewPlatformController.clearCache(); + return reload(); + } + + /// Update the widget managed by the [WebViewController]. + Future updateWidget(WebView widget) async { + _widget = widget; + await _updateSettings(_webSettingsFromWidget(widget)); + await _updateJavascriptChannels( + _javascriptChannelRegistry.channels.values.toSet()); + } + + Future _updateSettings(WebSettings newSettings) { + final WebSettings update = + _clearUnchangedWebSettings(_settings, newSettings); + _settings = newSettings; + return _webViewPlatformController.updateSettings(update); + } + + Future _updateJavascriptChannels( + Set? newChannels) async { + final Set currentChannels = + _javascriptChannelRegistry.channels.keys.toSet(); + final Set newChannelNames = _extractChannelNames(newChannels); + final Set channelsToAdd = + newChannelNames.difference(currentChannels); + final Set channelsToRemove = + currentChannels.difference(newChannelNames); + if (channelsToRemove.isNotEmpty) { + await _webViewPlatformController + .removeJavascriptChannels(channelsToRemove); + } + if (channelsToAdd.isNotEmpty) { + await _webViewPlatformController.addJavascriptChannels(channelsToAdd); + } + _javascriptChannelRegistry.updateJavascriptChannelsFromSet(newChannels); + } + + /// Evaluates a JavaScript expression in the context of the current page. + /// + /// On Android returns the evaluation result as a JSON formatted string. + /// + /// On iOS depending on the value type the return value would be one of: + /// + /// - For primitive JavaScript types: the value string formatted (e.g JavaScript 100 returns '100'). + /// - For JavaScript arrays of supported types: a string formatted NSArray(e.g '(1,2,3), note that the string for NSArray is formatted and might contain newlines and extra spaces.'). + /// - Other non-primitive types are not supported on iOS and will complete the Future with an error. + /// + /// The Future completes with an error if a JavaScript error occurred, or on iOS, if the type of the + /// evaluated expression is not supported as described above. + /// + /// When evaluating Javascript in a [WebView], it is best practice to wait for + /// the [WebView.onPageFinished] callback. This guarantees all the Javascript + /// embedded in the main frame HTML has been loaded. + Future evaluateJavascript(String javascriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); + } + // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. + // https://github.com/flutter/flutter/issues/26431 + // ignore: strong_mode_implicit_dynamic_method + return _webViewPlatformController.evaluateJavascript(javascriptString); + } + + /// Returns the title of the currently loaded page. + Future getTitle() { + return _webViewPlatformController.getTitle(); + } + + /// Sets the WebView's content scroll position. + /// + /// The parameters `x` and `y` specify the scroll position in WebView pixels. + Future scrollTo(int x, int y) { + return _webViewPlatformController.scrollTo(x, y); + } + + /// Move the scrolled position of this view. + /// + /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by horizontally and vertically respectively. + Future scrollBy(int x, int y) { + return _webViewPlatformController.scrollBy(x, y); + } + + /// Return the horizontal scroll position, in WebView pixels, of this view. + /// + /// Scroll position is measured from left. + Future getScrollX() { + return _webViewPlatformController.getScrollX(); + } + + /// Return the vertical scroll position, in WebView pixels, of this view. + /// + /// Scroll position is measured from top. + Future getScrollY() { + return _webViewPlatformController.getScrollY(); + } + + // This method assumes that no fields in `currentValue` are null. + WebSettings _clearUnchangedWebSettings( + WebSettings currentValue, WebSettings newValue) { + assert(currentValue.javascriptMode != null); + assert(currentValue.hasNavigationDelegate != null); + assert(currentValue.hasProgressTracking != null); + assert(currentValue.debuggingEnabled != null); + assert(currentValue.userAgent != null); + assert(newValue.javascriptMode != null); + assert(newValue.hasNavigationDelegate != null); + assert(newValue.debuggingEnabled != null); + assert(newValue.userAgent != null); + + JavascriptMode? javascriptMode; + bool? hasNavigationDelegate; + bool? hasProgressTracking; + bool? debuggingEnabled; + WebSetting userAgent = WebSetting.absent(); + if (currentValue.javascriptMode != newValue.javascriptMode) { + javascriptMode = newValue.javascriptMode; + } + if (currentValue.hasNavigationDelegate != newValue.hasNavigationDelegate) { + hasNavigationDelegate = newValue.hasNavigationDelegate; + } + if (currentValue.hasProgressTracking != newValue.hasProgressTracking) { + hasProgressTracking = newValue.hasProgressTracking; + } + if (currentValue.debuggingEnabled != newValue.debuggingEnabled) { + debuggingEnabled = newValue.debuggingEnabled; + } + if (currentValue.userAgent != newValue.userAgent) { + userAgent = newValue.userAgent; + } + + return WebSettings( + javascriptMode: javascriptMode, + hasNavigationDelegate: hasNavigationDelegate, + hasProgressTracking: hasProgressTracking, + debuggingEnabled: debuggingEnabled, + userAgent: userAgent, + ); + } + + Set _extractChannelNames(Set? channels) { + final Set channelNames = channels == null + ? {} + : channels.map((JavascriptChannel channel) => channel.name).toSet(); + return channelNames; + } + + // Throws an ArgumentError if `url` is not a valid URL string. + void _validateUrlString(String url) { + try { + final Uri uri = Uri.parse(url); + if (uri.scheme.isEmpty) { + throw ArgumentError('Missing scheme in URL string: "$url"'); + } + } on FormatException catch (e) { + throw ArgumentError(e); + } + } +} + +WebSettings _webSettingsFromWidget(WebView widget) { + return WebSettings( + javascriptMode: widget.javascriptMode, + hasNavigationDelegate: widget.navigationDelegate != null, + hasProgressTracking: widget.onProgress != null, + debuggingEnabled: widget.debuggingEnabled, + gestureNavigationEnabled: widget.gestureNavigationEnabled, + allowsInlineMediaPlayback: widget.allowsInlineMediaPlayback, + userAgent: WebSetting.of(widget.userAgent), + ); +} diff --git a/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml new file mode 100644 index 000000000000..1e065a6a5b0b --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/pubspec.yaml @@ -0,0 +1,33 @@ +name: webview_flutter_android_example +description: Demonstrates how to use the webview_flutter_android plugin. +publish_to: none + +environment: + sdk: ">=2.14.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + webview_flutter_android: + # When depending on this package from a real application you should use: + # webview_flutter: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + +dev_dependencies: + espresso: ^0.1.0+2 + flutter_test: + sdk: flutter + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + pedantic: ^1.10.0 + +flutter: + uses-material-design: true + assets: + - assets/sample_audio.ogg + - assets/sample_video.mp4 diff --git a/packages/webview_flutter/webview_flutter_android/example/test_driver/integration_test.dart b/packages/webview_flutter/webview_flutter_android/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart new file mode 100644 index 000000000000..a48e457d55ad --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_android.dart @@ -0,0 +1,62 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +/// Builds an Android webview. +/// +/// This is used as the default implementation for [WebView.platform] on Android. It uses +/// an [AndroidView] to embed the webview in the widget hierarchy, and uses a method channel to +/// communicate with the platform code. +class AndroidWebView implements WebViewPlatform { + @override + Widget build({ + required BuildContext context, + required CreationParams creationParams, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + required JavascriptChannelRegistry javascriptChannelRegistry, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, + }) { + assert(webViewPlatformCallbacksHandler != null); + return GestureDetector( + // We prevent text selection by intercepting the long press event. + // This is a temporary stop gap due to issues with text selection on Android: + // https://github.com/flutter/flutter/issues/24585 - the text selection + // dialog is not responding to touch events. + // https://github.com/flutter/flutter/issues/24584 - the text selection + // handles are not showing. + // TODO(amirh): remove this when the issues above are fixed. + onLongPress: () {}, + excludeFromSemantics: true, + child: AndroidView( + viewType: 'plugins.flutter.io/webview', + onPlatformViewCreated: (int id) { + if (onWebViewPlatformCreated == null) { + return; + } + onWebViewPlatformCreated(MethodChannelWebViewPlatform( + id, + webViewPlatformCallbacksHandler, + javascriptChannelRegistry, + )); + }, + gestureRecognizers: gestureRecognizers, + layoutDirection: Directionality.maybeOf(context) ?? TextDirection.rtl, + creationParams: + MethodChannelWebViewPlatform.creationParamsToMap(creationParams), + creationParamsCodec: const StandardMessageCodec(), + ), + ); + } + + @override + Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); +} diff --git a/packages/webview_flutter/webview_flutter_android/lib/webview_surface_android.dart b/packages/webview_flutter/webview_flutter_android/lib/webview_surface_android.dart new file mode 100644 index 000000000000..6beae105e2e5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/lib/webview_surface_android.dart @@ -0,0 +1,78 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'webview_android.dart'; + +/// Android [WebViewPlatform] that uses [AndroidViewSurface] to build the [WebView] widget. +/// +/// To use this, set [WebView.platform] to an instance of this class. +/// +/// This implementation uses hybrid composition to render the [WebView] on +/// Android. It solves multiple issues related to accessibility and interaction +/// with the [WebView] at the cost of some performance on Android versions below +/// 10. See https://github.com/flutter/flutter/wiki/Hybrid-Composition for more +/// information. +class SurfaceAndroidWebView extends AndroidWebView { + @override + Widget build({ + required BuildContext context, + required CreationParams creationParams, + required JavascriptChannelRegistry javascriptChannelRegistry, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + }) { + assert(webViewPlatformCallbacksHandler != null); + return PlatformViewLink( + viewType: 'plugins.flutter.io/webview', + surfaceFactory: ( + BuildContext context, + PlatformViewController controller, + ) { + return AndroidViewSurface( + controller: controller as AndroidViewController, + gestureRecognizers: gestureRecognizers ?? + const >{}, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + onCreatePlatformView: (PlatformViewCreationParams params) { + return PlatformViewsService.initSurfaceAndroidView( + id: params.id, + viewType: 'plugins.flutter.io/webview', + // WebView content is not affected by the Android view's layout direction, + // we explicitly set it here so that the widget doesn't require an ambient + // directionality. + layoutDirection: TextDirection.rtl, + creationParams: MethodChannelWebViewPlatform.creationParamsToMap( + creationParams, + usesHybridComposition: true, + ), + creationParamsCodec: const StandardMessageCodec(), + ) + ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated) + ..addOnPlatformViewCreatedListener((int id) { + if (onWebViewPlatformCreated == null) { + return; + } + onWebViewPlatformCreated( + MethodChannelWebViewPlatform( + id, + webViewPlatformCallbacksHandler, + javascriptChannelRegistry, + ), + ); + }) + ..create(); + }, + ); + } +} diff --git a/packages/webview_flutter/webview_flutter_android/pubspec.yaml b/packages/webview_flutter/webview_flutter_android/pubspec.yaml new file mode 100644 index 000000000000..36f186087c08 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_android/pubspec.yaml @@ -0,0 +1,31 @@ +name: webview_flutter_android +description: A Flutter plugin that provides a WebView widget on Android. +repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_android +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 +version: 2.0.15 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" + +flutter: + plugin: + implements: webview_flutter + platforms: + android: + package: io.flutter.plugins.webviewflutter + pluginClass: WebViewFlutterPlugin + +dependencies: + flutter: + sdk: flutter + + webview_flutter_platform_interface: ^1.0.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + pedantic: ^1.10.0 + diff --git a/packages/webview_flutter/webview_flutter_platform_interface/AUTHORS b/packages/webview_flutter/webview_flutter_platform_interface/AUTHORS new file mode 100644 index 000000000000..78f9e5ad9f6b --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/AUTHORS @@ -0,0 +1,67 @@ +# Below is a list of people and organizations that have contributed +# to the Flutter project. Names should be added to the list like so: +# +# Name/Organization + +Google Inc. +The Chromium Authors +German Saprykin +Benjamin Sauer +larsenthomasj@gmail.com +Ali Bitek +Pol Batlló +Anatoly Pulyaevskiy +Hayden Flinner +Stefano Rodriguez +Salvatore Giordano +Brian Armstrong +Paul DeMarco +Fabricio Nogueira +Simon Lightfoot +Ashton Thomas +Thomas Danner +Diego Velásquez +Hajime Nakamura +Tuyển Vũ Xuân +Miguel Ruivo +Sarthak Verma +Mike Diarmid +Invertase +Elliot Hesp +Vince Varga +Aawaz Gyawali +EUI Limited +Katarina Sheremet +Thomas Stockx +Sarbagya Dhaubanjar +Ozkan Eksi +Rishab Nayak +ko2ic +Jonathan Younger +Jose Sanchez +Debkanchan Samadder +Audrius Karosevicius +Lukasz Piliszczuk +SoundReply Solutions GmbH +Rafal Wachol +Pau Picas +Christian Weder +Alexandru Tuca +Christian Weder +Rhodes Davis Jr. +Luigi Agosti +Quentin Le Guennec +Koushik Ravikumar +Nissim Dsilva +Giancarlo Rocha +Ryo Miyake +Théo Champion +Kazuki Yamaguchi +Eitan Schwartz +Chris Rutkowski +Juan Alvarez +Aleksandr Yurkovskiy +Anton Borries +Alex Li +Rahul Raj <64.rahulraj@gmail.com> +Maurits van Beusekom diff --git a/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md new file mode 100644 index 000000000000..04641f97dc79 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/CHANGELOG.md @@ -0,0 +1,11 @@ +## 1.2.0 + +* Added `runJavascript` and `runJavascriptReturningResult` interface methods to supersede `evaluateJavascript`. + +## 1.1.0 + +* Add `zoomEnabled` functionality to `WebSettings`. + +## 1.0.0 + +* Extracted platform interface from `webview_flutter`. \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter_platform_interface/LICENSE b/packages/webview_flutter/webview_flutter_platform_interface/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/webview_flutter/webview_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/webview_flutter/webview_flutter_platform_interface/README.md b/packages/webview_flutter/webview_flutter_platform_interface/README.md new file mode 100644 index 000000000000..31e57ab61597 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/README.md @@ -0,0 +1,23 @@ +# webview_flutter_platform_interface + +A common platform interface for the [`webview_flutter`](https://pub.dev/packages/webview_flutter) plugin. + +This interface allows platform-specific implementations of the `webview_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 `webview_flutter`, extend +[`WebviewPlatform`](lib/src/platform_interface/webview_platform.dart) with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`WebviewPlatform` by calling +`WebviewPlatform.setInstance(MyPlatformWebview())`. + +# 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. diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart new file mode 100644 index 000000000000..9610038eec82 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/method_channel/webview_method_channel.dart @@ -0,0 +1,236 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/services.dart'; + +import '../platform_interface/javascript_channel_registry.dart'; +import '../platform_interface/platform_interface.dart'; +import '../types/types.dart'; + +/// A [WebViewPlatformController] that uses a method channel to control the webview. +class MethodChannelWebViewPlatform implements WebViewPlatformController { + /// Constructs an instance that will listen for webviews broadcasting to the + /// given [id], using the given [WebViewPlatformCallbacksHandler]. + MethodChannelWebViewPlatform( + int id, + this._platformCallbacksHandler, + this._javascriptChannelRegistry, + ) : assert(_platformCallbacksHandler != null), + _channel = MethodChannel('plugins.flutter.io/webview_$id') { + _channel.setMethodCallHandler(_onMethodCall); + } + + final JavascriptChannelRegistry _javascriptChannelRegistry; + + final WebViewPlatformCallbacksHandler _platformCallbacksHandler; + + final MethodChannel _channel; + + static const MethodChannel _cookieManagerChannel = + MethodChannel('plugins.flutter.io/cookie_manager'); + + Future _onMethodCall(MethodCall call) async { + switch (call.method) { + case 'javascriptChannelMessage': + final String channel = call.arguments['channel']!; + final String message = call.arguments['message']!; + _javascriptChannelRegistry.onJavascriptChannelMessage(channel, message); + return true; + case 'navigationRequest': + return await _platformCallbacksHandler.onNavigationRequest( + url: call.arguments['url']!, + isForMainFrame: call.arguments['isForMainFrame']!, + ); + case 'onPageFinished': + _platformCallbacksHandler.onPageFinished(call.arguments['url']!); + return null; + case 'onProgress': + _platformCallbacksHandler.onProgress(call.arguments['progress']); + return null; + case 'onPageStarted': + _platformCallbacksHandler.onPageStarted(call.arguments['url']!); + return null; + case 'onWebResourceError': + _platformCallbacksHandler.onWebResourceError( + WebResourceError( + errorCode: call.arguments['errorCode']!, + description: call.arguments['description']!, + // iOS doesn't support `failingUrl`. + failingUrl: call.arguments['failingUrl'], + domain: call.arguments['domain'], + errorType: call.arguments['errorType'] == null + ? null + : WebResourceErrorType.values.firstWhere( + (WebResourceErrorType type) { + return type.toString() == + '$WebResourceErrorType.${call.arguments['errorType']}'; + }, + ), + ), + ); + return null; + } + + throw MissingPluginException( + '${call.method} was invoked but has no handler', + ); + } + + @override + Future loadUrl( + String url, + Map? headers, + ) async { + assert(url != null); + return _channel.invokeMethod('loadUrl', { + 'url': url, + 'headers': headers, + }); + } + + @override + Future currentUrl() => _channel.invokeMethod('currentUrl'); + + @override + Future canGoBack() => + _channel.invokeMethod("canGoBack").then((result) => result!); + + @override + Future canGoForward() => + _channel.invokeMethod("canGoForward").then((result) => result!); + + @override + Future goBack() => _channel.invokeMethod("goBack"); + + @override + Future goForward() => _channel.invokeMethod("goForward"); + + @override + Future reload() => _channel.invokeMethod("reload"); + + @override + Future clearCache() => _channel.invokeMethod("clearCache"); + + @override + Future updateSettings(WebSettings settings) async { + final Map updatesMap = _webSettingsToMap(settings); + if (updatesMap.isNotEmpty) { + await _channel.invokeMethod('updateSettings', updatesMap); + } + } + + @override + Future evaluateJavascript(String javascript) { + return _channel + .invokeMethod('evaluateJavascript', javascript) + .then((result) => result!); + } + + @override + Future runJavascript(String javascript) async { + await _channel.invokeMethod('runJavascript', javascript); + } + + @override + Future runJavascriptReturningResult(String javascript) { + return _channel + .invokeMethod('runJavascriptReturningResult', javascript) + .then((result) => result!); + } + + @override + Future addJavascriptChannels(Set javascriptChannelNames) { + return _channel.invokeMethod( + 'addJavascriptChannels', javascriptChannelNames.toList()); + } + + @override + Future removeJavascriptChannels(Set javascriptChannelNames) { + return _channel.invokeMethod( + 'removeJavascriptChannels', javascriptChannelNames.toList()); + } + + @override + Future getTitle() => _channel.invokeMethod("getTitle"); + + @override + Future scrollTo(int x, int y) { + return _channel.invokeMethod('scrollTo', { + 'x': x, + 'y': y, + }); + } + + @override + Future scrollBy(int x, int y) { + return _channel.invokeMethod('scrollBy', { + 'x': x, + 'y': y, + }); + } + + @override + Future getScrollX() => + _channel.invokeMethod("getScrollX").then((result) => result!); + + @override + Future getScrollY() => + _channel.invokeMethod("getScrollY").then((result) => result!); + + /// Method channel implementation for [WebViewPlatform.clearCookies]. + static Future clearCookies() { + return _cookieManagerChannel + .invokeMethod('clearCookies') + .then((dynamic result) => result!); + } + + static Map _webSettingsToMap(WebSettings? settings) { + final Map map = {}; + void _addIfNonNull(String key, dynamic value) { + if (value == null) { + return; + } + map[key] = value; + } + + void _addSettingIfPresent(String key, WebSetting setting) { + if (!setting.isPresent) { + return; + } + map[key] = setting.value; + } + + _addIfNonNull('jsMode', settings!.javascriptMode?.index); + _addIfNonNull('hasNavigationDelegate', settings.hasNavigationDelegate); + _addIfNonNull('hasProgressTracking', settings.hasProgressTracking); + _addIfNonNull('debuggingEnabled', settings.debuggingEnabled); + _addIfNonNull( + 'gestureNavigationEnabled', settings.gestureNavigationEnabled); + _addIfNonNull( + 'allowsInlineMediaPlayback', settings.allowsInlineMediaPlayback); + _addSettingIfPresent('userAgent', settings.userAgent); + _addIfNonNull('zoomEnabled', settings.zoomEnabled); + return map; + } + + /// Converts a [CreationParams] object to a map as expected by `platform_views` channel. + /// + /// This is used for the `creationParams` argument of the platform views created by + /// [AndroidWebViewBuilder] and [CupertinoWebViewBuilder]. + static Map creationParamsToMap( + CreationParams creationParams, { + bool usesHybridComposition = false, + }) { + return { + 'initialUrl': creationParams.initialUrl, + 'settings': _webSettingsToMap(creationParams.webSettings), + 'javascriptChannelNames': creationParams.javascriptChannelNames.toList(), + 'userAgent': creationParams.userAgent, + 'autoMediaPlaybackPolicy': creationParams.autoMediaPlaybackPolicy.index, + 'usesHybridComposition': usesHybridComposition, + }; + } +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/javascript_channel_registry.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/javascript_channel_registry.dart new file mode 100644 index 000000000000..142d8eb00950 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/javascript_channel_registry.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 '../types/javascript_channel.dart'; +import '../types/javascript_message.dart'; + +/// Utility class for managing named JavaScript channels and forwarding incoming +/// messages on the correct channel. +class JavascriptChannelRegistry { + /// Constructs a [JavascriptChannelRegistry] initializing it with the given + /// set of [JavascriptChannel]s. + JavascriptChannelRegistry(Set? channels) { + updateJavascriptChannelsFromSet(channels); + } + + /// Maps a channel name to a channel. + final Map channels = {}; + + /// Invoked when a JavaScript channel message is received. + void onJavascriptChannelMessage(String channel, String message) { + final JavascriptChannel? javascriptChannel = channels[channel]; + + if (javascriptChannel == null) { + throw ArgumentError('No channel registered with name $channel.'); + } + + javascriptChannel.onMessageReceived(JavascriptMessage(message)); + } + + /// Updates the set of [JavascriptChannel]s with the new set. + void updateJavascriptChannelsFromSet(Set? channels) { + this.channels.clear(); + if (channels == null) { + return; + } + + for (final JavascriptChannel channel in channels) { + this.channels[channel.name] = channel; + } + } +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/platform_interface.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/platform_interface.dart new file mode 100644 index 000000000000..43f967fb13b0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/platform_interface.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. + +export 'javascript_channel_registry.dart'; +export 'webview_platform.dart'; +export 'webview_platform_callbacks_handler.dart'; +export 'webview_platform_controller.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform.dart new file mode 100644 index 000000000000..4732f54d6456 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform.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/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; +import 'package:webview_flutter_platform_interface/src/platform_interface/javascript_channel_registry.dart'; + +import '../types/types.dart'; +import 'webview_platform_callbacks_handler.dart'; +import 'webview_platform_controller.dart'; + +/// Signature for callbacks reporting that a [WebViewPlatformController] was created. +/// +/// See also the `onWebViewPlatformCreated` argument for [WebViewPlatform.build]. +typedef WebViewPlatformCreatedCallback = void Function( + WebViewPlatformController? webViewPlatformController); + +/// Interface for a platform implementation of a WebView. +/// +/// [WebView.platform] controls the builder that is used by [WebView]. +/// [AndroidWebViewPlatform] and [CupertinoWebViewPlatform] are the default implementations +/// for Android and iOS respectively. +abstract class WebViewPlatform { + /// Builds a new WebView. + /// + /// Returns a Widget tree that embeds the created webview. + /// + /// `creationParams` are the initial parameters used to setup the webview. + /// + /// `webViewPlatformHandler` will be used for handling callbacks that are made by the created + /// [WebViewPlatformController]. + /// + /// `onWebViewPlatformCreated` will be invoked after the platform specific [WebViewPlatformController] + /// implementation is created with the [WebViewPlatformController] instance as a parameter. + /// + /// `gestureRecognizers` specifies which gestures should be consumed by the web view. + /// It is possible for other gesture recognizers to be competing with the web view on pointer + /// events, e.g if the web view is inside a [ListView] the [ListView] will want to handle + /// vertical drags. The web view will claim gestures that are recognized by any of the + /// recognizers on this list. + /// When `gestureRecognizers` is empty or null, the web view will only handle pointer events for gestures that + /// were not claimed by any other gesture recognizer. + /// + /// `webViewPlatformHandler` must not be null. + Widget build({ + required BuildContext context, + // TODO(amirh): convert this to be the actual parameters. + // I'm starting without it as the PR is starting to become pretty big. + // I'll followup with the conversion PR. + required CreationParams creationParams, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + required JavascriptChannelRegistry javascriptChannelRegistry, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, + }); + + /// Clears all cookies for all [WebView] instances. + /// + /// Returns true if cookies were present before clearing, else false. + Future clearCookies() { + throw UnimplementedError( + "WebView clearCookies is not implemented on the current platform"); + } +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_callbacks_handler.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_callbacks_handler.dart new file mode 100644 index 000000000000..44dae2ece434 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_callbacks_handler.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 'dart:async'; + +import '../types/types.dart'; + +/// Interface for callbacks made by [WebViewPlatformController]. +/// +/// The webview plugin implements this class, and passes an instance to the [WebViewPlatformController]. +/// [WebViewPlatformController] is notifying this handler on events that happened on the platform's webview. +abstract class WebViewPlatformCallbacksHandler { + /// Invoked by [WebViewPlatformController] when a navigation request is pending. + /// + /// If true is returned the navigation is allowed, otherwise it is blocked. + FutureOr onNavigationRequest( + {required String url, required bool isForMainFrame}); + + /// Invoked by [WebViewPlatformController] when a page has started loading. + void onPageStarted(String url); + + /// Invoked by [WebViewPlatformController] when a page has finished loading. + void onPageFinished(String url); + + /// Invoked by [WebViewPlatformController] when a page is loading. + /// /// Only works when [WebSettings.hasProgressTracking] is set to `true`. + void onProgress(int progress); + + /// Report web resource loading error to the host application. + void onWebResourceError(WebResourceError error); +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.dart new file mode 100644 index 000000000000..b42da4326079 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/platform_interface/webview_platform_controller.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 '../types/types.dart'; +import 'webview_platform_callbacks_handler.dart'; + +/// Interface for talking to the webview's platform implementation. +/// +/// An instance implementing this interface is passed to the `onWebViewPlatformCreated` callback that is +/// passed to [WebViewPlatformBuilder#onWebViewPlatformCreated]. +/// +/// Platform implementations that live in a separate package should extend this class rather than +/// implement it as webview_flutter does not consider newly added methods to be breaking changes. +/// Extending this class (using `extends`) ensures that the subclass will get the default +/// implementation, while platform implementations that `implements` this interface will be broken +/// by newly added [WebViewPlatformController] methods. +abstract class WebViewPlatformController { + /// Creates a new WebViewPlatform. + /// + /// Callbacks made by the WebView will be delegated to `handler`. + /// + /// The `handler` parameter must not be null. + WebViewPlatformController(WebViewPlatformCallbacksHandler handler); + + /// Loads the specified URL. + /// + /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will + /// be added as key value pairs of HTTP headers for the request. + /// + /// `url` must not be null. + /// + /// Throws an ArgumentError if `url` is not a valid URL string. + Future loadUrl( + String url, + Map? headers, + ) { + throw UnimplementedError( + "WebView loadUrl is not implemented on the current platform"); + } + + /// Updates the webview settings. + /// + /// Any non null field in `settings` will be set as the new setting value. + /// All null fields in `settings` are ignored. + Future updateSettings(WebSettings setting) { + throw UnimplementedError( + "WebView updateSettings is not implemented on the current platform"); + } + + /// Accessor to the current URL that the WebView is displaying. + /// + /// If no URL was ever loaded, returns `null`. + Future currentUrl() { + throw UnimplementedError( + "WebView currentUrl is not implemented on the current platform"); + } + + /// Checks whether there's a back history item. + Future canGoBack() { + throw UnimplementedError( + "WebView canGoBack is not implemented on the current platform"); + } + + /// Checks whether there's a forward history item. + Future canGoForward() { + throw UnimplementedError( + "WebView canGoForward is not implemented on the current platform"); + } + + /// Goes back in the history of this WebView. + /// + /// If there is no back history item this is a no-op. + Future goBack() { + throw UnimplementedError( + "WebView goBack is not implemented on the current platform"); + } + + /// Goes forward in the history of this WebView. + /// + /// If there is no forward history item this is a no-op. + Future goForward() { + throw UnimplementedError( + "WebView goForward is not implemented on the current platform"); + } + + /// Reloads the current URL. + Future reload() { + throw UnimplementedError( + "WebView reload is not implemented on the current platform"); + } + + /// Clears all caches used by the [WebView]. + /// + /// The following caches are cleared: + /// 1. Browser HTTP Cache. + /// 2. [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api) caches. + /// These are not yet supported in iOS WkWebView. Service workers tend to use this cache. + /// 3. Application cache. + /// 4. Local Storage. + Future clearCache() { + throw UnimplementedError( + "WebView clearCache is not implemented on the current platform"); + } + + /// Evaluates a JavaScript expression in the context of the current page. + /// + /// The Future completes with an error if a JavaScript error occurred, or if the type of the + /// evaluated expression is not supported (e.g on iOS not all non-primitive types can be evaluated). + Future evaluateJavascript(String javascript) { + throw UnimplementedError( + "WebView evaluateJavascript is not implemented on the current platform"); + } + + /// Runs the given JavaScript in the context of the current page. + /// + /// The Future completes with an error if a JavaScript error occurred. + Future runJavascript(String javascript) { + throw UnimplementedError( + "WebView runJavascript is not implemented on the current platform"); + } + + /// Runs the given JavaScript in the context of the current page, and returns the result. + /// + /// The Future completes with an error if a JavaScript error occurred, or if the + /// type the given expression evaluates to is unsupported. Unsupported values include + /// certain non-primitive types on iOS, as well as `undefined` or `null` on iOS 14+. + Future runJavascriptReturningResult(String javascript) { + throw UnimplementedError( + "WebView runJavascriptReturningResult is not implemented on the current platform"); + } + + /// Adds new JavaScript channels to the set of enabled channels. + /// + /// For each value in this list the platform's webview should make sure that a corresponding + /// property with a postMessage method is set on `window`. For example for a JavaScript channel + /// named `Foo` it should be possible for JavaScript code executing in the webview to do + /// + /// ```javascript + /// Foo.postMessage('hello'); + /// ``` + /// + /// See also: [CreationParams.javascriptChannelNames]. + Future addJavascriptChannels(Set javascriptChannelNames) { + throw UnimplementedError( + "WebView addJavascriptChannels is not implemented on the current platform"); + } + + /// Removes JavaScript channel names from the set of enabled channels. + /// + /// This disables channels that were previously enabled by [addJavascriptChannels] or through + /// [CreationParams.javascriptChannelNames]. + Future removeJavascriptChannels(Set javascriptChannelNames) { + throw UnimplementedError( + "WebView removeJavascriptChannels is not implemented on the current platform"); + } + + /// Returns the title of the currently loaded page. + Future getTitle() { + throw UnimplementedError( + "WebView getTitle is not implemented on the current platform"); + } + + /// Set the scrolled position of this view. + /// + /// The parameters `x` and `y` specify the position to scroll to in WebView pixels. + Future scrollTo(int x, int y) { + throw UnimplementedError( + "WebView scrollTo is not implemented on the current platform"); + } + + /// Move the scrolled position of this view. + /// + /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by. + Future scrollBy(int x, int y) { + throw UnimplementedError( + "WebView scrollBy is not implemented on the current platform"); + } + + /// Return the horizontal scroll position of this view. + /// + /// Scroll position is measured from left. + Future getScrollX() { + throw UnimplementedError( + "WebView getScrollX is not implemented on the current platform"); + } + + /// Return the vertical scroll position of this view. + /// + /// Scroll position is measured from top. + Future getScrollY() { + throw UnimplementedError( + "WebView getScrollY is not implemented on the current platform"); + } +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/auto_media_playback_policy.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/auto_media_playback_policy.dart new file mode 100644 index 000000000000..7d6927ac7957 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/auto_media_playback_policy.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. + +/// Specifies possible restrictions on automatic media playback. +/// +/// This is typically used in [WebView.initialMediaPlaybackPolicy]. +// The method channel implementation is marshalling this enum to the value's index, so the order +// is important. +enum AutoMediaPlaybackPolicy { + /// Starting any kind of media playback requires a user action. + /// + /// For example: JavaScript code cannot start playing media unless the code was executed + /// as a result of a user action (like a touch event). + require_user_action_for_all_media_types, + + /// Starting any kind of media playback is always allowed. + /// + /// For example: JavaScript code that's triggered when the page is loaded can start playing + /// video or audio. + always_allow, +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/creation_params.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/creation_params.dart new file mode 100644 index 000000000000..f213e976ad84 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/creation_params.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 'auto_media_playback_policy.dart'; +import 'web_settings.dart'; + +/// Configuration to use when creating a new [WebViewPlatformController]. +/// +/// The `autoMediaPlaybackPolicy` parameter must not be null. +class CreationParams { + /// Constructs an instance to use when creating a new + /// [WebViewPlatformController]. + /// + /// The `autoMediaPlaybackPolicy` parameter must not be null. + CreationParams({ + this.initialUrl, + this.webSettings, + this.javascriptChannelNames = const {}, + this.userAgent, + this.autoMediaPlaybackPolicy = + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + }) : assert(autoMediaPlaybackPolicy != null); + + /// The initialUrl to load in the webview. + /// + /// When null the webview will be created without loading any page. + final String? initialUrl; + + /// The initial [WebSettings] for the new webview. + /// + /// This can later be updated with [WebViewPlatformController.updateSettings]. + final WebSettings? webSettings; + + /// The initial set of JavaScript channels that are configured for this webview. + /// + /// For each value in this set the platform's webview should make sure that a corresponding + /// property with a postMessage method is set on `window`. For example for a JavaScript channel + /// named `Foo` it should be possible for JavaScript code executing in the webview to do + /// + /// ```javascript + /// Foo.postMessage('hello'); + /// ``` + // TODO(amirh): describe what should happen when postMessage is called once that code is migrated + // to PlatformWebView. + final Set javascriptChannelNames; + + /// The value used for the HTTP User-Agent: request header. + /// + /// When null the platform's webview default is used for the User-Agent header. + final String? userAgent; + + /// Which restrictions apply on automatic media playback. + final AutoMediaPlaybackPolicy autoMediaPlaybackPolicy; + + @override + String toString() { + return '$runtimeType(initialUrl: $initialUrl, settings: $webSettings, javascriptChannelNames: $javascriptChannelNames, UserAgent: $userAgent)'; + } +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_channel.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_channel.dart new file mode 100644 index 000000000000..f32a41893eb5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_channel.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 'javascript_message.dart'; + +/// Callback type for handling messages sent from JavaScript running in a web view. +typedef void JavascriptMessageHandler(JavascriptMessage message); + +final RegExp _validChannelNames = RegExp('^[a-zA-Z_][a-zA-Z0-9_]*\$'); + +/// A named channel for receiving messaged from JavaScript code running inside a web view. +class JavascriptChannel { + /// Constructs a JavaScript channel. + /// + /// The parameters `name` and `onMessageReceived` must not be null. + JavascriptChannel({ + required this.name, + required this.onMessageReceived, + }) : assert(name != null), + assert(onMessageReceived != null), + assert(_validChannelNames.hasMatch(name)); + + /// The channel's name. + /// + /// Passing this channel object as part of a [WebView.javascriptChannels] adds a channel object to + /// the JavaScript window object's property named `name`. + /// + /// The name must start with a letter or underscore(_), followed by any combination of those + /// characters plus digits. + /// + /// Note that any JavaScript existing `window` property with this name will be overriden. + /// + /// See also [WebView.javascriptChannels] for more details on the channel registration mechanism. + final String name; + + /// A callback that's invoked when a message is received through the channel. + final JavascriptMessageHandler onMessageReceived; +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_message.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_message.dart new file mode 100644 index 000000000000..8d080452c54a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_message.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. + +/// A message that was sent by JavaScript code running in a [WebView]. +class JavascriptMessage { + /// Constructs a JavaScript message object. + /// + /// The `message` parameter must not be null. + const JavascriptMessage(this.message) : assert(message != null); + + /// The contents of the message that was sent by the JavaScript code. + final String message; +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_mode.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_mode.dart new file mode 100644 index 000000000000..53d049175907 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/javascript_mode.dart @@ -0,0 +1,12 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// Describes the state of JavaScript support in a given web view. +enum JavascriptMode { + /// JavaScript execution is disabled. + disabled, + + /// JavaScript execution is not restricted. + unrestricted, +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart new file mode 100644 index 000000000000..b1a9b9b9daa8 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/types.dart @@ -0,0 +1,12 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'auto_media_playback_policy.dart'; +export 'creation_params.dart'; +export 'javascript_channel.dart'; +export 'javascript_message.dart'; +export 'javascript_mode.dart'; +export 'web_resource_error.dart'; +export 'web_resource_error_type.dart'; +export 'web_settings.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error.dart new file mode 100644 index 000000000000..b61671f0ac45 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error.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 'web_resource_error_type.dart'; + +/// Error returned in `WebView.onWebResourceError` when a web resource loading error has occurred. +class WebResourceError { + /// Creates a new [WebResourceError] + /// + /// A user should not need to instantiate this class, but will receive one in + /// [WebResourceErrorCallback]. + WebResourceError({ + required this.errorCode, + required this.description, + this.domain, + this.errorType, + this.failingUrl, + }) : assert(errorCode != null), + assert(description != null); + + /// Raw code of the error from the respective platform. + /// + /// On Android, the error code will be a constant from a + /// [WebViewClient](https://developer.android.com/reference/android/webkit/WebViewClient#summary) and + /// will have a corresponding [errorType]. + /// + /// On iOS, the error code will be a constant from `NSError.code` in + /// Objective-C. See + /// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html + /// for more information on error handling on iOS. Some possible error codes + /// can be found at https://developer.apple.com/documentation/webkit/wkerrorcode?language=objc. + final int errorCode; + + /// The domain of where to find the error code. + /// + /// This field is only available on iOS and represents a "domain" from where + /// the [errorCode] is from. This value is taken directly from an `NSError` + /// in Objective-C. See + /// https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ErrorHandlingCocoa/ErrorObjectsDomains/ErrorObjectsDomains.html + /// for more information on error handling on iOS. + final String? domain; + + /// Description of the error that can be used to communicate the problem to the user. + final String description; + + /// The type this error can be categorized as. + /// + /// This will never be `null` on Android, but can be `null` on iOS. + final WebResourceErrorType? errorType; + + /// Gets the URL for which the resource request was made. + /// + /// This value is not provided on iOS. Alternatively, you can keep track of + /// the last values provided to [WebViewPlatformController.loadUrl]. + final String? failingUrl; +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error_type.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error_type.dart new file mode 100644 index 000000000000..a45816df8323 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_resource_error_type.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. + +/// Possible error type categorizations used by [WebResourceError]. +enum WebResourceErrorType { + /// User authentication failed on server. + authentication, + + /// Malformed URL. + badUrl, + + /// Failed to connect to the server. + connect, + + /// Failed to perform SSL handshake. + failedSslHandshake, + + /// Generic file error. + file, + + /// File not found. + fileNotFound, + + /// Server or proxy hostname lookup failed. + hostLookup, + + /// Failed to read or write to the server. + io, + + /// User authentication failed on proxy. + proxyAuthentication, + + /// Too many redirects. + redirectLoop, + + /// Connection timed out. + timeout, + + /// Too many requests during this load. + tooManyRequests, + + /// Generic error. + unknown, + + /// Resource load was canceled by Safe Browsing. + unsafeResource, + + /// Unsupported authentication scheme (not basic or digest). + unsupportedAuthScheme, + + /// Unsupported URI scheme. + unsupportedScheme, + + /// The web content process was terminated. + webContentProcessTerminated, + + /// The web view was invalidated. + webViewInvalidated, + + /// A JavaScript exception occurred. + javaScriptExceptionOccurred, + + /// The result of JavaScript execution could not be returned. + javaScriptResultTypeIsUnsupported, +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_settings.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_settings.dart new file mode 100644 index 000000000000..3d94153c886e --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/src/types/web_settings.dart @@ -0,0 +1,127 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/widgets.dart'; + +import 'javascript_mode.dart'; + +/// A single setting for configuring a WebViewPlatform which may be absent. +class WebSetting { + /// Constructs an absent setting instance. + /// + /// The [isPresent] field for the instance will be false. + /// + /// Accessing [value] for an absent instance will throw. + WebSetting.absent() + : _value = null, + isPresent = false; + + /// Constructs a setting of the given `value`. + /// + /// The [isPresent] field for the instance will be true. + WebSetting.of(T value) + : _value = value, + isPresent = true; + + final T? _value; + + /// The setting's value. + /// + /// Throws if [WebSetting.isPresent] is false. + T get value { + if (!isPresent) { + throw StateError('Cannot access a value of an absent WebSetting'); + } + assert(isPresent); + // The intention of this getter is to return T whether it is nullable or + // not whereas _value is of type T? since _value can be null even when + // T is not nullable (when isPresent == false). + // + // We promote _value to T using `as T` instead of `!` operator to handle + // the case when _value is legitimately null (and T is a nullable type). + // `!` operator would always throw if _value is null. + return _value as T; + } + + /// True when this web setting instance contains a value. + /// + /// When false the [WebSetting.value] getter throws. + final bool isPresent; + + @override + bool operator ==(Object other) { + if (other.runtimeType != runtimeType) return false; + final WebSetting typedOther = other as WebSetting; + return typedOther.isPresent == isPresent && typedOther._value == _value; + } + + @override + int get hashCode => hashValues(_value, isPresent); +} + +/// Settings for configuring a WebViewPlatform. +/// +/// Initial settings are passed as part of [CreationParams], settings updates are sent with +/// [WebViewPlatform#updateSettings]. +/// +/// The `userAgent` parameter must not be null. +class WebSettings { + /// Construct an instance with initial settings. Future setting changes can be + /// sent with [WebviewPlatform#updateSettings]. + /// + /// The `userAgent` parameter must not be null. + WebSettings({ + this.javascriptMode, + this.hasNavigationDelegate, + this.hasProgressTracking, + this.debuggingEnabled, + this.gestureNavigationEnabled, + this.allowsInlineMediaPlayback, + this.zoomEnabled, + required this.userAgent, + }) : assert(userAgent != null); + + /// The JavaScript execution mode to be used by the webview. + final JavascriptMode? javascriptMode; + + /// Whether the [WebView] has a [NavigationDelegate] set. + final bool? hasNavigationDelegate; + + /// Whether the [WebView] should track page loading progress. + /// See also: [WebViewPlatformCallbacksHandler.onProgress] to get the progress. + final bool? hasProgressTracking; + + /// Whether to enable the platform's webview content debugging tools. + /// + /// See also: [WebView.debuggingEnabled]. + final bool? debuggingEnabled; + + /// Whether to play HTML5 videos inline or use the native full-screen controller on iOS. + /// + /// This will have no effect on Android. + final bool? allowsInlineMediaPlayback; + + /// The value used for the HTTP `User-Agent:` request header. + /// + /// If [userAgent.value] is null the platform's default user agent should be used. + /// + /// An absent value ([userAgent.isPresent] is false) represents no change to this setting from the + /// last time it was set. + /// + /// See also [WebView.userAgent]. + final WebSetting userAgent; + + /// Sets whether the WebView should support zooming using its on-screen zoom controls and gestures. + final bool? zoomEnabled; + + /// Whether to allow swipe based navigation in iOS. + /// + /// See also: [WebView.gestureNavigationEnabled] + final bool? gestureNavigationEnabled; + + @override + String toString() { + return 'WebSettings(javascriptMode: $javascriptMode, hasNavigationDelegate: $hasNavigationDelegate, hasProgressTracking: $hasProgressTracking, debuggingEnabled: $debuggingEnabled, gestureNavigationEnabled: $gestureNavigationEnabled, userAgent: $userAgent, allowsInlineMediaPlayback: $allowsInlineMediaPlayback)'; + } +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/lib/webview_flutter_platform_interface.dart b/packages/webview_flutter/webview_flutter_platform_interface/lib/webview_flutter_platform_interface.dart new file mode 100644 index 000000000000..b508989ed978 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/lib/webview_flutter_platform_interface.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/platform_interface/platform_interface.dart'; +export 'src/types/types.dart'; +export 'src/method_channel/webview_method_channel.dart'; diff --git a/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml new file mode 100644 index 000000000000..994c3dcdebdf --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/pubspec.yaml @@ -0,0 +1,22 @@ +name: webview_flutter_platform_interface +description: A common platform interface for the webview_flutter plugin. +repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_platform_interface +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview_flutter%22 +# NOTE: We strongly prefer non-breaking changes, even at the expense of a +# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes +version: 1.2.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 \ No newline at end of file diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart new file mode 100644 index 000000000000..85f184f9f715 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/method_channel/webview_method_channel_test.dart @@ -0,0 +1,493 @@ +// Copyright 2013 The Flutter Authors. All rights 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:mockito/mockito.dart'; +import 'package:webview_flutter_platform_interface/src/method_channel/webview_method_channel.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Tests on `plugin.flutter.io/webview_` channel', () { + const int channelId = 1; + const MethodChannel channel = + MethodChannel('plugins.flutter.io/webview_$channelId'); + final WebViewPlatformCallbacksHandler callbacksHandler = + MockWebViewPlatformCallbacksHandler(); + final JavascriptChannelRegistry javascriptChannelRegistry = + MockJavascriptChannelRegistry(); + + final List log = []; + channel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + + switch (methodCall.method) { + case 'currentUrl': + return 'https://test.url'; + case 'canGoBack': + case 'canGoForward': + return true; + case 'runJavascriptReturningResult': + case 'evaluateJavascript': + return methodCall.arguments as String; + case 'getScrollX': + return 10; + case 'getScrollY': + return 20; + } + + // Return null explicitly instead of relying on the implicit null + // returned by the method channel if no return statement is specified. + return null; + }); + + final MethodChannelWebViewPlatform webViewPlatform = + MethodChannelWebViewPlatform( + channelId, + callbacksHandler, + javascriptChannelRegistry, + ); + + tearDown(() { + log.clear(); + }); + + test('loadUrl with headers', () async { + await webViewPlatform.loadUrl( + 'https://test.url', + const { + 'Content-Type': 'text/plain', + 'Accept': 'text/html', + }, + ); + + expect( + log, + [ + isMethodCall( + 'loadUrl', + arguments: { + 'url': 'https://test.url', + 'headers': { + 'Content-Type': 'text/plain', + 'Accept': 'text/html', + }, + }, + ), + ], + ); + }); + + test('loadUrl without headers', () async { + await webViewPlatform.loadUrl( + 'https://test.url', + null, + ); + + expect( + log, + [ + isMethodCall( + 'loadUrl', + arguments: { + 'url': 'https://test.url', + 'headers': null, + }, + ), + ], + ); + }); + + test('currentUrl', () async { + final String? currentUrl = await webViewPlatform.currentUrl(); + + expect(currentUrl, 'https://test.url'); + expect( + log, + [ + isMethodCall( + 'currentUrl', + arguments: null, + ), + ], + ); + }); + + test('canGoBack', () async { + final bool canGoBack = await webViewPlatform.canGoBack(); + + expect(canGoBack, true); + expect( + log, + [ + isMethodCall( + 'canGoBack', + arguments: null, + ), + ], + ); + }); + + test('canGoForward', () async { + final bool canGoForward = await webViewPlatform.canGoForward(); + + expect(canGoForward, true); + expect( + log, + [ + isMethodCall( + 'canGoForward', + arguments: null, + ), + ], + ); + }); + + test('goBack', () async { + await webViewPlatform.goBack(); + + expect( + log, + [ + isMethodCall( + 'goBack', + arguments: null, + ), + ], + ); + }); + + test('goForward', () async { + await webViewPlatform.goForward(); + + expect( + log, + [ + isMethodCall( + 'goForward', + arguments: null, + ), + ], + ); + }); + + test('reload', () async { + await webViewPlatform.reload(); + + expect( + log, + [ + isMethodCall( + 'reload', + arguments: null, + ), + ], + ); + }); + + test('clearCache', () async { + await webViewPlatform.clearCache(); + + expect( + log, + [ + isMethodCall( + 'clearCache', + arguments: null, + ), + ], + ); + }); + + test('updateSettings', () async { + final WebSettings settings = + WebSettings(userAgent: WebSetting.of('Dart Test')); + await webViewPlatform.updateSettings(settings); + + expect( + log, + [ + isMethodCall( + 'updateSettings', + arguments: { + 'userAgent': 'Dart Test', + }, + ), + ], + ); + }); + + test('updateSettings all parameters', () async { + final WebSettings settings = WebSettings( + userAgent: WebSetting.of('Dart Test'), + javascriptMode: JavascriptMode.disabled, + hasNavigationDelegate: true, + hasProgressTracking: true, + debuggingEnabled: true, + gestureNavigationEnabled: true, + allowsInlineMediaPlayback: true, + zoomEnabled: false, + ); + await webViewPlatform.updateSettings(settings); + + expect( + log, + [ + isMethodCall( + 'updateSettings', + arguments: { + 'userAgent': 'Dart Test', + 'jsMode': 0, + 'hasNavigationDelegate': true, + 'hasProgressTracking': true, + 'debuggingEnabled': true, + 'gestureNavigationEnabled': true, + 'allowsInlineMediaPlayback': true, + 'zoomEnabled': false, + }, + ), + ], + ); + }); + + test('updateSettings without settings', () async { + final WebSettings settings = + WebSettings(userAgent: WebSetting.absent()); + await webViewPlatform.updateSettings(settings); + + expect( + log.isEmpty, + true, + ); + }); + + test('evaluateJavascript', () async { + final String evaluateJavascript = + await webViewPlatform.evaluateJavascript( + 'This simulates some JavaScript code.', + ); + + expect('This simulates some JavaScript code.', evaluateJavascript); + expect( + log, + [ + isMethodCall( + 'evaluateJavascript', + arguments: 'This simulates some JavaScript code.', + ), + ], + ); + }); + + test('runJavascript', () async { + await webViewPlatform.runJavascript( + 'This simulates some JavaScript code.', + ); + + expect( + log, + [ + isMethodCall( + 'runJavascript', + arguments: 'This simulates some JavaScript code.', + ), + ], + ); + }); + + test('runJavascriptReturningResult', () async { + final String evaluateJavascript = + await webViewPlatform.runJavascriptReturningResult( + 'This simulates some JavaScript code.', + ); + + expect('This simulates some JavaScript code.', evaluateJavascript); + expect( + log, + [ + isMethodCall( + 'runJavascriptReturningResult', + arguments: 'This simulates some JavaScript code.', + ), + ], + ); + }); + + test('addJavascriptChannels', () async { + final Set channels = {'channel one', 'channel two'}; + await webViewPlatform.addJavascriptChannels(channels); + + expect(log, [ + isMethodCall( + 'addJavascriptChannels', + arguments: [ + 'channel one', + 'channel two', + ], + ), + ]); + }); + + test('addJavascriptChannels without channels', () async { + final Set channels = {}; + await webViewPlatform.addJavascriptChannels(channels); + + expect(log, [ + isMethodCall( + 'addJavascriptChannels', + arguments: [], + ), + ]); + }); + + test('removeJavascriptChannels', () async { + final Set channels = {'channel one', 'channel two'}; + await webViewPlatform.removeJavascriptChannels(channels); + + expect(log, [ + isMethodCall( + 'removeJavascriptChannels', + arguments: [ + 'channel one', + 'channel two', + ], + ), + ]); + }); + + test('removeJavascriptChannels without channels', () async { + final Set channels = {}; + await webViewPlatform.removeJavascriptChannels(channels); + + expect(log, [ + isMethodCall( + 'removeJavascriptChannels', + arguments: [], + ), + ]); + }); + + test('getTitle', () async { + final String? title = await webViewPlatform.getTitle(); + + expect(title, null); + expect( + log, + [ + isMethodCall('getTitle', arguments: null), + ], + ); + }); + + test('scrollTo', () async { + await webViewPlatform.scrollTo(10, 20); + + expect( + log, + [ + isMethodCall( + 'scrollTo', + arguments: { + 'x': 10, + 'y': 20, + }, + ), + ], + ); + }); + + test('scrollBy', () async { + await webViewPlatform.scrollBy(10, 20); + + expect( + log, + [ + isMethodCall( + 'scrollBy', + arguments: { + 'x': 10, + 'y': 20, + }, + ), + ], + ); + }); + + test('getScrollX', () async { + final int x = await webViewPlatform.getScrollX(); + + expect(x, 10); + expect( + log, + [ + isMethodCall( + 'getScrollX', + arguments: null, + ), + ], + ); + }); + + test('getScrollY', () async { + final int y = await webViewPlatform.getScrollY(); + + expect(y, 20); + expect( + log, + [ + isMethodCall( + 'getScrollY', + arguments: null, + ), + ], + ); + }); + }); + + group('Tests on `plugins.flutter.io/cookie_manager` channel', () { + const MethodChannel cookieChannel = + MethodChannel('plugins.flutter.io/cookie_manager'); + + final List log = []; + cookieChannel.setMockMethodCallHandler((MethodCall methodCall) async { + log.add(methodCall); + + if (methodCall.method == 'clearCookies') { + return true; + } + + // Return null explicitly instead of relying on the implicit null + // returned by the method channel if no return statement is specified. + return null; + }); + + tearDown(() { + log.clear(); + }); + + test('clearCookies', () async { + final bool clearCookies = + await MethodChannelWebViewPlatform.clearCookies(); + + expect(clearCookies, true); + expect( + log, + [ + isMethodCall( + 'clearCookies', + arguments: null, + ), + ], + ); + }); + }); +} + +class MockWebViewPlatformCallbacksHandler extends Mock + implements WebViewPlatformCallbacksHandler {} + +class MockJavascriptChannelRegistry extends Mock + implements JavascriptChannelRegistry {} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/javascript_channel_registry_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/javascript_channel_registry_test.dart new file mode 100644 index 000000000000..55d0e1e13bd1 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/platform_interface/javascript_channel_registry_test.dart @@ -0,0 +1,119 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter_platform_interface/src/types/javascript_channel.dart'; +import 'package:webview_flutter_platform_interface/src/types/types.dart'; +import 'package:webview_flutter_platform_interface/src/platform_interface/javascript_channel_registry.dart'; + +void main() { + final Map _log = {}; + final Set _channels = { + JavascriptChannel( + name: 'js_channel_1', + onMessageReceived: (JavascriptMessage message) => + _log['js_channel_1'] = message.message, + ), + JavascriptChannel( + name: 'js_channel_2', + onMessageReceived: (JavascriptMessage message) => + _log['js_channel_2'] = message.message, + ), + JavascriptChannel( + name: 'js_channel_3', + onMessageReceived: (JavascriptMessage message) => + _log['js_channel_3'] = message.message, + ), + }; + + tearDown(() { + _log.clear(); + }); + + test('ctor should initialize with channels.', () { + final JavascriptChannelRegistry registry = + JavascriptChannelRegistry(_channels); + + expect(registry.channels.length, 3); + for (final JavascriptChannel channel in _channels) { + expect(registry.channels[channel.name], channel); + } + }); + + test('onJavascriptChannelMessage should forward message on correct channel.', + () { + final JavascriptChannelRegistry registry = + JavascriptChannelRegistry(_channels); + + registry.onJavascriptChannelMessage( + 'js_channel_2', + 'test message on channel 2', + ); + + expect( + _log, + containsPair( + 'js_channel_2', + 'test message on channel 2', + )); + }); + + test( + 'onJavascriptChannelMessage should throw ArgumentError when message arrives on non-existing channel.', + () { + final JavascriptChannelRegistry registry = + JavascriptChannelRegistry(_channels); + + expect( + () => registry.onJavascriptChannelMessage( + 'js_channel_4', + 'test message on channel 2', + ), + throwsA( + isA().having((ArgumentError error) => error.message, + 'message', 'No channel registered with name js_channel_4.'), + )); + }); + + test( + 'updateJavascriptChannelsFromSet should clear all channels when null is supplied.', + () { + final JavascriptChannelRegistry registry = + JavascriptChannelRegistry(_channels); + + expect(registry.channels.length, 3); + + registry.updateJavascriptChannelsFromSet(null); + + expect(registry.channels, isEmpty); + }); + + test('updateJavascriptChannelsFromSet should update registry with new set.', + () { + final JavascriptChannelRegistry registry = + JavascriptChannelRegistry(_channels); + + expect(registry.channels.length, 3); + + final Set newChannels = { + JavascriptChannel( + name: 'new_js_channel_1', + onMessageReceived: (JavascriptMessage message) => + _log['new_js_channel_1'] = message.message, + ), + JavascriptChannel( + name: 'new_js_channel_2', + onMessageReceived: (JavascriptMessage message) => + _log['new_js_channel_2'] = message.message, + ), + }; + + registry.updateJavascriptChannelsFromSet(newChannels); + + expect(registry.channels.length, 2); + for (final JavascriptChannel channel in newChannels) { + expect(registry.channels[channel.name], channel); + } + }); +} diff --git a/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/javascript_channel_test.dart b/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/javascript_channel_test.dart new file mode 100644 index 000000000000..f481edda1edd --- /dev/null +++ b/packages/webview_flutter/webview_flutter_platform_interface/test/src/types/javascript_channel_test.dart @@ -0,0 +1,48 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; +import 'package:webview_flutter_platform_interface/src/types/javascript_channel.dart'; + +void main() { + final List _validChars = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_'.split(''); + final List _commonInvalidChars = + r'`~!@#$%^&*()-=+[]{}\|"' ':;/?<>,. '.split(''); + final List _digits = List.generate(10, (int index) => index++); + + test( + 'ctor should create JavascriptChannel when name starts with a valid character followed by a number.', + () { + for (final String char in _validChars) { + for (final int digit in _digits) { + final JavascriptChannel channel = + JavascriptChannel(name: '$char$digit', onMessageReceived: (_) {}); + + expect(channel.name, '$char$digit'); + } + } + }); + + test('ctor should assert when channel name starts with a number.', () { + for (final int i in _digits) { + expect( + () => JavascriptChannel(name: '$i', onMessageReceived: (_) {}), + throwsAssertionError, + ); + } + }); + + test('ctor should assert when channel contains invalid char.', () { + for (final String validChar in _validChars) { + for (final String invalidChar in _commonInvalidChars) { + expect( + () => JavascriptChannel( + name: validChar + invalidChar, onMessageReceived: (_) {}), + throwsAssertionError, + ); + } + } + }); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/AUTHORS b/packages/webview_flutter/webview_flutter_wkwebview/AUTHORS new file mode 100644 index 000000000000..78f9e5ad9f6b --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md new file mode 100644 index 000000000000..242d79b4bd82 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/CHANGELOG.md @@ -0,0 +1,7 @@ +## 2.0.14 + +* Update example App so navigation menu loads immediatly but only becomes available when `WebViewController` is available (same behavior as example App in webview_flutter package). + +## 2.0.13 + +* Extract WKWebView implementation from `webview_flutter`. diff --git a/packages/webview_flutter/webview_flutter_wkwebview/LICENSE b/packages/webview_flutter/webview_flutter_wkwebview/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/README.md b/packages/webview_flutter/webview_flutter_wkwebview/README.md new file mode 100644 index 000000000000..2e3a87b7f310 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/README.md @@ -0,0 +1,11 @@ +# webview\_flutter\_wkwebview + +The Apple WKWebView implementation of [`webview_flutter`][1]. + +## Usage + +This package is [endorsed][2], which means you can simply use `webview_flutter` +normally. This package will be automatically included in your app when you do. + +[1]: https://pub.dev/packages/webview_flutter +[2]: https://flutter.dev/docs/development/packages-and-plugins/developing-packages#endorsed-federated-plugin diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/.metadata b/packages/webview_flutter/webview_flutter_wkwebview/example/.metadata new file mode 100644 index 000000000000..da83b1ada1bd --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/.metadata @@ -0,0 +1,8 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: 1e5cb2d87f8542f9fbbd0f22d528823274be0acb + channel: master diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/README.md b/packages/webview_flutter/webview_flutter_wkwebview/example/README.md new file mode 100644 index 000000000000..850ee74397a9 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/README.md @@ -0,0 +1,8 @@ +# webview_flutter_example + +Demonstrates how to use the webview_flutter plugin. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](https://flutter.dev/). diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/assets/sample_audio.ogg b/packages/webview_flutter/webview_flutter_wkwebview/example/assets/sample_audio.ogg new file mode 100644 index 000000000000..27e17104277b Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/assets/sample_audio.ogg differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/assets/sample_video.mp4 b/packages/webview_flutter/webview_flutter_wkwebview/example/assets/sample_video.mp4 new file mode 100644 index 000000000000..a203d0cdf13e Binary files /dev/null and b/packages/webview_flutter/webview_flutter_wkwebview/example/assets/sample_video.mp4 differ diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart new file mode 100644 index 000000000000..8ba17a2428c5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/integration_test/webview_flutter_test.dart @@ -0,0 +1,1216 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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 'dart:typed_data'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_wkwebview_example/navigation_decision.dart'; +import 'package:webview_flutter_wkwebview_example/navigation_request.dart'; +import 'package:webview_flutter_wkwebview_example/web_view.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + // URLs to navigate to in tests. These need to be URLs that we are confident will + // always be accessible, and won't do redirection. (E.g., just + // 'https://www.google.com/' will sometimes redirect traffic that looks + // like it's coming from a bot, which is true of these tests). + const String primaryUrl = 'https://flutter.dev/'; + const String secondaryUrl = 'https://www.google.com/robots.txt'; + + // Set to `false` to include all flaky tests in the test run. See also https://github.com/flutter/flutter/issues/86757. + const bool _skipDueToIssue86757 = false; + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('initialUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); + }, skip: _skipDueToIssue86757); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets('loadUrl', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await controller.loadUrl(secondaryUrl); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); + }, skip: _skipDueToIssue86757); + + testWidgets('loadUrl with headers', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageStarts = StreamController(); + final StreamController pageLoads = StreamController(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarts.add(url); + }, + onPageFinished: (String url) { + pageLoads.add(url); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final Map headers = { + 'test_header': 'flutter_test_header' + }; + await controller.loadUrl('https://flutter-header-echo.herokuapp.com/', + headers: headers); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, 'https://flutter-header-echo.herokuapp.com/'); + + await pageStarts.stream.firstWhere((String url) => url == currentUrl); + await pageLoads.stream.firstWhere((String url) => url == currentUrl); + + final String content = await controller + .evaluateJavascript('document.documentElement.innerText'); + expect(content.contains('flutter_test_header'), isTrue); + }); + + testWidgets('JavaScriptChannel', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final List messagesReceived = []; + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + // This is the data URL for: '' + initialUrl: + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'Echo', + onMessageReceived: (JavascriptMessage message) { + messagesReceived.add(message.message); + }, + ), + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + expect(messagesReceived, isEmpty); + // Append a return value "1" in the end will prevent an iOS platform exception. + // See: https://github.com/flutter/flutter/issues/66318#issuecomment-701105380 + // TODO(cyanglaz): remove the workaround "1" in the end when the below issue is fixed. + // https://github.com/flutter/flutter/issues/66318 + await controller.evaluateJavascript('Echo.postMessage("hello");1;'); + expect(messagesReceived, equals(['hello'])); + }); + + testWidgets('resize webview', (WidgetTester tester) async { + final String resizeTest = ''' + + Resize test + + + + + + '''; + final String resizeTestBase64 = + base64Encode(const Utf8Encoder().convert(resizeTest)); + final Completer resizeCompleter = Completer(); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + final GlobalKey key = GlobalKey(); + + final WebView webView = WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$resizeTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptChannels: { + JavascriptChannel( + name: 'Resize', + onMessageReceived: (JavascriptMessage message) { + resizeCompleter.complete(true); + }, + ), + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + javascriptMode: JavascriptMode.unrestricted, + ); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: 200, + height: 200, + child: webView, + ), + ], + ), + ), + ); + + await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + expect(resizeCompleter.isCompleted, false); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: Column( + children: [ + SizedBox( + width: 400, + height: 400, + child: webView, + ), + ], + ), + ), + ); + + await resizeCompleter.future; + }); + + testWidgets('set custom userAgent', (WidgetTester tester) async { + final Completer controllerCompleter1 = + Completer(); + final GlobalKey _globalKey = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent1', + onWebViewCreated: (WebViewController controller) { + controllerCompleter1.complete(controller); + }, + ), + ), + ); + final WebViewController controller1 = await controllerCompleter1.future; + final String customUserAgent1 = await _getUserAgent(controller1); + expect(customUserAgent1, 'Custom_User_Agent1'); + // rebuild the WebView with a different user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent2', + ), + ), + ); + + final String customUserAgent2 = await _getUserAgent(controller1); + expect(customUserAgent2, 'Custom_User_Agent2'); + }); + + testWidgets('use default platform userAgent after webView is rebuilt', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final GlobalKey _globalKey = GlobalKey(); + // Build the webView with no user agent to get the default platform user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: primaryUrl, + javascriptMode: JavascriptMode.unrestricted, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String defaultPlatformUserAgent = await _getUserAgent(controller); + // rebuild the WebView with a custom user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + userAgent: 'Custom_User_Agent', + ), + ), + ); + final String customUserAgent = await _getUserAgent(controller); + expect(customUserAgent, 'Custom_User_Agent'); + // rebuilds the WebView with no user agent. + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: _globalKey, + initialUrl: 'about:blank', + javascriptMode: JavascriptMode.unrestricted, + ), + ), + ); + + final String customUserAgent2 = await _getUserAgent(controller); + expect(customUserAgent2, defaultPlatformUserAgent); + }); + + group('Video playback policy', () { + late String videoTestBase64; + setUpAll(() async { + final ByteData videoData = + await rootBundle.load('assets/sample_video.mp4'); + final String base64VideoData = + base64Encode(Uint8List.view(videoData.buffer)); + final String videoTest = ''' + + Video auto play + + + + + + + '''; + videoTestBase64 = base64Encode(const Utf8Encoder().convert(videoTest)); + }); + + testWidgets('Auto media playback', (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + controllerCompleter = Completer(); + pageLoaded = Completer(); + + // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + controller = await controllerCompleter.future; + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(true)); + }); + + testWidgets('Changes to initialMediaPlaybackPolicy are ignored', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + await controller.reload(); + + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + }); + + testWidgets('Video plays inline when allowsInlineMediaPlayback is true', + (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + Completer videoPlaying = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'VideoTestTime', + onMessageReceived: (JavascriptMessage message) { + final double currentTime = double.parse(message.message); + // Let it play for at least 1 second to make sure the related video's properties are set. + if (currentTime > 1) { + videoPlaying.complete(null); + } + }, + ), + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + allowsInlineMediaPlayback: true, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + // Pump once to trigger the video play. + await tester.pump(); + + // Makes sure we get the correct event that indicates the video is actually playing. + await videoPlaying.future; + + String fullScreen = + await controller.evaluateJavascript('isFullScreen();'); + expect(fullScreen, _webviewBool(false)); + }); + + testWidgets( + 'Video plays full screen when allowsInlineMediaPlayback is false', + (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + Completer videoPlaying = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$videoTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + javascriptChannels: { + JavascriptChannel( + name: 'VideoTestTime', + onMessageReceived: (JavascriptMessage message) { + final double currentTime = double.parse(message.message); + // Let it play for at least 1 second to make sure the related video's properties are set. + if (currentTime > 1) { + videoPlaying.complete(null); + } + }, + ), + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + allowsInlineMediaPlayback: false, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + // Pump once to trigger the video play. + await tester.pump(); + + // Makes sure we get the correct event that indicates the video is actually playing. + await videoPlaying.future; + + String fullScreen = + await controller.evaluateJavascript('isFullScreen();'); + expect(fullScreen, _webviewBool(true)); + }); + }); + + group('Audio playback policy', () { + late String audioTestBase64; + setUpAll(() async { + final ByteData audioData = + await rootBundle.load('assets/sample_audio.ogg'); + final String base64AudioData = + base64Encode(Uint8List.view(audioData.buffer)); + final String audioTest = ''' + + Audio auto play + + + + + + + '''; + audioTestBase64 = base64Encode(const Utf8Encoder().convert(audioTest)); + }); + + testWidgets('Auto media playback', (WidgetTester tester) async { + Completer controllerCompleter = + Completer(); + Completer pageStarted = Completer(); + Completer pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + controllerCompleter = Completer(); + pageStarted = Completer(); + pageLoaded = Completer(); + + // We change the key to re-create a new webview as we change the initialMediaPlaybackPolicy + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(true)); + }); + + testWidgets('Changes to initialMediaPlaybackPolocy are ignored', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageStarted = Completer(); + Completer pageLoaded = Completer(); + + final GlobalKey key = GlobalKey(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: AutoMediaPlaybackPolicy.always_allow, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + String isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + + pageStarted = Completer(); + pageLoaded = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: key, + initialUrl: 'data:text/html;charset=utf-8;base64,$audioTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + initialMediaPlaybackPolicy: + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + ), + ), + ); + + await controller.reload(); + + await pageStarted.future; + await pageLoaded.future; + + isPaused = await controller.evaluateJavascript('isPaused();'); + expect(isPaused, _webviewBool(false)); + }); + }); + + testWidgets('getTitle', (WidgetTester tester) async { + final String getTitleTest = ''' + + Some title + + + + + '''; + final String getTitleTestBase64 = + base64Encode(const Utf8Encoder().convert(getTitleTest)); + final Completer pageStarted = Completer(); + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: 'data:text/html;charset=utf-8;base64,$getTitleTestBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageStarted: (String url) { + pageStarted.complete(null); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageStarted.future; + await pageLoaded.future; + + final String? title = await controller.getTitle(); + expect(title, 'Some title'); + }); + + group('Programmatic Scroll', () { + testWidgets('setAndGetScrollPosition', (WidgetTester tester) async { + final String scrollTestPage = ''' + + + + + + +
      + + + '''; + + final String scrollTestPageBase64 = + base64Encode(const Utf8Encoder().convert(scrollTestPage)); + + final Completer pageLoaded = Completer(); + final Completer controllerCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + initialUrl: + 'data:text/html;charset=utf-8;base64,$scrollTestPageBase64', + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + + final WebViewController controller = await controllerCompleter.future; + await pageLoaded.future; + + await tester.pumpAndSettle(Duration(seconds: 3)); + + int scrollPosX = await controller.getScrollX(); + int scrollPosY = await controller.getScrollY(); + + // Check scrollTo() + const int X_SCROLL = 123; + const int Y_SCROLL = 321; + // Get the initial position; this ensures that scrollTo is actually + // changing something, but also gives the native view's scroll position + // time to settle. + expect(scrollPosX, isNot(X_SCROLL)); + expect(scrollPosX, isNot(Y_SCROLL)); + + await controller.scrollTo(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(scrollPosX, X_SCROLL); + expect(scrollPosY, Y_SCROLL); + + // Check scrollBy() (on top of scrollTo()) + await controller.scrollBy(X_SCROLL, Y_SCROLL); + scrollPosX = await controller.getScrollX(); + scrollPosY = await controller.getScrollY(); + expect(scrollPosX, X_SCROLL * 2); + expect(scrollPosY, Y_SCROLL * 2); + }); + }); + + group('NavigationDelegate', () { + final String blankPage = ""; + final String blankPageEncoded = 'data:text/html;charset=utf-8;base64,' + + base64Encode(const Utf8Encoder().convert(blankPage)); + + testWidgets('can allow requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller.evaluateJavascript('location.href = "$secondaryUrl"'); + + await pageLoads.stream.first; // Wait for the next page load. + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); + }); + + testWidgets('onWebResourceError', (WidgetTester tester) async { + final Completer errorCompleter = + Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: 'https://www.notawebsite..com', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + ), + ), + ); + + final WebResourceError error = await errorCompleter.future; + expect(error, isNotNull); + + if (Platform.isIOS) { + expect(error.domain, isNotNull); + expect(error.failingUrl, isNull); + } else if (Platform.isAndroid) { + expect(error.errorType, isNotNull); + expect(error.failingUrl?.startsWith('https://www.notawebsite..com'), + isTrue); + } + }); + + testWidgets('onWebResourceError is not called with valid url', + (WidgetTester tester) async { + final Completer errorCompleter = + Completer(); + final Completer pageFinishCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: + 'data:text/html;charset=utf-8;base64,PCFET0NUWVBFIGh0bWw+', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + onPageFinished: (_) => pageFinishCompleter.complete(), + ), + ), + ); + + expect(errorCompleter.future, doesNotComplete); + await pageFinishCompleter.future; + }); + + testWidgets( + 'onWebResourceError only called for main frame', + (WidgetTester tester) async { + final String iframeTest = ''' + + + + WebResourceError test + + + + + + '''; + final String iframeTestBase64 = + base64Encode(const Utf8Encoder().convert(iframeTest)); + + final Completer errorCompleter = + Completer(); + final Completer pageFinishCompleter = Completer(); + + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: + 'data:text/html;charset=utf-8;base64,$iframeTestBase64', + onWebResourceError: (WebResourceError error) { + errorCompleter.complete(error); + }, + onPageFinished: (_) => pageFinishCompleter.complete(), + ), + ), + ); + + expect(errorCompleter.future, doesNotComplete); + await pageFinishCompleter.future; + }, + ); + + testWidgets('can block requests', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + return (request.url.contains('youtube.com')) + ? NavigationDecision.prevent + : NavigationDecision.navigate; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller + .evaluateJavascript('location.href = "https://www.youtube.com/"'); + + // There should never be any second page load, since our new URL is + // blocked. Still wait for a potential page change for some time in order + // to give the test a chance to fail. + await pageLoads.stream.first + .timeout(const Duration(milliseconds: 500), onTimeout: () => ''); + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, isNot(contains('youtube.com'))); + }); + + testWidgets('supports asynchronous decisions', (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final StreamController pageLoads = + StreamController.broadcast(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + initialUrl: blankPageEncoded, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) async { + NavigationDecision decision = NavigationDecision.prevent; + decision = await Future.delayed( + const Duration(milliseconds: 10), + () => NavigationDecision.navigate); + return decision; + }, + onPageFinished: (String url) => pageLoads.add(url), + ), + ), + ); + + await pageLoads.stream.first; // Wait for initial page load. + final WebViewController controller = await controllerCompleter.future; + await controller.evaluateJavascript('location.href = "$secondaryUrl"'); + + await pageLoads.stream.first; // Wait for second page to load. + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, secondaryUrl); + }); + }); + + testWidgets('launches with gestureNavigationEnabled on iOS', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: SizedBox( + width: 400, + height: 300, + child: WebView( + key: GlobalKey(), + initialUrl: primaryUrl, + gestureNavigationEnabled: true, + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + ), + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); + }); + + testWidgets('target _blank opens in same window', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + final Completer pageLoaded = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(null); + }, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + await controller.evaluateJavascript('window.open("$primaryUrl", "_blank")'); + await pageLoaded.future; + final String? currentUrl = await controller.currentUrl(); + expect(currentUrl, primaryUrl); + }); + + // TODO(bparrishMines): skipped due to https://github.com/flutter/flutter/issues/86757. + testWidgets( + 'can open new window and go back', + (WidgetTester tester) async { + final Completer controllerCompleter = + Completer(); + Completer pageLoaded = Completer(); + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: WebView( + key: GlobalKey(), + onWebViewCreated: (WebViewController controller) { + controllerCompleter.complete(controller); + }, + javascriptMode: JavascriptMode.unrestricted, + onPageFinished: (String url) { + pageLoaded.complete(); + }, + initialUrl: primaryUrl, + ), + ), + ); + final WebViewController controller = await controllerCompleter.future; + expect(controller.currentUrl(), completion(primaryUrl)); + await pageLoaded.future; + pageLoaded = Completer(); + + await controller.evaluateJavascript('window.open("$secondaryUrl")'); + await pageLoaded.future; + pageLoaded = Completer(); + expect(controller.currentUrl(), completion(secondaryUrl)); + + expect(controller.canGoBack(), completion(true)); + await controller.goBack(); + await pageLoaded.future; + expect(controller.currentUrl(), completion(primaryUrl)); + }, + skip: _skipDueToIssue86757, + ); +} + +// JavaScript booleans evaluate to different string values on Android and iOS. +// This utility method returns the string boolean value of the current platform. +String _webviewBool(bool value) { + if (defaultTargetPlatform == TargetPlatform.iOS) { + return value ? '1' : '0'; + } + return value ? 'true' : 'false'; +} + +/// Returns the value used for the HTTP User-Agent: request header in subsequent HTTP requests. +Future _getUserAgent(WebViewController controller) async { + return _evaluateJavascript(controller, 'navigator.userAgent;'); +} + +Future _evaluateJavascript( + WebViewController controller, String js) async { + if (defaultTargetPlatform == TargetPlatform.iOS) { + return await controller.evaluateJavascript(js); + } + return jsonDecode(await controller.evaluateJavascript(js)); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/AppFrameworkInfo.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 000000000000..8d4492f977ad --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/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 + 9.0 + + diff --git a/packages/e2e/example/ios/Flutter/Debug.xcconfig b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/e2e/example/ios/Flutter/Debug.xcconfig rename to packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/Debug.xcconfig diff --git a/packages/e2e/example/ios/Flutter/Release.xcconfig b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/e2e/example/ios/Flutter/Release.xcconfig rename to packages/webview_flutter/webview_flutter_wkwebview/example/ios/Flutter/Release.xcconfig diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Podfile b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Podfile new file mode 100644 index 000000000000..66509fcae284 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/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 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/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 000000000000..62428d041adf --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,722 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 334734012669319100DCC49E /* FLTWebViewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */; }; + 334734022669319400DCC49E /* FLTWKNavigationDelegateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + 9D26F6F82D91F92CC095EBA9 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */; }; + D9A9D48F1A75E5C682944DDD /* libPods-RunnerTests.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 27CC950C9005575711528C12 /* libPods-RunnerTests.a */; }; + F7151F77266057800028CB91 /* FLTWebViewUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = F7151F76266057800028CB91 /* FLTWebViewUITests.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 68BDCAEE23C3F7CB00D9C032 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; + F7151F79266057800028CB91 /* 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 */ + 127772EEA7782174BE0D74B5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 27CC950C9005575711528C12 /* libPods-RunnerTests.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-RunnerTests.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTWKNavigationDelegateTests.m; sourceTree = ""; }; + 68BDCAE923C3F7CB00D9C032 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 68BDCAED23C3F7CB00D9C032 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLTWebViewTests.m; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 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 = ""; }; + C475C484BD510DD9CB2E403C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + F7151F74266057800028CB91 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + F7151F76266057800028CB91 /* FLTWebViewUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLTWebViewUITests.m; sourceTree = ""; }; + F7151F78266057800028CB91 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 68BDCAE623C3F7CB00D9C032 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D9A9D48F1A75E5C682944DDD /* libPods-RunnerTests.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9D26F6F82D91F92CC095EBA9 /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7151F71266057800028CB91 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 68BDCAEA23C3F7CB00D9C032 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 686B4BF82548DBC7000AEA36 /* FLTWKNavigationDelegateTests.m */, + 68BDCAF523C3F97800D9C032 /* FLTWebViewTests.m */, + 68BDCAED23C3F7CB00D9C032 /* Info.plist */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 68BDCAEA23C3F7CB00D9C032 /* RunnerTests */, + F7151F75266057800028CB91 /* RunnerUITests */, + 97C146EF1CF9000F007C117D /* Products */, + C6FFB52F5C2B8A41A7E39DE2 /* Pods */, + B6736FC417BDCCDA377E779D /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 68BDCAE923C3F7CB00D9C032 /* RunnerTests.xctest */, + F7151F74266057800028CB91 /* 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 = ""; + }; + B6736FC417BDCCDA377E779D /* Frameworks */ = { + isa = PBXGroup; + children = ( + 8B845D8FBDE0AAD6BE1A0386 /* libPods-Runner.a */, + 27CC950C9005575711528C12 /* libPods-RunnerTests.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + C6FFB52F5C2B8A41A7E39DE2 /* Pods */ = { + isa = PBXGroup; + children = ( + 127772EEA7782174BE0D74B5 /* Pods-Runner.debug.xcconfig */, + C475C484BD510DD9CB2E403C /* Pods-Runner.release.xcconfig */, + F674B2A05DAC369B4FF27850 /* Pods-RunnerTests.debug.xcconfig */, + E14113434CCE6D3186B5CBC3 /* Pods-RunnerTests.release.xcconfig */, + ); + name = Pods; + sourceTree = ""; + }; + F7151F75266057800028CB91 /* RunnerUITests */ = { + isa = PBXGroup; + children = ( + F7151F76266057800028CB91 /* FLTWebViewUITests.m */, + F7151F78266057800028CB91 /* Info.plist */, + ); + path = RunnerUITests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 68BDCAE823C3F7CB00D9C032 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 68BDCAF223C3F7CB00D9C032 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 53FD4CBDD9756D74B5A3B4C1 /* [CP] Check Pods Manifest.lock */, + 68BDCAE523C3F7CB00D9C032 /* Sources */, + 68BDCAE623C3F7CB00D9C032 /* Frameworks */, + 68BDCAE723C3F7CB00D9C032 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 68BDCAEF23C3F7CB00D9C032 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = webview_flutter_exampleTests; + productReference = 68BDCAE923C3F7CB00D9C032 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + B71376B4FB8384EF9D5F3F84 /* [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"; + }; + F7151F73266057800028CB91 /* RunnerUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = F7151F7B266057800028CB91 /* Build configuration list for PBXNativeTarget "RunnerUITests" */; + buildPhases = ( + F7151F70266057800028CB91 /* Sources */, + F7151F71266057800028CB91 /* Frameworks */, + F7151F72266057800028CB91 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + F7151F7A266057800028CB91 /* PBXTargetDependency */, + ); + name = RunnerUITests; + productName = RunnerUITests; + productReference = F7151F74266057800028CB91 /* RunnerUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + DefaultBuildSystemTypeForWorkspace = Original; + LastUpgradeCheck = 1030; + ORGANIZATIONNAME = "The Flutter Authors"; + TargetAttributes = { + 68BDCAE823C3F7CB00D9C032 = { + ProvisioningStyle = Automatic; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + F7151F73266057800028CB91 = { + CreatedOnToolsVersion = 12.5; + ProvisioningStyle = Automatic; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 68BDCAE823C3F7CB00D9C032 /* RunnerTests */, + F7151F73266057800028CB91 /* RunnerUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 68BDCAE723C3F7CB00D9C032 /* 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; + }; + F7151F72266057800028CB91 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed\n/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" thin\n"; + }; + 53FD4CBDD9756D74B5A3B4C1 /* [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\n"; + }; + B71376B4FB8384EF9D5F3F84 /* [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 */ + 68BDCAE523C3F7CB00D9C032 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 334734012669319100DCC49E /* FLTWebViewTests.m in Sources */, + 334734022669319400DCC49E /* FLTWKNavigationDelegateTests.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; + }; + F7151F70266057800028CB91 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + F7151F77266057800028CB91 /* FLTWebViewUITests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 68BDCAEF23C3F7CB00D9C032 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 68BDCAEE23C3F7CB00D9C032 /* PBXContainerItemProxy */; + }; + F7151F7A266057800028CB91 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = F7151F79266057800028CB91 /* 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 */ + 68BDCAF023C3F7CB00D9C032 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = F674B2A05DAC369B4FF27850 /* 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; + }; + 68BDCAF123C3F7CB00D9C032 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E14113434CCE6D3186B5CBC3 /* 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; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(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.webviewFlutterExample; + 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.webviewFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; + F7151F7C266057800028CB91 /* 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; + }; + F7151F7D266057800028CB91 /* 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 */ + 68BDCAF223C3F7CB00D9C032 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 68BDCAF023C3F7CB00D9C032 /* Debug */, + 68BDCAF123C3F7CB00D9C032 /* 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; + }; + F7151F7B266057800028CB91 /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + F7151F7C266057800028CB91 /* Debug */, + F7151F7D266057800028CB91 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 000000000000..d7453a8ce862 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/quick_actions/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/quick_actions/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000000..18d981003d68 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/AppDelegate.h b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/AppDelegate.h new file mode 100644 index 000000000000..0681d288bb70 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/AppDelegate.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/AppDelegate.m new file mode 100644 index 000000000000..30b87969f44a --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/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/in_app_purchase/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/packages/espresso/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/espresso/example/ios/Runner/Base.lproj/Main.storyboard b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/espresso/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist new file mode 100644 index 000000000000..a810c5a172c0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/Info.plist @@ -0,0 +1,45 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + webview_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 + + + diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/main.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/Runner/main.m new file mode 100644 index 000000000000..f97b9ef5c8a1 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m new file mode 100644 index 000000000000..eb6d1543ec07 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWKNavigationDelegateTests.m @@ -0,0 +1,41 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import Flutter; +@import XCTest; +@import webview_flutter_wkwebview; + +// OCMock library doesn't generate a valid modulemap. +#import + +@interface FLTWKNavigationDelegateTests : XCTestCase + +@property(strong, nonatomic) FlutterMethodChannel *mockMethodChannel; +@property(strong, nonatomic) FLTWKNavigationDelegate *navigationDelegate; + +@end + +@implementation FLTWKNavigationDelegateTests + +- (void)setUp { + self.mockMethodChannel = OCMClassMock(FlutterMethodChannel.class); + self.navigationDelegate = + [[FLTWKNavigationDelegate alloc] initWithChannel:self.mockMethodChannel]; +} + +- (void)testWebViewWebContentProcessDidTerminateCallsRecourseErrorChannel { + if (@available(iOS 9.0, *)) { + // `webViewWebContentProcessDidTerminate` is only available on iOS 9.0 and above. + WKWebView *webview = OCMClassMock(WKWebView.class); + [self.navigationDelegate webViewWebContentProcessDidTerminate:webview]; + OCMVerify([self.mockMethodChannel + invokeMethod:@"onWebResourceError" + arguments:[OCMArg checkWithBlock:^BOOL(NSDictionary *args) { + XCTAssertEqualObjects(args[@"errorType"], @"webContentProcessTerminated"); + return true; + }]]); + } +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m new file mode 100644 index 000000000000..631c4a105063 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/FLTWebViewTests.m @@ -0,0 +1,91 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import Flutter; +@import XCTest; +@import webview_flutter_wkwebview; + +// OCMock library doesn't generate a valid modulemap. +#import + +static bool feq(CGFloat a, CGFloat b) { return fabs(b - a) < FLT_EPSILON; } + +@interface FLTWebViewTests : XCTestCase + +@property(strong, nonatomic) NSObject *mockBinaryMessenger; + +@end + +@implementation FLTWebViewTests + +- (void)setUp { + [super setUp]; + self.mockBinaryMessenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); +} + +- (void)testCanInitFLTWebViewController { + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + XCTAssertNotNil(controller); +} + +- (void)testCanInitFLTWebViewFactory { + FLTWebViewFactory *factory = + [[FLTWebViewFactory alloc] initWithMessenger:self.mockBinaryMessenger]; + XCTAssertNotNil(factory); +} + +- (void)webViewContentInsetBehaviorShouldBeNeverOnIOS11 { + if (@available(iOS 11, *)) { + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + UIView *view = controller.view; + XCTAssertTrue([view isKindOfClass:WKWebView.class]); + WKWebView *webView = (WKWebView *)view; + XCTAssertEqual(webView.scrollView.contentInsetAdjustmentBehavior, + UIScrollViewContentInsetAdjustmentNever); + } +} + +- (void)testWebViewScrollIndicatorAticautomaticallyAdjustsScrollIndicatorInsetsShouldbeNoOnIOS13 { + if (@available(iOS 13, *)) { + FLTWebViewController *controller = + [[FLTWebViewController alloc] initWithFrame:CGRectMake(0, 0, 300, 400) + viewIdentifier:1 + arguments:nil + binaryMessenger:self.mockBinaryMessenger]; + UIView *view = controller.view; + XCTAssertTrue([view isKindOfClass:WKWebView.class]); + WKWebView *webView = (WKWebView *)view; + XCTAssertFalse(webView.scrollView.automaticallyAdjustsScrollIndicatorInsets); + } +} + +- (void)testContentInsetsSumAlwaysZeroAfterSetFrame { + FLTWKWebView *webView = [[FLTWKWebView alloc] initWithFrame:CGRectMake(0, 0, 300, 400)]; + webView.scrollView.contentInset = UIEdgeInsetsMake(0, 0, 300, 0); + XCTAssertFalse(UIEdgeInsetsEqualToEdgeInsets(webView.scrollView.contentInset, UIEdgeInsetsZero)); + webView.frame = CGRectMake(0, 0, 300, 200); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(webView.scrollView.contentInset, UIEdgeInsetsZero)); + XCTAssertTrue(CGRectEqualToRect(webView.frame, CGRectMake(0, 0, 300, 200))); + + if (@available(iOS 11, *)) { + // After iOS 11, we need to make sure the contentInset compensates the adjustedContentInset. + UIScrollView *partialMockScrollView = OCMPartialMock(webView.scrollView); + UIEdgeInsets insetToAdjust = UIEdgeInsetsMake(0, 0, 300, 0); + OCMStub(partialMockScrollView.adjustedContentInset).andReturn(insetToAdjust); + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(webView.scrollView.contentInset, UIEdgeInsetsZero)); + webView.frame = CGRectMake(0, 0, 300, 100); + XCTAssertTrue(feq(webView.scrollView.contentInset.bottom, -insetToAdjust.bottom)); + XCTAssertTrue(CGRectEqualToRect(webView.frame, CGRectMake(0, 0, 300, 100))); + } +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/Info.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerTests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/FLTWebViewUITests.m b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/FLTWebViewUITests.m new file mode 100644 index 000000000000..d193be745972 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/FLTWebViewUITests.m @@ -0,0 +1,101 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +@import XCTest; +@import os.log; + +@interface FLTWebViewUITests : XCTestCase +@property(nonatomic, strong) XCUIApplication* app; +@end + +@implementation FLTWebViewUITests + +- (void)setUp { + self.continueAfterFailure = NO; + + self.app = [[XCUIApplication alloc] init]; + [self.app launch]; +} + +- (void)testUserAgent { + XCUIApplication* app = self.app; + XCUIElement* menu = app.buttons[@"Show menu"]; + if (![menu waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find menu"); + } + [menu tap]; + + XCUIElement* userAgent = app.buttons[@"Show user agent"]; + if (![userAgent waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find Show user agent"); + } + NSPredicate* userAgentPredicate = + [NSPredicate predicateWithFormat:@"label BEGINSWITH 'User Agent: Mozilla/5.0 (iPhone; '"]; + XCUIElement* userAgentPopUp = [app.otherElements elementMatchingPredicate:userAgentPredicate]; + XCTAssertFalse(userAgentPopUp.exists); + [userAgent tap]; + if (![userAgentPopUp waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find user agent pop up"); + } +} + +- (void)testCache { + XCUIApplication* app = self.app; + XCUIElement* menu = app.buttons[@"Show menu"]; + if (![menu waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find menu"); + } + [menu tap]; + + XCUIElement* clearCache = app.buttons[@"Clear cache"]; + if (![clearCache waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find Clear cache"); + } + [clearCache tap]; + + [menu tap]; + + XCUIElement* listCache = app.buttons[@"List cache"]; + if (![listCache waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find List cache"); + } + [listCache tap]; + + XCUIElement* emptyCachePopup = app.otherElements[@"{\"cacheKeys\":[],\"localStorage\":{}}"]; + if (![emptyCachePopup waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find empty cache pop up"); + } + + [menu tap]; + XCUIElement* addCache = app.buttons[@"Add to cache"]; + if (![addCache waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find Add to cache"); + } + [addCache tap]; + [menu tap]; + + if (![listCache waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find List cache"); + } + [listCache tap]; + + XCUIElement* cachePopup = + app.otherElements[@"{\"cacheKeys\":[\"test_caches_entry\"],\"localStorage\":{\"test_" + @"localStorage\":\"dummy_entry\"}}"]; + if (![cachePopup waitForExistenceWithTimeout:30.0]) { + os_log_error(OS_LOG_DEFAULT, "%@", app.debugDescription); + XCTFail(@"Failed due to not able to find cache pop up"); + } +} + +@end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/Info.plist b/packages/webview_flutter/webview_flutter_wkwebview/example/ios/RunnerUITests/Info.plist new file mode 100644 index 000000000000..64d65ca49577 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/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/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart new file mode 100644 index 000000000000..15b4cfc7c549 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/main.dart @@ -0,0 +1,344 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +import 'navigation_decision.dart'; +import 'navigation_request.dart'; +import 'web_view.dart'; + +void main() { + runApp(MaterialApp(home: _WebViewExample())); +} + +const String kNavigationExamplePage = ''' + +Navigation Delegate Example + +

      +The navigation delegate is set to block navigation to the youtube website. +

      + + + +'''; + +class _WebViewExample extends StatefulWidget { + const _WebViewExample({Key? key}) : super(key: key); + + @override + _WebViewExampleState createState() => _WebViewExampleState(); +} + +class _WebViewExampleState extends State<_WebViewExample> { + final Completer _controller = + Completer(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Flutter WebView example'), + // This drop down menu demonstrates that Flutter widgets can be shown over the web view. + actions: [ + _NavigationControls(_controller.future), + _SampleMenu(_controller.future), + ], + ), + // We're using a Builder here so we have a context that is below the Scaffold + // to allow calling Scaffold.of(context) so we can show a snackbar. + body: Builder(builder: (context) { + return WebView( + initialUrl: 'https://flutter.dev', + onWebViewCreated: (WebViewController controller) { + _controller.complete(controller); + }, + javascriptChannels: _createJavascriptChannels(context), + javascriptMode: JavascriptMode.unrestricted, + navigationDelegate: (NavigationRequest request) { + if (request.url.startsWith('https://www.youtube.com/')) { + print('blocking navigation to $request}'); + return NavigationDecision.prevent; + } + print('allowing navigation to $request'); + return NavigationDecision.navigate; + }, + ); + }), + floatingActionButton: favoriteButton(), + ); + } + + Widget favoriteButton() { + return FutureBuilder( + future: _controller.future, + builder: (BuildContext context, + AsyncSnapshot controller) { + if (controller.hasData) { + return FloatingActionButton( + onPressed: () async { + final String url = (await controller.data!.currentUrl())!; + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + SnackBar(content: Text('Favorited $url')), + ); + }, + child: const Icon(Icons.favorite), + ); + } + return Container(); + }); + } +} + +Set _createJavascriptChannels(BuildContext context) { + return { + JavascriptChannel( + name: 'Snackbar', + onMessageReceived: (JavascriptMessage message) { + ScaffoldMessenger.of(context) + .showSnackBar(SnackBar(content: Text(message.message))); + }), + }; +} + +enum _MenuOptions { + showUserAgent, + listCookies, + clearCookies, + addToCache, + listCache, + clearCache, + navigationDelegate, +} + +class _SampleMenu extends StatelessWidget { + _SampleMenu(this.controller); + + final Future controller; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: controller, + builder: + (BuildContext context, AsyncSnapshot controller) { + return PopupMenuButton<_MenuOptions>( + onSelected: (_MenuOptions value) { + switch (value) { + case _MenuOptions.showUserAgent: + _onShowUserAgent(controller.data!, context); + break; + case _MenuOptions.listCookies: + _onListCookies(controller.data!, context); + break; + case _MenuOptions.clearCookies: + _onClearCookies(controller.data!, context); + break; + case _MenuOptions.addToCache: + _onAddToCache(controller.data!, context); + break; + case _MenuOptions.listCache: + _onListCache(controller.data!, context); + break; + case _MenuOptions.clearCache: + _onClearCache(controller.data!, context); + break; + case _MenuOptions.navigationDelegate: + _onNavigationDelegateExample(controller.data!, context); + break; + } + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem<_MenuOptions>( + value: _MenuOptions.showUserAgent, + child: const Text('Show user agent'), + enabled: controller.hasData, + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.listCookies, + child: Text('List cookies'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.clearCookies, + child: Text('Clear cookies'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.addToCache, + child: Text('Add to cache'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.listCache, + child: Text('List cache'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.clearCache, + child: Text('Clear cache'), + ), + const PopupMenuItem<_MenuOptions>( + value: _MenuOptions.navigationDelegate, + child: Text('Navigation Delegate example'), + ), + ], + ); + }, + ); + } + + void _onShowUserAgent( + WebViewController controller, BuildContext context) async { + // Send a message with the user agent string to the Snackbar JavaScript channel we registered + // with the WebView. + await controller.evaluateJavascript( + 'Snackbar.postMessage("User Agent: " + navigator.userAgent);'); + } + + void _onListCookies( + WebViewController controller, BuildContext context) async { + final String cookies = + await controller.evaluateJavascript('document.cookie'); + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Cookies:'), + _getCookieList(cookies), + ], + ), + )); + } + + void _onAddToCache(WebViewController controller, BuildContext context) async { + await controller.evaluateJavascript( + 'caches.open("test_caches_entry"); localStorage["test_localStorage"] = "dummy_entry";'); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('Added a test entry to cache.'), + )); + } + + void _onListCache(WebViewController controller, BuildContext context) async { + await controller.evaluateJavascript('caches.keys()' + '.then((cacheKeys) => JSON.stringify({"cacheKeys" : cacheKeys, "localStorage" : localStorage}))' + '.then((caches) => Snackbar.postMessage(caches))'); + } + + void _onClearCache(WebViewController controller, BuildContext context) async { + await controller.clearCache(); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text("Cache cleared."), + )); + } + + void _onClearCookies( + WebViewController controller, BuildContext context) async { + final bool hadCookies = await WebView.platform.clearCookies(); + String message = 'There were cookies. Now, they are gone!'; + if (!hadCookies) { + message = 'There are no cookies.'; + } + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + content: Text(message), + )); + } + + void _onNavigationDelegateExample( + WebViewController controller, BuildContext context) async { + final String contentBase64 = + base64Encode(const Utf8Encoder().convert(kNavigationExamplePage)); + await controller.loadUrl('data:text/html;base64,$contentBase64'); + } + + Widget _getCookieList(String cookies) { + if (cookies == null || cookies == '""') { + return Container(); + } + final List cookieList = cookies.split(';'); + final Iterable cookieWidgets = + cookieList.map((String cookie) => Text(cookie)); + return Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: cookieWidgets.toList(), + ); + } +} + +class _NavigationControls extends StatelessWidget { + const _NavigationControls(this._webViewControllerFuture) + : assert(_webViewControllerFuture != null); + + final Future _webViewControllerFuture; + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _webViewControllerFuture, + builder: + (BuildContext context, AsyncSnapshot snapshot) { + final bool webViewReady = + snapshot.connectionState == ConnectionState.done; + final WebViewController? controller = snapshot.data; + + return Row( + children: [ + IconButton( + icon: const Icon(Icons.arrow_back_ios), + onPressed: !webViewReady + ? null + : () async { + if (await controller!.canGoBack()) { + await controller.goBack(); + } else { + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + const SnackBar(content: Text("No back history item")), + ); + return; + } + }, + ), + IconButton( + icon: const Icon(Icons.arrow_forward_ios), + onPressed: !webViewReady + ? null + : () async { + if (await controller!.canGoForward()) { + await controller.goForward(); + } else { + // ignore: deprecated_member_use + Scaffold.of(context).showSnackBar( + const SnackBar( + content: Text("No forward history item")), + ); + return; + } + }, + ), + IconButton( + icon: const Icon(Icons.replay), + onPressed: !webViewReady + ? null + : () { + controller!.reload(); + }, + ), + ], + ); + }, + ); + } +} + +/// Callback type for handling messages sent from Javascript running in a web view. +typedef void JavascriptMessageHandler(JavascriptMessage message); diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_decision.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_decision.dart new file mode 100644 index 000000000000..d8178acd8096 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_decision.dart @@ -0,0 +1,12 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// A decision on how to handle a navigation request. +enum NavigationDecision { + /// Prevent the navigation from taking place. + prevent, + + /// Allow the navigation to take place. + navigate, +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_request.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_request.dart new file mode 100644 index 000000000000..c1ff8dc5a690 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/navigation_request.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. + +/// Information about a navigation action that is about to be executed. +class NavigationRequest { + NavigationRequest._({required this.url, required this.isForMainFrame}); + + /// The URL that will be loaded if the navigation is executed. + final String url; + + /// Whether the navigation request is to be loaded as the main frame. + final bool isForMainFrame; + + @override + String toString() { + return '$runtimeType(url: $url, isForMainFrame: $isForMainFrame)'; + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart new file mode 100644 index 000000000000..ddb8e9b0f14f --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/lib/web_view.dart @@ -0,0 +1,592 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:flutter/widgets.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; +import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; + +import 'navigation_decision.dart'; +import 'navigation_request.dart'; + +/// Optional callback invoked when a web view is first created. [controller] is +/// the [WebViewController] for the created web view. +typedef void WebViewCreatedCallback(WebViewController controller); + +/// Decides how to handle a specific navigation request. +/// +/// The returned [NavigationDecision] determines how the navigation described by +/// `navigation` should be handled. +/// +/// See also: [WebView.navigationDelegate]. +typedef FutureOr NavigationDelegate( + NavigationRequest navigation); + +/// Signature for when a [WebView] has started loading a page. +typedef void PageStartedCallback(String url); + +/// Signature for when a [WebView] has finished loading a page. +typedef void PageFinishedCallback(String url); + +/// Signature for when a [WebView] is loading a page. +typedef void PageLoadingCallback(int progress); + +/// Signature for when a [WebView] has failed to load a resource. +typedef void WebResourceErrorCallback(WebResourceError error); + +/// A web view widget for showing html content. +/// +/// There is a known issue that on iOS 13.4 and 13.5, other flutter widgets covering +/// the `WebView` is not able to block the `WebView` from receiving touch events. +/// See https://github.com/flutter/flutter/issues/53490. +class WebView extends StatefulWidget { + /// Creates a new web view. + /// + /// The web view can be controlled using a `WebViewController` that is passed to the + /// `onWebViewCreated` callback once the web view is created. + /// + /// The `javascriptMode` and `autoMediaPlaybackPolicy` parameters must not be null. + const WebView({ + Key? key, + this.onWebViewCreated, + this.initialUrl, + this.javascriptMode = JavascriptMode.disabled, + this.javascriptChannels, + this.navigationDelegate, + this.gestureRecognizers, + this.onPageStarted, + this.onPageFinished, + this.onProgress, + this.onWebResourceError, + this.debuggingEnabled = false, + this.gestureNavigationEnabled = false, + this.userAgent, + this.initialMediaPlaybackPolicy = + AutoMediaPlaybackPolicy.require_user_action_for_all_media_types, + this.allowsInlineMediaPlayback = false, + }) : assert(javascriptMode != null), + assert(initialMediaPlaybackPolicy != null), + assert(allowsInlineMediaPlayback != null), + super(key: key); + + /// The WebView platform that's used by this WebView. + static final WebViewPlatform platform = CupertinoWebView(); + + /// If not null invoked once the web view is created. + final WebViewCreatedCallback? onWebViewCreated; + + /// Which gestures should be consumed by the web view. + /// + /// It is possible for other gesture recognizers to be competing with the web view on pointer + /// events, e.g if the web view is inside a [ListView] the [ListView] will want to handle + /// vertical drags. The web view will claim gestures that are recognized by any of the + /// recognizers on this list. + /// + /// When this set is empty or null, the web view will only handle pointer events for gestures that + /// were not claimed by any other gesture recognizer. + final Set>? gestureRecognizers; + + /// The initial URL to load. + final String? initialUrl; + + /// Whether Javascript execution is enabled. + final JavascriptMode javascriptMode; + + /// The set of [JavascriptChannel]s available to JavaScript code running in the web view. + /// + /// For each [JavascriptChannel] in the set, a channel object is made available for the + /// JavaScript code in a window property named [JavascriptChannel.name]. + /// The JavaScript code can then call `postMessage` on that object to send a message that will be + /// passed to [JavascriptChannel.onMessageReceived]. + /// + /// For example for the following JavascriptChannel: + /// + /// ```dart + /// JavascriptChannel(name: 'Print', onMessageReceived: (JavascriptMessage message) { print(message.message); }); + /// ``` + /// + /// JavaScript code can call: + /// + /// ```javascript + /// Print.postMessage('Hello'); + /// ``` + /// + /// To asynchronously invoke the message handler which will print the message to standard output. + /// + /// Adding a new JavaScript channel only takes affect after the next page is loaded. + /// + /// Set values must not be null. A [JavascriptChannel.name] cannot be the same for multiple + /// channels in the list. + /// + /// A null value is equivalent to an empty set. + final Set? javascriptChannels; + + /// A delegate function that decides how to handle navigation actions. + /// + /// When a navigation is initiated by the WebView (e.g when a user clicks a link) + /// this delegate is called and has to decide how to proceed with the navigation. + /// + /// See [NavigationDecision] for possible decisions the delegate can take. + /// + /// When null all navigation actions are allowed. + /// + /// Caveats on Android: + /// + /// * Navigation actions targeted to the main frame can be intercepted, + /// navigation actions targeted to subframes are allowed regardless of the value + /// returned by this delegate. + /// * Setting a navigationDelegate makes the WebView treat all navigations as if they were + /// triggered by a user gesture, this disables some of Chromium's security mechanisms. + /// A navigationDelegate should only be set when loading trusted content. + /// * On Android WebView versions earlier than 67(most devices running at least Android L+ should have + /// a later version): + /// * When a navigationDelegate is set pages with frames are not properly handled by the + /// webview, and frames will be opened in the main frame. + /// * When a navigationDelegate is set HTTP requests do not include the HTTP referer header. + final NavigationDelegate? navigationDelegate; + + /// Controls whether inline playback of HTML5 videos is allowed on iOS. + /// + /// This field is ignored on Android because Android allows it by default. + /// + /// By default `allowsInlineMediaPlayback` is false. + final bool allowsInlineMediaPlayback; + + /// Invoked when a page starts loading. + final PageStartedCallback? onPageStarted; + + /// Invoked when a page has finished loading. + /// + /// This is invoked only for the main frame. + /// + /// When [onPageFinished] is invoked on Android, the page being rendered may + /// not be updated yet. + /// + /// When invoked on iOS or Android, any Javascript code that is embedded + /// directly in the HTML has been loaded and code injected with + /// [WebViewController.evaluateJavascript] can assume this. + final PageFinishedCallback? onPageFinished; + + /// Invoked when a page is loading. + final PageLoadingCallback? onProgress; + + /// Invoked when a web resource has failed to load. + /// + /// This callback is only called for the main page. + final WebResourceErrorCallback? onWebResourceError; + + /// Controls whether WebView debugging is enabled. + /// + /// Setting this to true enables [WebView debugging on Android](https://developers.google.com/web/tools/chrome-devtools/remote-debugging/). + /// + /// WebView debugging is enabled by default in dev builds on iOS. + /// + /// To debug WebViews on iOS: + /// - Enable developer options (Open Safari, go to Preferences -> Advanced and make sure "Show Develop Menu in Menubar" is on.) + /// - From the Menu-bar (of Safari) select Develop -> iPhone Simulator -> + /// + /// By default `debuggingEnabled` is false. + final bool debuggingEnabled; + + /// A Boolean value indicating whether horizontal swipe gestures will trigger back-forward list navigations. + /// + /// This only works on iOS. + /// + /// By default `gestureNavigationEnabled` is false. + final bool gestureNavigationEnabled; + + /// The value used for the HTTP User-Agent: request header. + /// + /// When null the platform's webview default is used for the User-Agent header. + /// + /// When the [WebView] is rebuilt with a different `userAgent`, the page reloads and the request uses the new User Agent. + /// + /// When [WebViewController.goBack] is called after changing `userAgent` the previous `userAgent` value is used until the page is reloaded. + /// + /// This field is ignored on iOS versions prior to 9 as the platform does not support a custom + /// user agent. + /// + /// By default `userAgent` is null. + final String? userAgent; + + /// Which restrictions apply on automatic media playback. + /// + /// This initial value is applied to the platform's webview upon creation. Any following + /// changes to this parameter are ignored (as long as the state of the [WebView] is preserved). + /// + /// The default policy is [AutoMediaPlaybackPolicy.require_user_action_for_all_media_types]. + final AutoMediaPlaybackPolicy initialMediaPlaybackPolicy; + + @override + _WebViewState createState() => _WebViewState(); +} + +class _WebViewState extends State { + final Completer _controller = + Completer(); + late final JavascriptChannelRegistry _javascriptChannelRegistry; + late final _PlatformCallbacksHandler _platformCallbacksHandler; + + @override + void initState() { + super.initState(); + _platformCallbacksHandler = _PlatformCallbacksHandler(widget); + _javascriptChannelRegistry = + JavascriptChannelRegistry(widget.javascriptChannels); + } + + @override + void didUpdateWidget(WebView oldWidget) { + super.didUpdateWidget(oldWidget); + _controller.future.then((WebViewController controller) { + controller._updateWidget(widget); + }); + } + + @override + Widget build(BuildContext context) { + return WebView.platform.build( + context: context, + onWebViewPlatformCreated: + (WebViewPlatformController? webViewPlatformController) { + WebViewController controller = WebViewController._( + widget, + webViewPlatformController!, + _javascriptChannelRegistry, + ); + _controller.complete(controller); + + if (widget.onWebViewCreated != null) { + widget.onWebViewCreated!(controller); + } + }, + webViewPlatformCallbacksHandler: _platformCallbacksHandler, + creationParams: CreationParams( + initialUrl: widget.initialUrl, + webSettings: _webSettingsFromWidget(widget), + javascriptChannelNames: + _javascriptChannelRegistry.channels.keys.toSet(), + autoMediaPlaybackPolicy: widget.initialMediaPlaybackPolicy, + userAgent: widget.userAgent, + ), + javascriptChannelRegistry: _javascriptChannelRegistry, + ); + } +} + +/// Controls a [WebView]. +/// +/// A [WebViewController] instance can be obtained by setting the [WebView.onWebViewCreated] +/// callback for a [WebView] widget. +class WebViewController { + WebViewController._( + this._widget, + this._webViewPlatformController, + this._javascriptChannelRegistry, + ) : assert(_webViewPlatformController != null) { + _settings = _webSettingsFromWidget(_widget); + } + + final JavascriptChannelRegistry _javascriptChannelRegistry; + + final WebViewPlatformController _webViewPlatformController; + + late WebSettings _settings; + + WebView _widget; + + /// Loads the specified URL. + /// + /// If `headers` is not null and the URL is an HTTP URL, the key value paris in `headers` will + /// be added as key value pairs of HTTP headers for the request. + /// + /// `url` must not be null. + /// + /// Throws an ArgumentError if `url` is not a valid URL string. + Future loadUrl( + String url, { + Map? headers, + }) async { + assert(url != null); + _validateUrlString(url); + return _webViewPlatformController.loadUrl(url, headers); + } + + /// Accessor to the current URL that the WebView is displaying. + /// + /// If [WebView.initialUrl] was never specified, returns `null`. + /// Note that this operation is asynchronous, and it is possible that the + /// current URL changes again by the time this function returns (in other + /// words, by the time this future completes, the WebView may be displaying a + /// different URL). + Future currentUrl() { + return _webViewPlatformController.currentUrl(); + } + + /// Checks whether there's a back history item. + /// + /// Note that this operation is asynchronous, and it is possible that the "canGoBack" state has + /// changed by the time the future completed. + Future canGoBack() { + return _webViewPlatformController.canGoBack(); + } + + /// Checks whether there's a forward history item. + /// + /// Note that this operation is asynchronous, and it is possible that the "canGoForward" state has + /// changed by the time the future completed. + Future canGoForward() { + return _webViewPlatformController.canGoForward(); + } + + /// Goes back in the history of this WebView. + /// + /// If there is no back history item this is a no-op. + Future goBack() { + return _webViewPlatformController.goBack(); + } + + /// Goes forward in the history of this WebView. + /// + /// If there is no forward history item this is a no-op. + Future goForward() { + return _webViewPlatformController.goForward(); + } + + /// Reloads the current URL. + Future reload() { + return _webViewPlatformController.reload(); + } + + /// Clears all caches used by the [WebView]. + /// + /// The following caches are cleared: + /// 1. Browser HTTP Cache. + /// 2. [Cache API](https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/cache-api) caches. + /// These are not yet supported in iOS WkWebView. Service workers tend to use this cache. + /// 3. Application cache. + /// 4. Local Storage. + /// + /// Note: Calling this method also triggers a reload. + Future clearCache() async { + await _webViewPlatformController.clearCache(); + return reload(); + } + + Future _updateWidget(WebView widget) async { + _widget = widget; + await _updateSettings(_webSettingsFromWidget(widget)); + await _updateJavascriptChannels( + _javascriptChannelRegistry.channels.values.toSet()); + } + + Future _updateSettings(WebSettings newSettings) { + final WebSettings update = + _clearUnchangedWebSettings(_settings, newSettings); + _settings = newSettings; + return _webViewPlatformController.updateSettings(update); + } + + Future _updateJavascriptChannels( + Set? newChannels) async { + final Set currentChannels = + _javascriptChannelRegistry.channels.keys.toSet(); + final Set newChannelNames = _extractChannelNames(newChannels); + final Set channelsToAdd = + newChannelNames.difference(currentChannels); + final Set channelsToRemove = + currentChannels.difference(newChannelNames); + if (channelsToRemove.isNotEmpty) { + await _webViewPlatformController + .removeJavascriptChannels(channelsToRemove); + } + if (channelsToAdd.isNotEmpty) { + await _webViewPlatformController.addJavascriptChannels(channelsToAdd); + } + _javascriptChannelRegistry.updateJavascriptChannelsFromSet(newChannels); + } + + /// Evaluates a JavaScript expression in the context of the current page. + /// + /// On Android returns the evaluation result as a JSON formatted string. + /// + /// On iOS depending on the value type the return value would be one of: + /// + /// - For primitive JavaScript types: the value string formatted (e.g JavaScript 100 returns '100'). + /// - For JavaScript arrays of supported types: a string formatted NSArray(e.g '(1,2,3), note that the string for NSArray is formatted and might contain newlines and extra spaces.'). + /// - Other non-primitive types are not supported on iOS and will complete the Future with an error. + /// + /// The Future completes with an error if a JavaScript error occurred, or on iOS, if the type of the + /// evaluated expression is not supported as described above. + /// + /// When evaluating Javascript in a [WebView], it is best practice to wait for + /// the [WebView.onPageFinished] callback. This guarantees all the Javascript + /// embedded in the main frame HTML has been loaded. + Future evaluateJavascript(String javascriptString) { + if (_settings.javascriptMode == JavascriptMode.disabled) { + return Future.error(FlutterError( + 'JavaScript mode must be enabled/unrestricted when calling evaluateJavascript.')); + } + // TODO(amirh): remove this on when the invokeMethod update makes it to stable Flutter. + // https://github.com/flutter/flutter/issues/26431 + // ignore: strong_mode_implicit_dynamic_method + return _webViewPlatformController.evaluateJavascript(javascriptString); + } + + /// Returns the title of the currently loaded page. + Future getTitle() { + return _webViewPlatformController.getTitle(); + } + + /// Sets the WebView's content scroll position. + /// + /// The parameters `x` and `y` specify the scroll position in WebView pixels. + Future scrollTo(int x, int y) { + return _webViewPlatformController.scrollTo(x, y); + } + + /// Move the scrolled position of this view. + /// + /// The parameters `x` and `y` specify the amount of WebView pixels to scroll by horizontally and vertically respectively. + Future scrollBy(int x, int y) { + return _webViewPlatformController.scrollBy(x, y); + } + + /// Return the horizontal scroll position, in WebView pixels, of this view. + /// + /// Scroll position is measured from left. + Future getScrollX() { + return _webViewPlatformController.getScrollX(); + } + + /// Return the vertical scroll position, in WebView pixels, of this view. + /// + /// Scroll position is measured from top. + Future getScrollY() { + return _webViewPlatformController.getScrollY(); + } + + // This method assumes that no fields in `currentValue` are null. + WebSettings _clearUnchangedWebSettings( + WebSettings currentValue, WebSettings newValue) { + assert(currentValue.javascriptMode != null); + assert(currentValue.hasNavigationDelegate != null); + assert(currentValue.hasProgressTracking != null); + assert(currentValue.debuggingEnabled != null); + assert(currentValue.userAgent != null); + assert(newValue.javascriptMode != null); + assert(newValue.hasNavigationDelegate != null); + assert(newValue.debuggingEnabled != null); + assert(newValue.userAgent != null); + + JavascriptMode? javascriptMode; + bool? hasNavigationDelegate; + bool? hasProgressTracking; + bool? debuggingEnabled; + WebSetting userAgent = WebSetting.absent(); + if (currentValue.javascriptMode != newValue.javascriptMode) { + javascriptMode = newValue.javascriptMode; + } + if (currentValue.hasNavigationDelegate != newValue.hasNavigationDelegate) { + hasNavigationDelegate = newValue.hasNavigationDelegate; + } + if (currentValue.hasProgressTracking != newValue.hasProgressTracking) { + hasProgressTracking = newValue.hasProgressTracking; + } + if (currentValue.debuggingEnabled != newValue.debuggingEnabled) { + debuggingEnabled = newValue.debuggingEnabled; + } + if (currentValue.userAgent != newValue.userAgent) { + userAgent = newValue.userAgent; + } + + return WebSettings( + javascriptMode: javascriptMode, + hasNavigationDelegate: hasNavigationDelegate, + hasProgressTracking: hasProgressTracking, + debuggingEnabled: debuggingEnabled, + userAgent: userAgent, + ); + } + + Set _extractChannelNames(Set? channels) { + final Set channelNames = channels == null + ? {} + : channels.map((JavascriptChannel channel) => channel.name).toSet(); + return channelNames; + } + +// Throws an ArgumentError if `url` is not a valid URL string. + void _validateUrlString(String url) { + try { + final Uri uri = Uri.parse(url); + if (uri.scheme.isEmpty) { + throw ArgumentError('Missing scheme in URL string: "$url"'); + } + } on FormatException catch (e) { + throw ArgumentError(e); + } + } +} + +WebSettings _webSettingsFromWidget(WebView widget) { + return WebSettings( + javascriptMode: widget.javascriptMode, + hasNavigationDelegate: widget.navigationDelegate != null, + hasProgressTracking: widget.onProgress != null, + debuggingEnabled: widget.debuggingEnabled, + gestureNavigationEnabled: widget.gestureNavigationEnabled, + allowsInlineMediaPlayback: widget.allowsInlineMediaPlayback, + userAgent: WebSetting.of(widget.userAgent), + ); +} + +class _PlatformCallbacksHandler implements WebViewPlatformCallbacksHandler { + _PlatformCallbacksHandler(this._webView); + + final WebView _webView; + + @override + FutureOr onNavigationRequest({ + required String url, + required bool isForMainFrame, + }) async { + if (url.startsWith('https://www.youtube.com/')) { + print('blocking navigation to $url'); + return false; + } + print('allowing navigation to $url'); + return true; + } + + @override + void onPageStarted(String url) { + if (_webView.onPageStarted != null) { + _webView.onPageStarted!(url); + } + } + + @override + void onPageFinished(String url) { + if (_webView.onPageFinished != null) { + _webView.onPageFinished!(url); + } + } + + @override + void onProgress(int progress) { + if (_webView.onProgress != null) { + _webView.onProgress!(progress); + } + } + + void onWebResourceError(WebResourceError error) { + if (_webView.onWebResourceError != null) { + _webView.onWebResourceError!(error); + } + } +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml new file mode 100644 index 000000000000..229da5e337a5 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/example/pubspec.yaml @@ -0,0 +1,33 @@ +name: webview_flutter_wkwebview_example +description: Demonstrates how to use the webview_flutter_wkwebview plugin. +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + webview_flutter_wkwebview: + # When depending on this package from a real application you should use: + # webview_flutter: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + +dev_dependencies: + espresso: ^0.1.0+2 + flutter_test: + sdk: flutter + flutter_driver: + sdk: flutter + integration_test: + sdk: flutter + pedantic: ^1.10.0 + +flutter: + uses-material-design: true + assets: + - assets/sample_audio.ogg + - assets/sample_video.mp4 diff --git a/packages/webview_flutter/webview_flutter_wkwebview/example/test_driver/integration_test.dart b/packages/webview_flutter/webview_flutter_wkwebview/example/test_driver/integration_test.dart new file mode 100644 index 000000000000..4f10f2a522f3 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/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/e2e/e2e_macos/macos/Assets/.gitkeep b/packages/webview_flutter/webview_flutter_wkwebview/ios/Assets/.gitkeep similarity index 100% rename from packages/e2e/e2e_macos/macos/Assets/.gitkeep rename to packages/webview_flutter/webview_flutter_wkwebview/ios/Assets/.gitkeep diff --git a/packages/webview_flutter/ios/Classes/FLTCookieManager.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.h similarity index 81% rename from packages/webview_flutter/ios/Classes/FLTCookieManager.h rename to packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.h index 3ad5c7e0d9bf..8fe331875250 100644 --- a/packages/webview_flutter/ios/Classes/FLTCookieManager.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.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/webview_flutter/ios/Classes/FLTCookieManager.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.m similarity index 96% rename from packages/webview_flutter/ios/Classes/FLTCookieManager.m rename to packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.m index 47948bf6b9f0..f4783ffb4123 100644 --- a/packages/webview_flutter/ios/Classes/FLTCookieManager.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTCookieManager.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/webview_flutter/ios/Classes/FLTWKNavigationDelegate.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.h similarity index 88% rename from packages/webview_flutter/ios/Classes/FLTWKNavigationDelegate.h rename to packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.h index 1625c4999bd2..31edadc8cc05 100644 --- a/packages/webview_flutter/ios/Classes/FLTWKNavigationDelegate.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.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/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.m similarity index 91% rename from packages/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m rename to packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.m index dd9608d57ac0..8b7ee7d0cfb7 100644 --- a/packages/webview_flutter/ios/Classes/FLTWKNavigationDelegate.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKNavigationDelegate.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. @@ -104,4 +104,13 @@ - (void)webView:(WKWebView *)webView withError:(NSError *)error { [self onWebResourceError:error]; } + +- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView { + NSError *contentProcessTerminatedError = + [[NSError alloc] initWithDomain:WKErrorDomain + code:WKErrorWebContentProcessTerminated + userInfo:nil]; + [self onWebResourceError:contentProcessTerminatedError]; +} + @end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.h new file mode 100644 index 000000000000..96af4ef6c578 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.h @@ -0,0 +1,19 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface FLTWKProgressionDelegate : NSObject + +- (instancetype)initWithWebView:(WKWebView *)webView channel:(FlutterMethodChannel *)channel; + +- (void)stopObservingProgress:(WKWebView *)webView; + +@end + +NS_ASSUME_NONNULL_END diff --git a/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.m new file mode 100644 index 000000000000..8e7af4649aa0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWKProgressionDelegate.m @@ -0,0 +1,41 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#import "FLTWKProgressionDelegate.h" + +NSString *const FLTWKEstimatedProgressKeyPath = @"estimatedProgress"; + +@implementation FLTWKProgressionDelegate { + FlutterMethodChannel *_methodChannel; +} + +- (instancetype)initWithWebView:(WKWebView *)webView channel:(FlutterMethodChannel *)channel { + self = [super init]; + if (self) { + _methodChannel = channel; + [webView addObserver:self + forKeyPath:FLTWKEstimatedProgressKeyPath + options:NSKeyValueObservingOptionNew + context:nil]; + } + return self; +} + +- (void)stopObservingProgress:(WKWebView *)webView { + [webView removeObserver:self forKeyPath:FLTWKEstimatedProgressKeyPath]; +} + +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if ([keyPath isEqualToString:FLTWKEstimatedProgressKeyPath]) { + NSNumber *newValue = + change[NSKeyValueChangeNewKey] ?: 0; // newValue is anywhere between 0.0 and 1.0 + int newValueAsInt = [newValue floatValue] * 100; // Anywhere between 0 and 100 + [_methodChannel invokeMethod:@"onProgress" arguments:@{@"progress" : @(newValueAsInt)}]; + } +} + +@end diff --git a/packages/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.h similarity index 76% rename from packages/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.h rename to packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.h index fffaedbe513b..2a80c7d886f2 100644 --- a/packages/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.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/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m similarity index 90% rename from packages/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.m rename to packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.m index a131263c9a92..9f01416acc6a 100644 --- a/packages/webview_flutter/ios/Classes/FLTWebViewFlutterPlugin.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FLTWebViewFlutterPlugin.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/webview_flutter/ios/Classes/FlutterWebView.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h similarity index 93% rename from packages/webview_flutter/ios/Classes/FlutterWebView.h rename to packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.h index 875551d3535d..6e795f7d1528 100644 --- a/packages/webview_flutter/ios/Classes/FlutterWebView.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.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/webview_flutter/ios/Classes/FlutterWebView.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m similarity index 90% rename from packages/webview_flutter/ios/Classes/FlutterWebView.m rename to packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m index 969e010913f3..c6d926d3cfc2 100644 --- a/packages/webview_flutter/ios/Classes/FlutterWebView.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/FlutterWebView.m @@ -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 "FlutterWebView.h" #import "FLTWKNavigationDelegate.h" +#import "FLTWKProgressionDelegate.h" #import "JavaScriptChannelHandler.h" @implementation FLTWebViewFactory { @@ -64,6 +65,7 @@ @implementation FLTWebViewController { // The set of registered JavaScript channel names. NSMutableSet* _javaScriptChannelNames; FLTWKNavigationDelegate* _navigationDelegate; + FLTWKProgressionDelegate* _progressionDelegate; } - (instancetype)initWithFrame:(CGRect)frame @@ -87,6 +89,7 @@ - (instancetype)initWithFrame:(CGRect)frame NSDictionary* settings = args[@"settings"]; WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init]; + [self applyConfigurationSettings:settings toConfiguration:configuration]; configuration.userContentController = userContentController; [self updateAutoMediaPlaybackPolicy:args[@"autoMediaPlaybackPolicy"] inConfiguration:configuration]; @@ -119,6 +122,12 @@ - (instancetype)initWithFrame:(CGRect)frame return self; } +- (void)dealloc { + if (_progressionDelegate != nil) { + [_progressionDelegate stopObservingProgress:_webView]; + } +} + - (UIView*)view { return _webView; } @@ -185,12 +194,12 @@ - (void)onLoadUrl:(FlutterMethodCall*)call result:(FlutterResult)result { - (void)onCanGoBack:(FlutterMethodCall*)call result:(FlutterResult)result { BOOL canGoBack = [_webView canGoBack]; - result([NSNumber numberWithBool:canGoBack]); + result(@(canGoBack)); } - (void)onCanGoForward:(FlutterMethodCall*)call result:(FlutterResult)result { BOOL canGoForward = [_webView canGoForward]; - result([NSNumber numberWithBool:canGoForward]); + result(@(canGoForward)); } - (void)onGoBack:(FlutterMethodCall*)call result:(FlutterResult)result { @@ -305,12 +314,12 @@ - (void)onScrollBy:(FlutterMethodCall*)call result:(FlutterResult)result { - (void)getScrollX:(FlutterMethodCall*)call result:(FlutterResult)result { int offsetX = _webView.scrollView.contentOffset.x; - result([NSNumber numberWithInt:offsetX]); + result(@(offsetX)); } - (void)getScrollY:(FlutterMethodCall*)call result:(FlutterResult)result { int offsetY = _webView.scrollView.contentOffset.y; - result([NSNumber numberWithInt:offsetY]); + result(@(offsetY)); } // Returns nil when successful, or an error message when one or more keys are unknown. @@ -323,6 +332,13 @@ - (NSString*)applySettings:(NSDictionary*)settings { } else if ([key isEqualToString:@"hasNavigationDelegate"]) { NSNumber* hasDartNavigationDelegate = settings[key]; _navigationDelegate.hasDartNavigationDelegate = [hasDartNavigationDelegate boolValue]; + } else if ([key isEqualToString:@"hasProgressTracking"]) { + NSNumber* hasProgressTrackingValue = settings[key]; + bool hasProgressTracking = [hasProgressTrackingValue boolValue]; + if (hasProgressTracking) { + _progressionDelegate = [[FLTWKProgressionDelegate alloc] initWithWebView:_webView + channel:_channel]; + } } else if ([key isEqualToString:@"debuggingEnabled"]) { // no-op debugging is always enabled on iOS. } else if ([key isEqualToString:@"gestureNavigationEnabled"]) { @@ -343,6 +359,18 @@ - (NSString*)applySettings:(NSDictionary*)settings { [unknownKeys componentsJoinedByString:@", "]]; } +- (void)applyConfigurationSettings:(NSDictionary*)settings + toConfiguration:(WKWebViewConfiguration*)configuration { + NSAssert(configuration != _webView.configuration, + @"configuration needs to be updated before webView.configuration."); + for (NSString* key in settings) { + if ([key isEqualToString:@"allowsInlineMediaPlayback"]) { + NSNumber* allowsInlineMediaPlayback = settings[key]; + configuration.allowsInlineMediaPlayback = [allowsInlineMediaPlayback boolValue]; + } + } +} + - (void)updateJsMode:(NSNumber*)mode { WKPreferences* preferences = [[_webView configuration] preferences]; switch ([mode integerValue]) { @@ -363,15 +391,25 @@ - (void)updateAutoMediaPlaybackPolicy:(NSNumber*)policy case 0: // require_user_action_for_all_media_types if (@available(iOS 10.0, *)) { configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeAll; + } else if (@available(iOS 9.0, *)) { + configuration.requiresUserActionForMediaPlayback = true; } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" configuration.mediaPlaybackRequiresUserAction = true; +#pragma clang diagnostic pop } break; case 1: // always_allow if (@available(iOS 10.0, *)) { configuration.mediaTypesRequiringUserActionForPlayback = WKAudiovisualMediaTypeNone; + } else if (@available(iOS 9.0, *)) { + configuration.requiresUserActionForMediaPlayback = false; } else { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" configuration.mediaPlaybackRequiresUserAction = false; +#pragma clang diagnostic pop } break; default: diff --git a/packages/webview_flutter/ios/Classes/JavaScriptChannelHandler.h b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.h similarity index 87% rename from packages/webview_flutter/ios/Classes/JavaScriptChannelHandler.h rename to packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.h index 1e0a9f2fe9d6..a0a5ec657295 100644 --- a/packages/webview_flutter/ios/Classes/JavaScriptChannelHandler.h +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.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/webview_flutter/ios/Classes/JavaScriptChannelHandler.m b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.m similarity index 95% rename from packages/webview_flutter/ios/Classes/JavaScriptChannelHandler.m rename to packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.m index 5bafd8c715dd..ec9a363a4b2e 100644 --- a/packages/webview_flutter/ios/Classes/JavaScriptChannelHandler.m +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/Classes/JavaScriptChannelHandler.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/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec b/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec new file mode 100644 index 000000000000..37905f147489 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/ios/webview_flutter_wkwebview.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# +Pod::Spec.new do |s| + s.name = 'webview_flutter_wkwebview' + s.version = '0.0.1' + s.summary = 'A WebView Plugin for Flutter.' + s.description = <<-DESC +A Flutter plugin that provides a WebView widget. +Downloaded by pub (not CocoaPods). + DESC + s.homepage = 'https://github.com/flutter/plugins' + s.license = { :type => 'BSD', :file => '../LICENSE' } + s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } + s.source = { :http => 'https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_wkwebview' } + s.documentation_url = 'https://pub.dev/packages/webview_flutter' + s.source_files = 'Classes/**/*' + s.public_header_files = 'Classes/**/*.h' + s.dependency 'Flutter' + + s.platform = :ios, '8.0' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } +end diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webview_cupertino.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webview_cupertino.dart new file mode 100644 index 000000000000..05b79d0a72e4 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/src/webview_cupertino.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 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:webview_flutter_platform_interface/webview_flutter_platform_interface.dart'; + +/// Builds an iOS webview. +/// +/// This is used as the default implementation for [WebView.platform] on iOS. It uses +/// a [UiKitView] to embed the webview in the widget hierarchy, and uses a method channel to +/// communicate with the platform code. +class CupertinoWebView implements WebViewPlatform { + @override + Widget build({ + required BuildContext context, + required CreationParams creationParams, + required WebViewPlatformCallbacksHandler webViewPlatformCallbacksHandler, + required JavascriptChannelRegistry javascriptChannelRegistry, + WebViewPlatformCreatedCallback? onWebViewPlatformCreated, + Set>? gestureRecognizers, + }) { + return UiKitView( + viewType: 'plugins.flutter.io/webview', + onPlatformViewCreated: (int id) { + if (onWebViewPlatformCreated == null) { + return; + } + onWebViewPlatformCreated(MethodChannelWebViewPlatform( + id, + webViewPlatformCallbacksHandler, + javascriptChannelRegistry, + )); + }, + gestureRecognizers: gestureRecognizers, + creationParams: + MethodChannelWebViewPlatform.creationParamsToMap(creationParams), + creationParamsCodec: const StandardMessageCodec(), + ); + } + + @override + Future clearCookies() => MethodChannelWebViewPlatform.clearCookies(); +} diff --git a/packages/webview_flutter/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart b/packages/webview_flutter/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart new file mode 100644 index 000000000000..bbec415dccd0 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/lib/webview_flutter_wkwebview.dart @@ -0,0 +1,5 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export 'src/webview_cupertino.dart'; diff --git a/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml new file mode 100644 index 000000000000..a7305cea7a94 --- /dev/null +++ b/packages/webview_flutter/webview_flutter_wkwebview/pubspec.yaml @@ -0,0 +1,29 @@ +name: webview_flutter_wkwebview +description: A Flutter plugin that provides a WebView widget based on Apple's WKWebView control. +repository: https://github.com/flutter/plugins/tree/master/packages/webview_flutter/webview_flutter_wkwebview +issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+webview%22 +version: 2.0.14 + +environment: + sdk: ">=2.14.0 <3.0.0" + flutter: ">=2.5.0" + +flutter: + plugin: + implements: webview_flutter + platforms: + ios: + pluginClass: FLTWebViewFlutterPlugin + +dependencies: + flutter: + sdk: flutter + + webview_flutter_platform_interface: ^1.0.0 + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + pedantic: ^1.10.0 \ No newline at end of file diff --git a/packages/wifi_info_flutter/analysis_options.yaml b/packages/wifi_info_flutter/analysis_options.yaml new file mode 100644 index 000000000000..cda4f6e153e6 --- /dev/null +++ b/packages/wifi_info_flutter/analysis_options.yaml @@ -0,0 +1 @@ +include: ../../analysis_options_legacy.yaml diff --git a/packages/wifi_info_flutter/wifi_info_flutter/.metadata b/packages/wifi_info_flutter/wifi_info_flutter/.metadata new file mode 100644 index 000000000000..e40242b8f94f --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/.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: plugin diff --git a/packages/wifi_info_flutter/wifi_info_flutter/AUTHORS b/packages/wifi_info_flutter/wifi_info_flutter/AUTHORS new file mode 100644 index 000000000000..493a0b4ef9c2 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_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/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md b/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md new file mode 100644 index 000000000000..3d5599743710 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/CHANGELOG.md @@ -0,0 +1,38 @@ +## NEXT + +* Updated Android lint settings. +* Updated package description. + +## 2.0.2 + +* Update README to point to Plus Plugins version. + +## 2.0.1 + +* Migrate maven repository from jcenter to mavenCentral. + +## 2.0.0 + +* Migrate to null safety. + +## 1.0.4 + +* Android: Add Log warning for unsatisfied requirement(s) in Android P or higher. +* Android: Update Example project. + +## 1.0.3 + +* Fix README example. + +## 1.0.2 + +* Update Flutter SDK constraint. + +## 1.0.1 + +* Fixed method channel name in android implementation. [Issue](https://github.com/flutter/flutter/issues/69073). + +## 1.0.0 + +* Initial release of the plugin. This plugin retrieves information about a device's connection to wifi. +* See [README](./README.md) for details. diff --git a/packages/wifi_info_flutter/wifi_info_flutter/LICENSE b/packages/wifi_info_flutter/wifi_info_flutter/LICENSE new file mode 100644 index 000000000000..c6823b81eb84 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/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/README.md b/packages/wifi_info_flutter/wifi_info_flutter/README.md new file mode 100644 index 000000000000..3f1084eb1d2c --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/README.md @@ -0,0 +1,86 @@ +# wifi_info_flutter + +--- + +## Deprecation Notice + +This plugin has been replaced by the [Flutter Community Plus +Plugins](https://plus.fluttercommunity.dev/) version, +[`network_info_plus`](https://pub.dev/packages/network_info_plus). +No further updates are planned to this plugin, and we encourage all users to +migrate to the Plus version. + +Critical fixes (e.g., for any security incidents) will be provided through the +end of 2021, at which point this package will be marked as discontinued. + +--- + +This plugin retrieves information about a device's connection to wifi. + +> Note that on Android, this does not guarantee connection to Internet. For instance, +the app might have wifi access but it might be a VPN or a hotel WiFi with no access. + +## Usage + +### Android + +Sample usage to check current status: + +To successfully get WiFi Name or Wi-Fi BSSID starting with Android O, ensure all of the following conditions are met: + + * If your app is targeting Android 10 (API level 29) SDK or higher, your app has the ACCESS_FINE_LOCATION permission. + + * If your app is targeting SDK lower than Android 10 (API level 29), your app has the ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION permission. + + * Location services are enabled on the device (under Settings > Location). + +You can get wi-fi related information using: + +```dart +import 'package:wifi_info_flutter/wifi_info_flutter.dart'; + +var wifiBSSID = await WifiInfo().getWifiBSSID(); +var wifiIP = await WifiInfo().getWifiIP(); +var wifiName = await WifiInfo().getWifiName(); +``` + +### iOS 12 + +To use `.getWifiBSSID()` and `.getWifiName()` on iOS >= 12, the `Access WiFi information capability` in XCode must be enabled. Otherwise, both methods will return null. + +### iOS 13 + +The methods `.getWifiBSSID()` and `.getWifiName()` utilize the [`CNCopyCurrentNetworkInfo`](https://developer.apple.com/documentation/systemconfiguration/1614126-cncopycurrentnetworkinfo) function on iOS. + +As of iOS 13, Apple announced that these APIs will no longer return valid information. +An app linked against iOS 12 or earlier receives pseudo-values such as: + + * SSID: "Wi-Fi" or "WLAN" ("WLAN" will be returned for the China SKU). + + * BSSID: "00:00:00:00:00:00" + +An app linked against iOS 13 or later receives `null`. + +The `CNCopyCurrentNetworkInfo` will work for Apps that: + + * The app uses Core Location, and has the user’s authorization to use location information. + + * The app uses the NEHotspotConfiguration API to configure the current Wi-Fi network. + + * The app has active VPN configurations installed. + +If your app falls into the last two categories, it will work as it is. If your app doesn't fall into the last two categories, +and you still need to access the wifi information, you should request user's authorization to use location information. + +There is a helper method provided in this plugin to request the location authorization: `requestLocationServiceAuthorization`. +To request location authorization, make sure to add the following keys to your _Info.plist_ file, located in `/ios/Runner/Info.plist`: + +* `NSLocationAlwaysAndWhenInUseUsageDescription` - describe why the app needs access to the user’s location information all the time (foreground and background). This is called _Privacy - Location Always and When In Use Usage Description_ in the visual editor. +* `NSLocationWhenInUseUsageDescription` - describe why the app needs access to the user’s location information when the app is running in the foreground. This is called _Privacy - Location When In Use Usage Description_ in the visual editor. + +## Getting Started + +For help getting started with Flutter, view our online +[documentation](http://flutter.io/). + +For help on editing plugin code, view the [documentation](https://flutter.io/platform-plugins/#edit-code). diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle b/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle new file mode 100644 index 000000000000..661ee82da4d0 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/android/build.gradle @@ -0,0 +1,47 @@ +group 'io.flutter.plugins.wifi_info_flutter' +version '1.0' + +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:3.5.0' + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion 29 + + defaultConfig { + minSdkVersion 16 + } + lintOptions { + disable 'InvalidPackage' + disable 'GradleDependency' + } + + + testOptions { + unitTests.includeAndroidResources = true + unitTests.returnDefaultValues = true + unitTests.all { + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/gradle/wrapper/gradle-wrapper.properties b/packages/wifi_info_flutter/wifi_info_flutter/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000000..01a286e96a21 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/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/wifi_info_flutter/wifi_info_flutter/android/settings.gradle b/packages/wifi_info_flutter/wifi_info_flutter/android/settings.gradle new file mode 100644 index 000000000000..ec0e24958ea9 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'wifi_info_flutter' diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/AndroidManifest.xml b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000000..03ac924f9427 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutter.java b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutter.java new file mode 100644 index 000000000000..bd4c8f10ce3b --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutter.java @@ -0,0 +1,157 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.wifi_info_flutter; + +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import android.location.LocationManager; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.os.Build; +import androidx.core.content.ContextCompat; +import io.flutter.Log; + +/** Reports wifi information. */ +class WifiInfoFlutter { + private WifiManager wifiManager; + private Context context; + private static final String TAG = "WifiInfoFlutter"; + + WifiInfoFlutter(WifiManager wifiManager, Context context) { + this.wifiManager = wifiManager; + this.context = context; + } + + String getWifiName() { + if (!checkPermissions()) { + return null; + } + final WifiInfo wifiInfo = getWifiInfo(); + String ssid = null; + if (wifiInfo != null) ssid = wifiInfo.getSSID(); + if (ssid != null) ssid = ssid.replaceAll("\"", ""); // Android returns "SSID" + if (ssid != null && ssid.equals("")) ssid = null; + return ssid; + } + + String getWifiBSSID() { + if (!checkPermissions()) { + return null; + } + final WifiInfo wifiInfo = getWifiInfo(); + String bssid = null; + if (wifiInfo != null) { + bssid = wifiInfo.getBSSID(); + } + return bssid; + } + + String getWifiIPAddress() { + WifiInfo wifiInfo = null; + if (wifiManager != null) wifiInfo = wifiManager.getConnectionInfo(); + + String ip = null; + int i_ip = 0; + if (wifiInfo != null) i_ip = wifiInfo.getIpAddress(); + + if (i_ip != 0) + ip = + String.format( + "%d.%d.%d.%d", + (i_ip & 0xff), (i_ip >> 8 & 0xff), (i_ip >> 16 & 0xff), (i_ip >> 24 & 0xff)); + + return ip; + } + + private WifiInfo getWifiInfo() { + return wifiManager == null ? null : wifiManager.getConnectionInfo(); + } + + private Boolean checkPermissions() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return true; + } + + boolean grantedChangeWifiState = + ContextCompat.checkSelfPermission(context, Manifest.permission.CHANGE_WIFI_STATE) + == PackageManager.PERMISSION_GRANTED; + + boolean grantedAccessFine = + ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) + == PackageManager.PERMISSION_GRANTED; + + boolean grantedAccessCoarse = + ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) + == PackageManager.PERMISSION_GRANTED; + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P + && !grantedChangeWifiState + && !grantedAccessFine + && !grantedAccessCoarse) { + Log.w( + TAG, + "Attempted to get Wi-Fi data that requires additional permission(s).\n" + + "To successfully get WiFi Name or Wi-Fi BSSID starting with Android O, please ensure your app has one of the following permissions:\n" + + "- CHANGE_WIFI_STATE\n" + + "- ACCESS_FINE_LOCATION\n" + + "- ACCESS_COARSE_LOCATION\n" + + "For more information about Wi-Fi Restrictions in Android 8.0 and above, please consult the following link:\n" + + "https://developer.android.com/guide/topics/connectivity/wifi-scan"); + return false; + } + + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P && !grantedChangeWifiState) { + Log.w( + TAG, + "Attempted to get Wi-Fi data that requires additional permission(s).\n" + + "To successfully get WiFi Name or Wi-Fi BSSID starting with Android P, please ensure your app has the CHANGE_WIFI_STATE permission.\n" + + "For more information about Wi-Fi Restrictions in Android 9.0 and above, please consult the following link:\n" + + "https://developer.android.com/guide/topics/connectivity/wifi-scan"); + return false; + } + + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.P + && !grantedAccessFine + && !grantedAccessCoarse) { + Log.w( + TAG, + "Attempted to get Wi-Fi data that requires additional permission(s).\n" + + "To successfully get WiFi Name or Wi-Fi BSSID starting with Android P, additional to CHANGE_WIFI_STATE please ensure your app has one of the following permissions too:\n" + + "- ACCESS_FINE_LOCATION\n" + + "- ACCESS_COARSE_LOCATION\n" + + "For more information about Wi-Fi Restrictions in Android 9.0 and above, please consult the following link:\n" + + "https://developer.android.com/guide/topics/connectivity/wifi-scan"); + return false; + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q + && (!grantedAccessFine || !grantedChangeWifiState)) { + Log.w( + TAG, + "Attempted to get Wi-Fi data that requires additional permission(s).\n" + + "To successfully get WiFi Name or Wi-Fi BSSID starting with Android Q, please ensure your app has the CHANGE_WIFI_STATE and ACCESS_FINE_LOCATION permission.\n" + + "For more information about Wi-Fi Restrictions in Android 10.0 and above, please consult the following link:\n" + + "https://developer.android.com/guide/topics/connectivity/wifi-scan"); + return false; + } + + LocationManager locationManager = + (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); + + boolean gpsEnabled = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && !gpsEnabled) { + Log.w( + TAG, + "Attempted to get Wi-Fi data that requires additional permission(s).\n" + + "To successfully get WiFi Name or Wi-Fi BSSID starting with Android P, please ensure Location services are enabled on the device (under Settings > Location).\n" + + "For more information about Wi-Fi Restrictions in Android 9.0 and above, please consult the following link:\n" + + "https://developer.android.com/guide/topics/connectivity/wifi-scan"); + return false; + } + return true; + } +} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterMethodChannelHandler.java b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterMethodChannelHandler.java new file mode 100644 index 000000000000..9ceed5968e63 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterMethodChannelHandler.java @@ -0,0 +1,44 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.wifi_info_flutter; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +/** + * The handler receives {@link MethodCall}s from the UIThread, gets the related information from + * a @{@link WifiInfoFlutter}, and then send the result back to the UIThread through the {@link + * MethodChannel.Result}. + */ +class WifiInfoFlutterMethodChannelHandler implements MethodChannel.MethodCallHandler { + private WifiInfoFlutter wifiInfoFlutter; + + /** + * Construct the WifiInfoFlutterMethodChannelHandler with a {@code wifiInfoFlutter}. The {@code + * wifiInfoFlutter} must not be null. + */ + WifiInfoFlutterMethodChannelHandler(WifiInfoFlutter wifiInfoFlutter) { + assert (wifiInfoFlutter != null); + this.wifiInfoFlutter = wifiInfoFlutter; + } + + @Override + public void onMethodCall(MethodCall call, MethodChannel.Result result) { + switch (call.method) { + case "wifiName": + result.success(wifiInfoFlutter.getWifiName()); + break; + case "wifiBSSID": + result.success(wifiInfoFlutter.getWifiBSSID()); + break; + case "wifiIPAddress": + result.success(wifiInfoFlutter.getWifiIPAddress()); + break; + default: + result.notImplemented(); + break; + } + } +} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterPlugin.java b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterPlugin.java new file mode 100644 index 000000000000..7757688bc9fa --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/android/src/main/java/io/flutter/plugins/wifi_info_flutter/WifiInfoFlutterPlugin.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.wifi_info_flutter; + +import android.content.Context; +import android.net.wifi.WifiManager; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodChannel; + +/** WifiInfoFlutterPlugin */ +public class WifiInfoFlutterPlugin implements FlutterPlugin { + private MethodChannel methodChannel; + + /** Plugin registration. */ + @SuppressWarnings("deprecation") + public static void registerWith(io.flutter.plugin.common.PluginRegistry.Registrar registrar) { + WifiInfoFlutterPlugin plugin = new WifiInfoFlutterPlugin(); + plugin.setupChannels(registrar.messenger(), registrar.context()); + } + + @Override + public void onAttachedToEngine(FlutterPluginBinding binding) { + setupChannels(binding.getBinaryMessenger(), binding.getApplicationContext()); + } + + @Override + public void onDetachedFromEngine(FlutterPluginBinding binding) { + methodChannel.setMethodCallHandler(null); + methodChannel = null; + } + + private void setupChannels(BinaryMessenger messenger, Context context) { + methodChannel = new MethodChannel(messenger, "plugins.flutter.io/wifi_info_flutter"); + final WifiManager wifiManager = + (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE); + + final WifiInfoFlutter wifiInfoFlutter = new WifiInfoFlutter(wifiManager, context); + + final WifiInfoFlutterMethodChannelHandler methodChannelHandler = + new WifiInfoFlutterMethodChannelHandler(wifiInfoFlutter); + methodChannel.setMethodCallHandler(methodChannelHandler); + } +} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/.metadata b/packages/wifi_info_flutter/wifi_info_flutter/example/.metadata new file mode 100644 index 000000000000..8407594c70b4 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/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: 4513e96a3022d70aa7686906c2e9bdfbbc448334 + channel: master + +project_type: app diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/README.md b/packages/wifi_info_flutter/wifi_info_flutter/example/README.md new file mode 100644 index 000000000000..30c38ad1ba92 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/README.md @@ -0,0 +1,16 @@ +# wifi_info_flutter_example + +Demonstrates how to use the wifi_info_flutter plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/build.gradle b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/build.gradle new file mode 100644 index 000000000000..86cf517168ef --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/build.gradle @@ -0,0 +1,54 @@ +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterRoot = localProperties.getProperty('flutter.sdk') +if (flutterRoot == null) { + throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +apply plugin: 'com.android.application' +apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" + +android { + compileSdkVersion 29 + + lintOptions { + disable 'InvalidPackage' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "io.flutter.plugins.wifi_info_flutter_example" + minSdkVersion 16 + targetSdkVersion 29 + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/debug/AndroidManifest.xml b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 000000000000..357602a1d503 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + 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/path_provider/path_provider_macos/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/path_provider/path_provider_macos/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/path_provider/path_provider_macos/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/path_provider/path_provider_macos/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/path_provider/path_provider_macos/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/path_provider/path_provider_macos/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/path_provider/path_provider_macos/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/path_provider/path_provider_macos/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/path_provider/path_provider_macos/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/path_provider/path_provider_macos/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/webview_flutter/example/android/gradle.properties b/packages/wifi_info_flutter/wifi_info_flutter/example/android/gradle.properties similarity index 100% rename from packages/webview_flutter/example/android/gradle.properties rename to packages/wifi_info_flutter/wifi_info_flutter/example/android/gradle.properties diff --git a/packages/ios_platform_images/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/wifi_info_flutter/wifi_info_flutter/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/ios_platform_images/example/android/gradle/wrapper/gradle-wrapper.properties rename to packages/wifi_info_flutter/wifi_info_flutter/example/android/gradle/wrapper/gradle-wrapper.properties 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/espresso/example/ios/Flutter/Debug.xcconfig b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/espresso/example/ios/Flutter/Debug.xcconfig rename to packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/Debug.xcconfig diff --git a/packages/espresso/example/ios/Flutter/Release.xcconfig b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/espresso/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..98b6089f0d68 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,545 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 78A2B22AE5FB53474D0E7B48 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5304CE43F05781426D604828 /* libPods-Runner.a */; }; + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; + 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 11F3C35054888B3724893A22 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1E5A9EB282D46A445314F9FD /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 2BADCFEAF6163E1D252C8765 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 5304CE43F05781426D604828 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 78A2B22AE5FB53474D0E7B48 /* libPods-Runner.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 53534922C743E29B902DE7D2 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 5304CE43F05781426D604828 /* libPods-Runner.a */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 9938C0E7E0C974A660788A69 /* Pods */, + 53534922C743E29B902DE7D2 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, + 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 97C146F11CF9000F007C117D /* Supporting Files */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + ); + path = Runner; + sourceTree = ""; + }; + 97C146F11CF9000F007C117D /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 97C146F21CF9000F007C117D /* main.m */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 9938C0E7E0C974A660788A69 /* Pods */ = { + isa = PBXGroup; + children = ( + 1E5A9EB282D46A445314F9FD /* Pods-Runner.debug.xcconfig */, + 2BADCFEAF6163E1D252C8765 /* Pods-Runner.release.xcconfig */, + 11F3C35054888B3724893A22 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 77C985AE0A130001A78D36BE /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 77C985AE0A130001A78D36BE /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, + 97C146F31CF9000F007C117D /* main.m in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.wifiInfoFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.wifiInfoFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + ENABLE_BITCODE = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + LIBRARY_SEARCH_PATHS = ( + "$(inherited)", + "$(PROJECT_DIR)/Flutter", + ); + PRODUCT_BUNDLE_IDENTIFIER = dev.flutter.plugins.wifiInfoFlutterExample; + PRODUCT_NAME = "$(TARGET_NAME)"; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000000..919434a6254f --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + 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/webview_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/webview_flutter/example/ios/Runner.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/webview_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/wifi_info_flutter/wifi_info_flutter/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from packages/webview_flutter/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/espresso/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/espresso/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/webview_flutter/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/webview_flutter/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/webview_flutter/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/webview_flutter/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/webview_flutter/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/webview_flutter/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/webview_flutter/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/webview_flutter/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/webview_flutter/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/webview_flutter/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/in_app_purchase/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/in_app_purchase/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/flutter_plugin_android_lifecycle/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/flutter_plugin_android_lifecycle/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/e2e/ios/Assets/.gitkeep b/packages/wifi_info_flutter/wifi_info_flutter/ios/Assets/.gitkeep similarity index 100% rename from packages/e2e/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..b1e1e756c633 --- /dev/null +++ b/packages/wifi_info_flutter/wifi_info_flutter/pubspec.yaml @@ -0,0 +1,27 @@ +name: wifi_info_flutter +description: A Flutter plugin to get WiFi information such as connection status and network identifiers. +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 deleted file mode 100755 index 0d670af88814..000000000000 --- a/script/build_all_plugins_app.sh +++ /dev/null @@ -1,71 +0,0 @@ -#!/bin/bash - -# 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 - -readonly EXCLUDED_PLUGINS_LIST=( - "connectivity_macos" - "connectivity_platform_interface" - "connectivity_web" - "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_macos" - "path_provider_platform_interface" - "path_provider_web" - "plugin_platform_interface" - "shared_preferences_macos" - "shared_preferences_platform_interface" - "shared_preferences_web" - "shared_preferences_windows" - "url_launcher_macos" - "url_launcher_platform_interface" - "url_launcher_web" - "video_player_platform_interface" - "video_player_web" -) -# 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) - -function error() { - echo "$@" 1>&2 -} - -failures=0 - -for version in "debug" "release"; do - (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." - 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 - failures=$(($failures + 1)) - fi -done - -rm -rf $REPO_DIR/all_plugins/ -exit $failures 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 deleted file mode 100644 index 7950a3ea71cd..000000000000 --- a/script/common.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/bin/bash - -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 -} diff --git a/script/configs/README.md b/script/configs/README.md new file mode 100644 index 000000000000..96423cf2779b --- /dev/null +++ b/script/configs/README.md @@ -0,0 +1,8 @@ +This folder contains configuration files that are passed to commands in place +of plugin lists. They are primarily used by CI to opt specific packages out of +tests, but can also useful when running multi-plugin tests locally. + +**Any entry added to a file in this directory should include a comment**. +Skipping tests or checks for plugins is usually not something we want to do, +so should the comment should either include an issue link to the issue tracking +removing it or—much more rarely—explaining why it is a permanent exclusion. diff --git a/script/configs/custom_analysis.yaml b/script/configs/custom_analysis.yaml new file mode 100644 index 000000000000..71d8fb825add --- /dev/null +++ b/script/configs/custom_analysis.yaml @@ -0,0 +1,42 @@ +# Plugins that deliberately use their own analysis_options.yaml. +# +# This only exists to allow incrementally switching to the newer, stricter +# analysis_options.yaml based on flutter/flutter, rather than the original +# rules based on pedantic (now at analysis_options_legacy.yaml). +# +# DO NOT add new entries to the list, unless it is to push the legacy rules +# from a top-level package into more specific packages in order to incrementally +# migrate a federated plugin. +# +# DO NOT move or delete this file without updating +# https://github.com/dart-lang/sdk/blob/master/tools/bots/flutter/analyze_flutter_plugins.sh +# which references this file from source, but out-of-repo. +# Contact stuartmorgan or devoncarew for assistance if necessary. + +# TODO(ecosystem): Remove everything from this list. See: +# https://github.com/flutter/flutter/issues/76229 +- camera +- google_maps_flutter +- google_sign_in +- image_picker +- in_app_purchase +- ios_platform_images +- local_auth +- quick_actions +- shared_preferences +- url_launcher +- video_player +- webview_flutter + +# These plugins are deprecated in favor of the Community Plus versions, and +# will be removed from the repo once the critical support window has passed, +# so are not worth updating. +- android_alarm_manager +- android_intent +- battery +- connectivity +- device_info +- package_info +- sensors +- share +- wifi_info_flutter diff --git a/script/configs/exclude_all_plugins_app.yaml b/script/configs/exclude_all_plugins_app.yaml new file mode 100644 index 000000000000..8dd0fde5ef5f --- /dev/null +++ b/script/configs/exclude_all_plugins_app.yaml @@ -0,0 +1,10 @@ +# 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. + +# This is a permament entry, as it should never be a direct app dependency. +- plugin_platform_interface diff --git a/script/configs/exclude_integration_android.yaml b/script/configs/exclude_integration_android.yaml new file mode 100644 index 000000000000..d8bd10b3a36e --- /dev/null +++ b/script/configs/exclude_integration_android.yaml @@ -0,0 +1,18 @@ +# Currently missing harness files: https://github.com/flutter/flutter/issues/86749) +- camera/camera +- in_app_purchase/in_app_purchase +- in_app_purchase_android +- shared_preferences/shared_preferences +- url_launcher/url_launcher +- video_player/video_player + +# Deprecated; no plan to backfill the missing files +- android_intent +- connectivity/connectivity +- device_info/device_info +- sensors +- share +- wifi_info_flutter/wifi_info_flutter + +# No integration tests to run: +- espresso diff --git a/script/configs/exclude_integration_ios.yaml b/script/configs/exclude_integration_ios.yaml new file mode 100644 index 000000000000..e1ae6adf49cf --- /dev/null +++ b/script/configs/exclude_integration_ios.yaml @@ -0,0 +1,6 @@ +# Currently missing: https://github.com/flutter/flutter/issues/81695 +- in_app_purchase_ios +# Currently missing: https://github.com/flutter/flutter/issues/82208 +- ios_platform_images +# Hangs on CI. Deprecated, so there is no plan to fix it. +- sensors diff --git a/script/configs/exclude_integration_web.yaml b/script/configs/exclude_integration_web.yaml new file mode 100644 index 000000000000..6c0fc4efcb7a --- /dev/null +++ b/script/configs/exclude_integration_web.yaml @@ -0,0 +1,2 @@ +# Currently missing: https://github.com/flutter/flutter/issues/82211 +- file_selector diff --git a/script/configs/exclude_native_ios.yaml b/script/configs/exclude_native_ios.yaml new file mode 100644 index 000000000000..723fcfa64715 --- /dev/null +++ b/script/configs/exclude_native_ios.yaml @@ -0,0 +1,7 @@ +# Deprecated; no plan to backfill the missing files +- battery +- connectivity/connectivity +- device_info/device_info +- package_info +- sensors +- wifi_info_flutter/wifi_info_flutter diff --git a/script/configs/exclude_native_macos.yaml b/script/configs/exclude_native_macos.yaml new file mode 100644 index 000000000000..8a817a9c0178 --- /dev/null +++ b/script/configs/exclude_native_macos.yaml @@ -0,0 +1,3 @@ +# Deprecated plugins that will not be getting unit test backfill. +- connectivity_macos +- package_info diff --git a/script/configs/exclude_native_unit_android.yaml b/script/configs/exclude_native_unit_android.yaml new file mode 100644 index 000000000000..5ec80eee73a0 --- /dev/null +++ b/script/configs/exclude_native_unit_android.yaml @@ -0,0 +1,11 @@ +# Deprecated; no plan to backfill the missing files +- android_alarm_manager +- battery +- device_info/device_info +- package_info +- sensors +- share +- wifi_info_flutter/wifi_info_flutter + +# No need for unit tests: +- espresso diff --git a/script/incremental_build.sh b/script/incremental_build.sh deleted file mode 100755 index 3b0b97f0dbe6..000000000000 --- a/script/incremental_build.sh +++ /dev/null @@ -1,49 +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" - -# 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" -) -# 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)"}" -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..2e6404e2cee4 --- /dev/null +++ b/script/tool/CHANGELOG.md @@ -0,0 +1,443 @@ +## NEXT + +- `native-test --android`, `--ios`, and `--macos` now fail plugins that don't + have unit tests, rather than skipping them. +- Added a new `federation-safety-check` command to help catch changes to + federated packages that have been done in such a way that they will pass in + CI, but fail once the change is landed and published. +- `publish-check` now validates that there is an `AUTHORS` file. +- Added flags to `version-check` to allow overriding the platform interface + major version change restriction. +- Improved error handling and error messages in CHANGELOG version checks. +- `license-check` now validates Kotlin files. +- `pubspec-check` now checks that the description is of the pub-recommended + length. + +## 0.7.1 + +- Add support for `.pluginToolsConfig.yaml` in the `build-examples` command. + +## 0.7.0 + +- `native-test` now supports `--linux` for unit tests. +- Formatting now skips Dart files that contain a line that exactly + matches the string `// This file is hand-formatted.`. + +## 0.6.0+1 + +- Fixed `build-examples` to work for non-plugin packages. + +## 0.6.0 + +- Added Android native integration test support to `native-test`. +- Added a new `android-lint` command to lint Android plugin native code. +- Pubspec validation now checks for `implements` in implementation packages. +- Pubspec valitation now checks the full relative path of `repository` entries. +- `build-examples` now supports UWP plugins via a `--winuwp` flag. +- `native-test` now supports `--windows` for unit tests. +- **Breaking change**: `publish` no longer accepts `--no-tag-release` or + `--no-push-flags`. Releases now always tag and push. +- **Breaking change**: `publish`'s `--package` flag has been replaced with the + `--packages` flag used by most other packages. +- **Breaking change** Passing both `--run-on-changed-packages` and `--packages` + is now an error; previously it the former would be ignored. + +## 0.5.0 + +- `--exclude` and `--custom-analysis` now accept paths to YAML files that + contain lists of packages to exclude, in addition to just package names, + so that exclude lists can be maintained separately from scripts and CI + configuration. +- Added an `xctest` flag to select specific test targets, to allow running only + unit tests or integration tests. +- **Breaking change**: Split Xcode analysis out of `xctest` and into a new + `xcode-analyze` command. +- Fixed a bug that caused `firebase-test-lab` to hang if it tried to run more + than one plugin's tests in a single run. +- **Breaking change**: If `firebase-test-lab` is run on a package that supports + Android, but for which no tests are run, it now fails instead of skipping. + This matches `drive-examples`, as this command is what is used for driving + Android Flutter integration tests on CI. +- **Breaking change**: Replaced `xctest` with a new `native-test` command that + will eventually be able to run native unit and integration tests for all + platforms. + - Adds the ability to disable test types via `--no-unit` or + `--no-integration`. +- **Breaking change**: Replaced `java-test` with Android unit test support for + the new `native-test` command. +- Commands that print a run summary at the end now track and log exclusions + similarly to skips for easier auditing. +- `version-check` now validates that `NEXT` is not present when changing + the version. + +## 0.4.1 + +- Improved `license-check` output. +- Use `java -version` rather than `java --version`, for compatibility with more + versions of Java. + +## 0.4.0 + +- Modified the output format of many commands +- **Breaking change**: `firebase-test-lab` no longer supports `*_e2e.dart` + files, only `integration_test/*_test.dart`. +- Add a summary to the end of successful command runs for commands using the + new output format. +- Fixed some cases where a failure in a command for a single package would + immediately abort the test. +- Deprecated `--plugins` in favor of new `--packages`. `--plugins` continues to + work for now, but will be removed in the future. +- Make `drive-examples` device detection robust against Flutter tool banners. +- `format` is now supported on Windows. + +## 0.3.0 + +- 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. +- **Breaking change**: `build-examples` for iOS now uses `--ios` rather than + `--ipa`. +- The tooling now runs in strong null-safe mode. +- `publish plugins` check against pub.dev to determine if a release should happen. +- Modified the output format of many commands +- Removed `podspec`'s `--skip` in favor of `--ignore` using the new structure. + +## 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..1a87f098757b --- /dev/null +++ b/script/tool/README.md @@ -0,0 +1,134 @@ +# 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/bin/flutter_plugin_tools.dart +``` + +### Published Version + +Set up: + +```sh +dart pub global activate flutter_plugin_tools +``` + +Run: + +```sh +dart pub global run flutter_plugin_tools +``` + +## Commands + +Run with `--help` for a full list of commands and arguments, but the +following shows a number of common commands being run for a specific 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/bin/flutter_plugin_tools.dart format --packages plugin_name +``` + +### Run the Dart Static Analyzer + +```sh +cd +dart run ./script/tool/bin/flutter_plugin_tools.dart analyze --packages plugin_name +``` + +### Run Dart Unit Tests + +```sh +cd +dart run ./script/tool/bin/flutter_plugin_tools.dart test --packages plugin_name +``` + +### Run Dart Integration Tests + +```sh +cd +dart run ./script/tool/bin/flutter_plugin_tools.dart build-examples --packages plugin_name +dart run ./script/tool/bin/flutter_plugin_tools.dart drive-examples --packages plugin_name +``` + +### Run Native Tests + +`native-test` takes one or more platform flags to run tests for. By default it +runs both unit tests and (on platforms that support it) integration tests, but +`--no-unit` or `--no-integration` can be used to run just one type. + +Examples: + +```sh +cd +# Run just unit tests for iOS and Android: +dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --ios --android --no-integration --packages plugin_name +# Run all tests for macOS: +dart run ./script/tool/bin/flutter_plugin_tools.dart native-test --macos --packages plugin_name +``` + +### Publish a Release + +``sh +cd +git checkout +dart run ./script/tool/bin/flutter_plugin_tools.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/bin/flutter_plugin_tools.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..faad7f4736eb --- /dev/null +++ b/script/tool/lib/src/analyze_command.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:async'; + +import 'package:file/file.dart'; +import 'package:platform/platform.dart'; +import 'package:yaml/yaml.dart'; + +import 'common/core.dart'; +import 'common/package_looping_command.dart'; +import 'common/plugin_command.dart'; +import 'common/process_runner.dart'; +import 'common/repository_package.dart'; + +const int _exitPackagesGetFailed = 3; + +/// A command to run Dart analysis on packages. +class AnalyzeCommand extends PackageLoopingCommand { + /// Creates a analysis command instance. + AnalyzeCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + }) : super(packagesDir, processRunner: processRunner, platform: platform) { + argParser.addMultiOption(_customAnalysisFlag, + help: + 'Directories (comma separated) that are allowed to have their own ' + 'analysis options.\n\n' + 'Alternately, a list of one or more YAML files that contain a list ' + 'of allowed directories.', + 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'; + + late String _dartBinaryPath; + + Set _allowedCustomAnalysisDirectories = const {}; + + @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 + final bool hasLongOutput = false; + + /// Checks that there are no unexpected analysis_options.yaml files. + bool _hasUnexpecetdAnalysisOptions(RepositoryPackage package) { + final List files = + package.directory.listSync(recursive: true); + for (final FileSystemEntity file in files) { + if (file.basename != 'analysis_options.yaml' && + file.basename != '.analysis_options') { + continue; + } + + final bool allowed = _allowedCustomAnalysisDirectories.any( + (String directory) => + directory.isNotEmpty && + path.isWithin( + packagesDir.childDirectory(directory).path, file.path)); + if (allowed) { + continue; + } + + printError( + 'Found an extra analysis_options.yaml at ${file.absolute.path}.'); + printError( + 'If this was deliberate, pass the package to the analyze command ' + 'with the --$_customAnalysisFlag flag and try again.'); + return true; + } + return false; + } + + /// Ensures that the dependent packages have been fetched for all packages + /// (including their sub-packages) that will be analyzed. + Future _runPackagesGetOnTargetPackages() async { + final List packageDirectories = + await getTargetPackagesAndSubpackages() + .map((PackageEnumerationEntry entry) => entry.package.directory) + .toList(); + final Set packagePaths = + packageDirectories.map((Directory dir) => dir.path).toSet(); + packageDirectories.removeWhere((Directory directory) { + // Remove the 'example' subdirectories; 'flutter packages get' + // automatically runs 'pub get' there as part of handling the parent + // directory. + return directory.basename == 'example' && + packagePaths.contains(directory.parent.path); + }); + for (final Directory package in packageDirectories) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, ['packages', 'get'], + workingDir: package); + if (exitCode != 0) { + return false; + } + } + return true; + } + + @override + Future initializeRun() async { + print('Fetching dependencies...'); + if (!await _runPackagesGetOnTargetPackages()) { + printError('Unable to get dependencies.'); + throw ToolExit(_exitPackagesGetFailed); + } + + _allowedCustomAnalysisDirectories = + getStringListArg(_customAnalysisFlag).expand((String item) { + if (item.endsWith('.yaml')) { + final File file = packagesDir.fileSystem.file(item); + return (loadYaml(file.readAsStringSync()) as YamlList) + .toList() + .cast(); + } + return [item]; + }).toSet(); + + // Use the Dart SDK override if one was passed in. + final String? dartSdk = argResults![_analysisSdk] as String?; + _dartBinaryPath = + dartSdk == null ? 'dart' : path.join(dartSdk, 'bin', 'dart'); + } + + @override + Future runForPackage(RepositoryPackage package) async { + if (_hasUnexpecetdAnalysisOptions(package)) { + return PackageResult.fail(['Unexpected local analysis options']); + } + final int exitCode = await processRunner.runAndStream( + _dartBinaryPath, ['analyze', '--fatal-infos'], + workingDir: package.directory); + if (exitCode != 0) { + return PackageResult.fail(); + } + return PackageResult.success(); + } +} 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..82ed074c462a --- /dev/null +++ b/script/tool/lib/src/build_examples_command.dart @@ -0,0 +1,352 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; + +import 'package:file/file.dart'; +import 'package:platform/platform.dart'; +import 'package:yaml/yaml.dart'; + +import 'common/core.dart'; +import 'common/package_looping_command.dart'; +import 'common/plugin_utils.dart'; +import 'common/process_runner.dart'; +import 'common/repository_package.dart'; + +/// Key for APK. +const String _platformFlagApk = 'apk'; + +const String _pluginToolsConfigFileName = '.pluginToolsConfig.yaml'; +const String _pluginToolsConfigBuildFlagsKey = 'buildFlags'; +const String _pluginToolsConfigGlobalKey = 'global'; + +const String _pluginToolsConfigExample = ''' +$_pluginToolsConfigBuildFlagsKey: + $_pluginToolsConfigGlobalKey: + - "--no-tree-shake-icons" + - "--dart-define=buildmode=testing" +'''; + +const int _exitNoPlatformFlags = 3; +const int _exitInvalidPluginToolsConfig = 4; + +// Flutter build types. These are the values passed to `flutter build `. +const String _flutterBuildTypeAndroid = 'apk'; +const String _flutterBuildTypeIos = 'ios'; +const String _flutterBuildTypeLinux = 'linux'; +const String _flutterBuildTypeMacOS = 'macos'; +const String _flutterBuildTypeWeb = 'web'; +const String _flutterBuildTypeWin32 = 'windows'; +const String _flutterBuildTypeWinUwp = 'winuwp'; + +/// A command to build the example applications for packages. +class BuildExamplesCommand extends PackageLoopingCommand { + /// Creates an instance of the build command. + BuildExamplesCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + }) : super(packagesDir, processRunner: processRunner, platform: platform) { + argParser.addFlag(kPlatformLinux); + argParser.addFlag(kPlatformMacos); + argParser.addFlag(kPlatformWeb); + argParser.addFlag(kPlatformWindows); + argParser.addFlag(kPlatformWinUwp); + argParser.addFlag(kPlatformIos); + argParser.addFlag(_platformFlagApk); + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: 'Enables the given Dart SDK experiments.', + ); + } + + // Maps the switch this command uses to identify a platform to information + // about it. + static final Map _platforms = + { + _platformFlagApk: const _PlatformDetails( + 'Android', + pluginPlatform: kPlatformAndroid, + flutterBuildType: _flutterBuildTypeAndroid, + ), + kPlatformIos: const _PlatformDetails( + 'iOS', + pluginPlatform: kPlatformIos, + flutterBuildType: _flutterBuildTypeIos, + extraBuildFlags: ['--no-codesign'], + ), + kPlatformLinux: const _PlatformDetails( + 'Linux', + pluginPlatform: kPlatformLinux, + flutterBuildType: _flutterBuildTypeLinux, + ), + kPlatformMacos: const _PlatformDetails( + 'macOS', + pluginPlatform: kPlatformMacos, + flutterBuildType: _flutterBuildTypeMacOS, + ), + kPlatformWeb: const _PlatformDetails( + 'web', + pluginPlatform: kPlatformWeb, + flutterBuildType: _flutterBuildTypeWeb, + ), + kPlatformWindows: const _PlatformDetails( + 'Win32', + pluginPlatform: kPlatformWindows, + pluginPlatformVariant: platformVariantWin32, + flutterBuildType: _flutterBuildTypeWin32, + ), + kPlatformWinUwp: const _PlatformDetails( + 'UWP', + pluginPlatform: kPlatformWindows, + pluginPlatformVariant: platformVariantWinUwp, + flutterBuildType: _flutterBuildTypeWinUwp, + ), + }; + + @override + final String name = 'build-examples'; + + @override + final String description = + 'Builds all example apps (IPA for iOS and APK for Android).\n\n' + 'This command requires "flutter" to be in your path.\n\n' + 'A $_pluginToolsConfigFileName file can be placed in an example app ' + 'directory to specify additional build arguments. It should be a YAML ' + 'file with a top-level map containing a single key ' + '"$_pluginToolsConfigBuildFlagsKey" containing a map containing a ' + 'single key "$_pluginToolsConfigGlobalKey" containing a list of build ' + 'arguments.'; + + @override + Future initializeRun() async { + final List platformFlags = _platforms.keys.toList(); + platformFlags.sort(); + if (!platformFlags.any((String platform) => getBoolArg(platform))) { + printError( + 'None of ${platformFlags.map((String platform) => '--$platform').join(', ')} ' + 'were specified. At least one platform must be provided.'); + throw ToolExit(_exitNoPlatformFlags); + } + } + + @override + Future runForPackage(RepositoryPackage package) async { + final List errors = []; + + final bool isPlugin = isFlutterPlugin(package); + final Iterable<_PlatformDetails> requestedPlatforms = _platforms.entries + .where( + (MapEntry entry) => getBoolArg(entry.key)) + .map((MapEntry entry) => entry.value); + + // Platform support is checked at the package level for plugins; there is + // no package-level platform information for non-plugin packages. + final Set<_PlatformDetails> buildPlatforms = isPlugin + ? requestedPlatforms + .where((_PlatformDetails platform) => pluginSupportsPlatform( + platform.pluginPlatform, package, + variant: platform.pluginPlatformVariant)) + .toSet() + : requestedPlatforms.toSet(); + + String platformDisplayList(Iterable<_PlatformDetails> platforms) { + return platforms.map((_PlatformDetails p) => p.label).join(', '); + } + + if (buildPlatforms.isEmpty) { + final String unsupported = requestedPlatforms.length == 1 + ? '${requestedPlatforms.first.label} is not supported' + : 'None of [${platformDisplayList(requestedPlatforms)}] are supported'; + return PackageResult.skip('$unsupported by this plugin'); + } + print('Building for: ${platformDisplayList(buildPlatforms)}'); + + final Set<_PlatformDetails> unsupportedPlatforms = + requestedPlatforms.toSet().difference(buildPlatforms); + if (unsupportedPlatforms.isNotEmpty) { + final List skippedPlatforms = unsupportedPlatforms + .map((_PlatformDetails platform) => platform.label) + .toList(); + skippedPlatforms.sort(); + print('Skipping unsupported platform(s): ' + '${skippedPlatforms.join(', ')}'); + } + print(''); + + bool builtSomething = false; + for (final RepositoryPackage example in package.getExamples()) { + final String packageName = + getRelativePosixPath(example.directory, from: packagesDir); + + for (final _PlatformDetails platform in buildPlatforms) { + // Repo policy is that a plugin must have examples configured for all + // supported platforms. For packages, just log and skip any requested + // platform that a package doesn't have set up. + if (!isPlugin && + !example.directory + .childDirectory(platform.flutterPlatformDirectory) + .existsSync()) { + print('Skipping ${platform.label} for $packageName; not supported.'); + continue; + } + + builtSomething = true; + + String buildPlatform = platform.label; + if (platform.label.toLowerCase() != platform.flutterBuildType) { + buildPlatform += ' (${platform.flutterBuildType})'; + } + print('\nBUILDING $packageName for $buildPlatform'); + if (!await _buildExample(example, platform.flutterBuildType, + extraBuildFlags: platform.extraBuildFlags)) { + errors.add('$packageName (${platform.label})'); + } + } + } + + if (!builtSomething) { + if (isPlugin) { + errors.add('No examples found'); + } else { + return PackageResult.skip( + 'No examples found supporting requested platform(s).'); + } + } + + return errors.isEmpty + ? PackageResult.success() + : PackageResult.fail(errors); + } + + Iterable _readExtraBuildFlagsConfiguration( + Directory directory) sync* { + final File pluginToolsConfig = + directory.childFile(_pluginToolsConfigFileName); + if (pluginToolsConfig.existsSync()) { + final Object? configuration = + loadYaml(pluginToolsConfig.readAsStringSync()); + if (configuration is! YamlMap) { + printError('The $_pluginToolsConfigFileName file must be a YAML map.'); + printError( + 'Currently, the key "$_pluginToolsConfigBuildFlagsKey" is the only one that has an effect.'); + printError( + 'It must itself be a map. Currently, in that map only the key "$_pluginToolsConfigGlobalKey"'); + printError( + 'has any effect; it must contain a list of arguments to pass to the'); + printError('flutter tool.'); + printError(_pluginToolsConfigExample); + throw ToolExit(_exitInvalidPluginToolsConfig); + } + if (configuration.containsKey(_pluginToolsConfigBuildFlagsKey)) { + final Object? buildFlagsConfiguration = + configuration[_pluginToolsConfigBuildFlagsKey]; + if (buildFlagsConfiguration is! YamlMap) { + printError( + 'The $_pluginToolsConfigFileName file\'s "$_pluginToolsConfigBuildFlagsKey" key must be a map.'); + printError( + 'Currently, in that map only the key "$_pluginToolsConfigGlobalKey" has any effect; it must '); + printError( + 'contain a list of arguments to pass to the flutter tool.'); + printError(_pluginToolsConfigExample); + throw ToolExit(_exitInvalidPluginToolsConfig); + } + if (buildFlagsConfiguration.containsKey(_pluginToolsConfigGlobalKey)) { + final Object? globalBuildFlagsConfiguration = + buildFlagsConfiguration[_pluginToolsConfigGlobalKey]; + if (globalBuildFlagsConfiguration is! YamlList) { + printError( + 'The $_pluginToolsConfigFileName file\'s "$_pluginToolsConfigBuildFlagsKey" key must be a map'); + printError('whose "$_pluginToolsConfigGlobalKey" key is a list.'); + printError( + 'That list must contain a list of arguments to pass to the flutter tool.'); + printError( + 'For example, the $_pluginToolsConfigFileName file could look like:'); + printError(_pluginToolsConfigExample); + throw ToolExit(_exitInvalidPluginToolsConfig); + } + yield* globalBuildFlagsConfiguration.cast(); + } + } + } + } + + Future _buildExample( + RepositoryPackage example, + String flutterBuildType, { + List extraBuildFlags = const [], + }) async { + final String enableExperiment = getStringArg(kEnableExperiment); + + // The UWP template is not yet stable, so the UWP directory + // needs to be created on the fly with 'flutter create .' + Directory? temporaryPlatformDirectory; + if (flutterBuildType == _flutterBuildTypeWinUwp) { + final Directory uwpDirectory = example.directory.childDirectory('winuwp'); + if (!uwpDirectory.existsSync()) { + print('Creating temporary winuwp folder'); + final int exitCode = await processRunner.runAndStream(flutterCommand, + ['create', '--platforms=$kPlatformWinUwp', '.'], + workingDir: example.directory); + if (exitCode == 0) { + temporaryPlatformDirectory = uwpDirectory; + } + } + } + + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + flutterBuildType, + ...extraBuildFlags, + ..._readExtraBuildFlagsConfiguration(example.directory), + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + ], + workingDir: example.directory, + ); + + if (temporaryPlatformDirectory != null && + temporaryPlatformDirectory.existsSync()) { + print('Cleaning up ${temporaryPlatformDirectory.path}'); + temporaryPlatformDirectory.deleteSync(recursive: true); + } + + return exitCode == 0; + } +} + +/// A collection of information related to a specific platform. +class _PlatformDetails { + const _PlatformDetails( + this.label, { + required this.pluginPlatform, + this.pluginPlatformVariant, + required this.flutterBuildType, + this.extraBuildFlags = const [], + }); + + /// The name to use in output. + final String label; + + /// The key in a pubspec's platform: entry. + final String pluginPlatform; + + /// The supportedVariants key under a plugin's [pluginPlatform] entry, if + /// applicable. + final String? pluginPlatformVariant; + + /// The `flutter build` build type. + final String flutterBuildType; + + /// The Flutter platform directory name. + // In practice, this is the same as the plugin platform key for all platforms. + // If that changes, this can be adjusted. + String get flutterPlatformDirectory => pluginPlatform; + + /// Any extra flags to pass to `flutter build`. + final List extraBuildFlags; +} diff --git a/script/tool/lib/src/common/core.dart b/script/tool/lib/src/common/core.dart new file mode 100644 index 000000000000..53778eccb87f --- /dev/null +++ b/script/tool/lib/src/common/core.dart @@ -0,0 +1,106 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:colorize/colorize.dart'; +import 'package:file/file.dart'; +import 'package:yaml/yaml.dart'; + +/// The signature for a print handler for commands that allow overriding the +/// print destination. +typedef Print = void Function(Object? object); + +/// Key for APK (Android) platform. +const String kPlatformAndroid = 'android'; + +/// Key for IPA (iOS) platform. +const String kPlatformIos = 'ios'; + +/// Key for linux platform. +const String kPlatformLinux = 'linux'; + +/// Key for macos platform. +const String kPlatformMacos = 'macos'; + +/// Key for Web platform. +const String kPlatformWeb = 'web'; + +/// Key for windows platform. +/// +/// Note that this corresponds to the Win32 variant for flutter commands like +/// `build` and `run`, but is a general platform containing all Windows +/// variants for purposes of the `platform` section of a plugin pubspec). +const String kPlatformWindows = 'windows'; + +/// Key for WinUWP platform. +/// +/// Note that UWP is a platform for the purposes of flutter commands like +/// `build` and `run`, but a variant of the `windows` platform for the purposes +/// of plugin pubspecs). +const String kPlatformWinUwp = 'winuwp'; + +/// Key for Win32 variant of the Windows platform. +const String platformVariantWin32 = 'win32'; + +/// Key for UWP variant of the Windows platform. +/// +/// See the note on [kPlatformWinUwp]. +const String platformVariantWinUwp = 'uwp'; + +/// 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; + } +} + +/// Prints `successMessage` in green. +void printSuccess(String successMessage) { + print(Colorize(successMessage)..green()); +} + +/// Prints `errorMessage` in red. +void printError(String errorMessage) { + print(Colorize(errorMessage)..red()); +} + +/// Error thrown when a command needs to exit with a non-zero exit code. +/// +/// While there is no specific definition of the meaning of different non-zero +/// exit codes for this tool, commands should follow the general convention: +/// 1: The command ran correctly, but found errors. +/// 2: The command failed to run because the arguments were invalid. +/// >2: The command failed to run correctly for some other reason. Ideally, +/// each such failure should have a unique exit code within the context of +/// that command. +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; +} + +/// A exit code for [ToolExit] for a successful run that found errors. +const int exitCommandFoundErrors = 1; + +/// A exit code for [ToolExit] for a failure to run due to invalid arguments. +const int exitInvalidArguments = 2; diff --git a/script/tool/lib/src/common/file_utils.dart b/script/tool/lib/src/common/file_utils.dart new file mode 100644 index 000000000000..3c2f2f18f954 --- /dev/null +++ b/script/tool/lib/src/common/file_utils.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:file/file.dart'; + +/// Returns a [File] created by appending all but the last item in [components] +/// to [base] as subdirectories, then appending the last as a file. +/// +/// Example: +/// childFileWithSubcomponents(rootDir, ['foo', 'bar', 'baz.txt']) +/// creates a File representing /rootDir/foo/bar/baz.txt. +File childFileWithSubcomponents(Directory base, List components) { + Directory dir = base; + final String basename = components.removeLast(); + for (final String directoryName in components) { + dir = dir.childDirectory(directoryName); + } + return dir.childFile(basename); +} diff --git a/script/tool/lib/src/common/git_version_finder.dart b/script/tool/lib/src/common/git_version_finder.dart new file mode 100644 index 000000000000..1cdd2fcc409b --- /dev/null +++ b/script/tool/lib/src/common/git_version_finder.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 'dart:io' as io; + +import 'package:git/git.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:yaml/yaml.dart'; + +/// Finding diffs based on `baseGitDir` and `baseSha`. +class GitVersionFinder { + /// Constructor + GitVersionFinder(this.baseGitDir, String? baseSha) : _baseSha = 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. + 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']); + 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; + if (fileContent.trim().isEmpty) { + return null; + } + final String? versionString = loadYaml(fileContent)['version'] as String?; + return versionString == null ? null : Version.parse(versionString); + } + + /// Returns the base used to diff against. + Future getBaseSha() async { + String? baseSha = _baseSha; + 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']); + } + baseSha = (baseShaFromMergeBase.stdout as String).trim(); + _baseSha = baseSha; + return baseSha; + } +} diff --git a/script/tool/lib/src/common/gradle.dart b/script/tool/lib/src/common/gradle.dart new file mode 100644 index 000000000000..e7214bf29714 --- /dev/null +++ b/script/tool/lib/src/common/gradle.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:file/file.dart'; +import 'package:platform/platform.dart'; + +import 'process_runner.dart'; + +const String _gradleWrapperWindows = 'gradlew.bat'; +const String _gradleWrapperNonWindows = 'gradlew'; + +/// A utility class for interacting with Gradle projects. +class GradleProject { + /// Creates an instance that runs commands for [project] with the given + /// [processRunner]. + /// + /// If [log] is true, commands run by this instance will long various status + /// messages. + GradleProject( + this.flutterProject, { + this.processRunner = const ProcessRunner(), + this.platform = const LocalPlatform(), + }); + + /// The directory of a Flutter project to run Gradle commands in. + final Directory flutterProject; + + /// The [ProcessRunner] used to run commands. Overridable for testing. + final ProcessRunner processRunner; + + /// The platform that commands are being run on. + final Platform platform; + + /// The project's 'android' directory. + Directory get androidDirectory => flutterProject.childDirectory('android'); + + /// The path to the Gradle wrapper file for the project. + File get gradleWrapper => androidDirectory.childFile( + platform.isWindows ? _gradleWrapperWindows : _gradleWrapperNonWindows); + + /// Whether or not the project is ready to have Gradle commands run on it + /// (i.e., whether the `flutter` tool has generated the necessary files). + bool isConfigured() => gradleWrapper.existsSync(); + + /// Runs a `gradlew` command with the given parameters. + Future runCommand( + String target, { + List arguments = const [], + }) { + return processRunner.runAndStream( + gradleWrapper.path, + [target, ...arguments], + workingDir: androidDirectory, + ); + } +} diff --git a/script/tool/lib/src/common/package_looping_command.dart b/script/tool/lib/src/common/package_looping_command.dart new file mode 100644 index 000000000000..973ac9995cb8 --- /dev/null +++ b/script/tool/lib/src/common/package_looping_command.dart @@ -0,0 +1,404 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// 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:colorize/colorize.dart'; +import 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; + +import 'core.dart'; +import 'plugin_command.dart'; +import 'process_runner.dart'; +import 'repository_package.dart'; + +/// Possible outcomes of a command run for a package. +enum RunState { + /// The command succeeded for the package. + succeeded, + + /// The command was skipped for the package. + skipped, + + /// The command was skipped for the package because it was explicitly excluded + /// in the command arguments. + excluded, + + /// The command failed for the package. + failed, +} + +/// The result of a [runForPackage] call. +class PackageResult { + /// A successful result. + PackageResult.success() : this._(RunState.succeeded); + + /// A run that was skipped as explained in [reason]. + PackageResult.skip(String reason) + : this._(RunState.skipped, [reason]); + + /// A run that was excluded by the command invocation. + PackageResult.exclude() : this._(RunState.excluded); + + /// A run that failed. + /// + /// If [errors] are provided, they will be listed in the summary, otherwise + /// the summary will simply show that the package failed. + PackageResult.fail([List errors = const []]) + : this._(RunState.failed, errors); + + const PackageResult._(this.state, [this.details = const []]); + + /// The state the package run completed with. + final RunState state; + + /// Information about the result: + /// - For `succeeded`, this is empty. + /// - For `skipped`, it contains a single entry describing why the run was + /// skipped. + /// - For `failed`, it contains zero or more specific error details to be + /// shown in the summary. + final List details; +} + +/// An abstract base class for a command that iterates over a set of packages +/// controlled by a standard set of flags, running some actions on each package, +/// and collecting and reporting the success/failure of those actions. +abstract class PackageLoopingCommand extends PluginCommand { + /// Creates a command to operate on [packagesDir] with the given environment. + PackageLoopingCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + GitDir? gitDir, + }) : super(packagesDir, + processRunner: processRunner, platform: platform, gitDir: gitDir); + + /// Packages that had at least one [logWarning] call. + final Set _packagesWithWarnings = + {}; + + /// Number of warnings that happened outside of a [runForPackage] call. + int _otherWarningCount = 0; + + /// The package currently being run by [runForPackage]. + PackageEnumerationEntry? _currentPackageEntry; + + /// Called during [run] before any calls to [runForPackage]. This provides an + /// opportunity to fail early if the command can't be run (e.g., because the + /// arguments are invalid), and to set up any run-level state. + Future initializeRun() async {} + + /// Returns the packages to process. By default, this returns the packages + /// defined by the standard tooling flags and the [inculdeSubpackages] option, + /// but can be overridden for custom package enumeration. + /// + /// Note: Consistent behavior across commands whenever possibel is a goal for + /// this tool, so this should be overridden only in rare cases. + Stream getPackagesToProcess() async* { + yield* includeSubpackages + ? getTargetPackagesAndSubpackages(filterExcluded: false) + : getTargetPackages(filterExcluded: false); + } + + /// Runs the command for [package], returning a list of errors. + /// + /// Errors may either be an empty string if there is no context that should + /// be included in the final error summary (e.g., a command that only has a + /// single failure mode), or strings that should be listed for that package + /// in the final summary. An empty list indicates success. + Future runForPackage(RepositoryPackage package); + + /// Called during [run] after all calls to [runForPackage]. This provides an + /// opportunity to do any cleanup of run-level state. + Future completeRun() async {} + + /// If [captureOutput], this is called just before exiting with all captured + /// [output]. + Future handleCapturedOutput(List output) async {} + + /// Whether or not the output (if any) of [runForPackage] is long, or short. + /// + /// This changes the logging that happens at the start of each package's + /// run; long output gets a banner-style message to make it easier to find, + /// while short output gets a single-line entry. + /// + /// When this is false, runForPackage output should be indented if possible, + /// to make the output structure easier to follow. + bool get hasLongOutput => true; + + /// Whether to loop over all packages (e.g., including example/), rather than + /// only top-level packages. + bool get includeSubpackages => false; + + /// The text to output at the start when reporting one or more failures. + /// This will be followed by a list of packages that reported errors, with + /// the per-package details if any. + /// + /// This only needs to be overridden if the summary should provide extra + /// context. + String get failureListHeader => 'The following packages had errors:'; + + /// The text to output at the end when reporting one or more failures. This + /// will be printed immediately after the a list of packages that reported + /// errors. + /// + /// This only needs to be overridden if the summary should provide extra + /// context. + String get failureListFooter => 'See above for full details.'; + + /// The summary string used for a successful run in the final overview output. + String get successSummaryMessage => 'ran'; + + /// If true, all printing (including the summary) will be redirected to a + /// buffer, and provided in a call to [handleCapturedOutput] at the end of + /// the run. + /// + /// Capturing output will disable any colorizing of output from this base + /// class. + bool get captureOutput => false; + + // ---------------------------------------- + + /// Logs that a warning occurred, and prints `warningMessage` in yellow. + /// + /// Warnings are not surfaced in CI summaries, so this is only useful for + /// highlighting something when someone is already looking though the log + /// messages. DO NOT RELY on someone noticing a warning; instead, use it for + /// things that might be useful to someone debugging an unexpected result. + void logWarning(String warningMessage) { + print(Colorize(warningMessage)..yellow()); + if (_currentPackageEntry != null) { + _packagesWithWarnings.add(_currentPackageEntry!); + } else { + ++_otherWarningCount; + } + } + + /// Returns the relative path from [from] to [entity] in Posix style. + /// + /// This should be used when, for example, printing package-relative paths in + /// status or error messages. + String getRelativePosixPath( + FileSystemEntity entity, { + required Directory from, + }) => + p.posix.joinAll(path.split(path.relative(entity.path, from: from.path))); + + /// The suggested indentation for printed output. + String get indentation => hasLongOutput ? '' : ' '; + + // ---------------------------------------- + + @override + Future run() async { + bool succeeded; + if (captureOutput) { + final List output = []; + final ZoneSpecification logSwitchSpecification = ZoneSpecification( + print: (Zone self, ZoneDelegate parent, Zone zone, String message) { + output.add(message); + }); + succeeded = await runZoned>(_runInternal, + zoneSpecification: logSwitchSpecification); + await handleCapturedOutput(output); + } else { + succeeded = await _runInternal(); + } + + if (!succeeded) { + throw ToolExit(exitCommandFoundErrors); + } + } + + Future _runInternal() async { + _packagesWithWarnings.clear(); + _otherWarningCount = 0; + _currentPackageEntry = null; + + await initializeRun(); + + final List targetPackages = + await getPackagesToProcess().toList(); + + final Map results = + {}; + for (final PackageEnumerationEntry entry in targetPackages) { + _currentPackageEntry = entry; + _printPackageHeading(entry); + + // Command implementations should never see excluded packages; they are + // included at this level only for logging. + if (entry.excluded) { + results[entry] = PackageResult.exclude(); + continue; + } + + PackageResult result; + try { + result = await runForPackage(entry.package); + } catch (e, stack) { + printError(e.toString()); + printError(stack.toString()); + result = PackageResult.fail(['Unhandled exception']); + } + if (result.state == RunState.skipped) { + final String message = + '${indentation}SKIPPING: ${result.details.first}'; + captureOutput ? print(message) : print(Colorize(message)..darkGray()); + } + results[entry] = result; + } + _currentPackageEntry = null; + + completeRun(); + + print('\n'); + // If there were any errors reported, summarize them and exit. + if (results.values + .any((PackageResult result) => result.state == RunState.failed)) { + _printFailureSummary(targetPackages, results); + return false; + } + + // Otherwise, print a summary of what ran for ease of auditing that all the + // expected tests ran. + _printRunSummary(targetPackages, results); + + print('\n'); + _printSuccess('No issues found!'); + return true; + } + + void _printSuccess(String message) { + captureOutput ? print(message) : printSuccess(message); + } + + void _printError(String message) { + captureOutput ? print(message) : printError(message); + } + + /// Prints the status message indicating that the command is being run for + /// [package]. + /// + /// Something is always printed to make it easier to distinguish between + /// a command running for a package and producing no output, and a command + /// not having been run for a package. + void _printPackageHeading(PackageEnumerationEntry entry) { + final String packageDisplayName = entry.package.displayName; + String heading = entry.excluded + ? 'Not running for $packageDisplayName; excluded' + : 'Running for $packageDisplayName'; + if (hasLongOutput) { + heading = ''' + +============================================================ +|| $heading +============================================================ +'''; + } else if (!entry.excluded) { + heading = '$heading...'; + } + if (captureOutput) { + print(heading); + } else { + final Colorize colorizeHeading = Colorize(heading); + print( + entry.excluded ? colorizeHeading.darkGray() : colorizeHeading.cyan()); + } + } + + /// Prints a summary of packges run, packages skipped, and warnings. + void _printRunSummary(List packages, + Map results) { + final Set skippedPackages = results.entries + .where((MapEntry entry) => + entry.value.state == RunState.skipped) + .map((MapEntry entry) => + entry.key) + .toSet(); + final int skipCount = skippedPackages.length + + packages + .where((PackageEnumerationEntry package) => package.excluded) + .length; + // Split the warnings into those from packages that ran, and those that + // were skipped. + final Set _skippedPackagesWithWarnings = + _packagesWithWarnings.intersection(skippedPackages); + final int skippedWarningCount = _skippedPackagesWithWarnings.length; + final int runWarningCount = + _packagesWithWarnings.length - skippedWarningCount; + + final String runWarningSummary = + runWarningCount > 0 ? ' ($runWarningCount with warnings)' : ''; + final String skippedWarningSummary = + runWarningCount > 0 ? ' ($skippedWarningCount with warnings)' : ''; + print('------------------------------------------------------------'); + if (hasLongOutput) { + _printPerPackageRunOverview(packages, skipped: skippedPackages); + } + print( + 'Ran for ${packages.length - skipCount} package(s)$runWarningSummary'); + if (skipCount > 0) { + print('Skipped $skipCount package(s)$skippedWarningSummary'); + } + if (_otherWarningCount > 0) { + print('$_otherWarningCount warnings not associated with a package'); + } + } + + /// Prints a one-line-per-package overview of the run results for each + /// package. + void _printPerPackageRunOverview( + List packageEnumeration, + {required Set skipped}) { + print('Run overview:'); + for (final PackageEnumerationEntry entry in packageEnumeration) { + final bool hadWarning = _packagesWithWarnings.contains(entry); + Styles style; + String summary; + if (entry.excluded) { + summary = 'excluded'; + style = Styles.DARK_GRAY; + } else if (skipped.contains(entry)) { + summary = 'skipped'; + style = hadWarning ? Styles.LIGHT_YELLOW : Styles.DARK_GRAY; + } else { + summary = successSummaryMessage; + style = hadWarning ? Styles.YELLOW : Styles.GREEN; + } + if (hadWarning) { + summary += ' (with warning)'; + } + + if (!captureOutput) { + summary = (Colorize(summary)..apply(style)).toString(); + } + print(' ${entry.package.displayName} - $summary'); + } + print(''); + } + + /// Prints a summary of all of the failures from [results]. + void _printFailureSummary(List packageEnumeration, + Map results) { + const String indentation = ' '; + _printError(failureListHeader); + for (final PackageEnumerationEntry entry in packageEnumeration) { + final PackageResult result = results[entry]!; + if (result.state == RunState.failed) { + final String errorIndentation = indentation * 2; + String errorDetails = ''; + if (result.details.isNotEmpty) { + errorDetails = + ':\n$errorIndentation${result.details.join('\n$errorIndentation')}'; + } + _printError('$indentation${entry.package.displayName}$errorDetails'); + } + } + _printError(failureListFooter); + } +} diff --git a/script/tool/lib/src/common/plugin_command.dart b/script/tool/lib/src/common/plugin_command.dart new file mode 100644 index 000000000000..5d5cbd9abf6c --- /dev/null +++ b/script/tool/lib/src/common/plugin_command.dart @@ -0,0 +1,477 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' as io; +import 'dart:math'; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:yaml/yaml.dart'; + +import 'core.dart'; +import 'git_version_finder.dart'; +import 'process_runner.dart'; +import 'repository_package.dart'; + +/// An entry in package enumeration for APIs that need to include extra +/// data about the entry. +class PackageEnumerationEntry { + /// Creates a new entry for the given package. + PackageEnumerationEntry(this.package, {required this.excluded}); + + /// The package this entry corresponds to. Be sure to check `excluded` before + /// using this, as having an entry does not necessarily mean that the package + /// should be included in the processing of the enumeration. + final RepositoryPackage package; + + /// Whether or not this package was excluded by the command invocation. + final bool excluded; +} + +/// Interface definition for all commands in this tool. +// TODO(stuartmorgan): Move most of this logic to PackageLoopingCommand. +abstract class PluginCommand extends Command { + /// Creates a command to operate on [packagesDir] with the given environment. + PluginCommand( + this.packagesDir, { + this.processRunner = const ProcessRunner(), + this.platform = const LocalPlatform(), + GitDir? gitDir, + }) : _gitDir = gitDir { + argParser.addMultiOption( + _packagesArg, + splitCommas: true, + help: + 'Specifies which packages the command should run on (before sharding).\n', + valueHelp: 'package1,package2,...', + aliases: [_pluginsArg], + ); + 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: 'A list of packages to exclude from from this command.\n\n' + 'Alternately, a list of one or more YAML files that contain a list ' + 'of packages to exclude.', + defaultsTo: [], + ); + argParser.addFlag(_runOnChangedPackagesArg, + help: 'Run the command on changed packages/plugins.\n' + 'If no packages have changed, or if there have been changes that may\n' + 'affect all packages, the command runs on all packages.\n' + 'The packages excluded with $_excludeArg is also excluded even if changed.\n' + 'See $_kBaseSha if a custom base is needed to determine the diff.\n\n' + 'Cannot be combined with $_packagesArg.\n'); + argParser.addFlag(_packagesForBranchArg, + help: + 'This runs on all packages (equivalent to no package selection flag)\n' + 'on master, and behaves like --run-on-changed-packages on any other branch.\n\n' + 'Cannot be combined with $_packagesArg.\n\n' + 'This is intended for use in CI.\n', + hide: true); + 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 _packagesArg = 'packages'; + 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 _packagesForBranchArg = 'packages-for-branch'; + 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 current platform. + /// + /// This can be overridden for testing. + final Platform platform; + + /// The git directory to use. If unset, [gitDir] populates it from the + /// packages directory's enclosing repository. + /// + /// This can be mocked for testing. + GitDir? _gitDir; + + int? _shardIndex; + int? _shardCount; + + // Cached set of explicitly excluded packages. + Set? _excludedPackages; + + /// A context that matches the default for [platform]. + p.Context get path => platform.isWindows ? p.windows : p.posix; + + /// The command to use when running `flutter`. + String get flutterCommand => platform.isWindows ? 'flutter.bat' : 'flutter'; + + /// 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!; + } + + /// Returns the [GitDir] containing [packagesDir]. + Future get gitDir async { + GitDir? gitDir = _gitDir; + if (gitDir != null) { + return gitDir; + } + + // 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)) { + printError('$packagesPath is not a valid Git repository.'); + throw ToolExit(2); + } + gitDir = + await GitDir.fromExisting(packagesDir.path, allowSubdirectory: true); + _gitDir = gitDir; + return gitDir; + } + + /// 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 set of plugins to exclude based on the `--exclude` argument. + Set getExcludedPackageNames() { + final Set excludedPackages = _excludedPackages ?? + getStringListArg(_excludeArg).expand((String item) { + if (item.endsWith('.yaml')) { + final File file = packagesDir.fileSystem.file(item); + return (loadYaml(file.readAsStringSync()) as YamlList) + .toList() + .cast(); + } + return [item]; + }).toSet(); + // Cache for future calls. + _excludedPackages = excludedPackages; + return excludedPackages; + } + + /// Returns the root diretories of the packages involved in this command + /// execution. + /// + /// Depending on the command arguments, this may be a user-specified set of + /// packages, the set of packages that should be run for a given diff, or all + /// packages. + /// + /// By default, packages excluded via --exclude will not be in the stream, but + /// they can be included by passing false for [filterExcluded]. + Stream getTargetPackages( + {bool filterExcluded = true}) async* { + // To avoid assuming consistency of `Directory.list` across command + // invocations, we collect and sort the plugin folders before sharding. + // This is considered an implementation detail which is why the API still + // uses streams. + final List allPlugins = + await _getAllPackages().toList(); + allPlugins.sort((PackageEnumerationEntry p1, PackageEnumerationEntry p2) => + p1.package.path.compareTo(p2.package.path)); + final int shardSize = allPlugins.length ~/ shardCount + + (allPlugins.length % shardCount == 0 ? 0 : 1); + final int start = min(shardIndex * shardSize, allPlugins.length); + final int end = min(start + shardSize, allPlugins.length); + + for (final PackageEnumerationEntry plugin + in allPlugins.sublist(start, end)) { + if (!(filterExcluded && plugin.excluded)) { + yield plugin; + } + } + } + + /// Returns the root Dart package folders of the packages involved in this + /// command execution, assuming there is only one shard. Depending on the + /// command arguments, this may be a user-specified set of packages, the + /// set of packages that should be run for a given diff, or all packages. + /// + /// This will return packages that have been excluded by the --exclude + /// parameter, annotated in the entry as excluded. + /// + /// 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 non-plugin package, or a non-federated + /// plugin. + /// 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 an + /// "app-facing" package which declares the API for the plugin, a + /// platform interface package which declares the API for implementations, + /// and one or more platform-specific implementation packages. + /// 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 _getAllPackages() async* { + final Set packageSelectionFlags = { + _packagesArg, + _runOnChangedPackagesArg, + _packagesForBranchArg, + }; + if (packageSelectionFlags + .where((String flag) => argResults!.wasParsed(flag)) + .length > + 1) { + printError('Only one of --$_packagesArg, --$_runOnChangedPackagesArg, or ' + '--$_packagesForBranchArg can be provided.'); + throw ToolExit(exitInvalidArguments); + } + + Set plugins = Set.from(getStringListArg(_packagesArg)); + + final bool runOnChangedPackages; + if (getBoolArg(_runOnChangedPackagesArg)) { + runOnChangedPackages = true; + } else if (getBoolArg(_packagesForBranchArg)) { + final String? branch = await _getBranch(); + if (branch == null) { + printError('Unabled to determine branch; --$_packagesForBranchArg can ' + 'only be used in a git repository.'); + throw ToolExit(exitInvalidArguments); + } else { + runOnChangedPackages = branch != 'master'; + // Log the mode for auditing what was intended to run. + print('--$_packagesForBranchArg: running on ' + '${runOnChangedPackages ? 'changed' : 'all'} packages'); + } + } else { + runOnChangedPackages = false; + } + + final Set excludedPluginNames = getExcludedPackageNames(); + + if (runOnChangedPackages) { + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + final String baseSha = await gitVersionFinder.getBaseSha(); + print( + 'Running for all packages that have changed relative to "$baseSha"\n'); + final List changedFiles = + await gitVersionFinder.getChangedFiles(); + if (!_changesRequireFullTest(changedFiles)) { + plugins = _getChangedPackages(changedFiles); + } + } + + 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 (plugins.isEmpty || plugins.contains(p.basename(entity.path))) { + yield PackageEnumerationEntry( + RepositoryPackage(entity as Directory), + excluded: excludedPluginNames.contains(entity.basename)); + } + } 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 = + path.relative(subdir.path, from: dir.path); + final String packageName = path.basename(subdir.path); + final String basenamePath = path.basename(entity.path); + if (plugins.isEmpty || + plugins.contains(relativePath) || + plugins.contains(basenamePath)) { + yield PackageEnumerationEntry( + RepositoryPackage(subdir as Directory), + excluded: excludedPluginNames.contains(basenamePath) || + excludedPluginNames.contains(packageName) || + excludedPluginNames.contains(relativePath)); + } + } + } + } + } + } + } + + /// Returns all Dart package folders (typically, base package + example) of + /// the packages involved in this command execution. + /// + /// By default, packages excluded via --exclude will not be in the stream, but + /// they can be included by passing false for [filterExcluded]. + Stream getTargetPackagesAndSubpackages( + {bool filterExcluded = true}) async* { + await for (final PackageEnumerationEntry plugin + in getTargetPackages(filterExcluded: filterExcluded)) { + yield plugin; + yield* plugin.package.directory + .list(recursive: true, followLinks: false) + .where(_isDartPackage) + .map((FileSystemEntity directory) => PackageEnumerationEntry( + // _isDartPackage guarantees that this cast is valid. + RepositoryPackage(directory as Directory), + excluded: plugin.excluded)); + } + } + + /// Returns the files contained, recursively, within the packages + /// involved in this command execution. + Stream getFiles() { + return getTargetPackages().asyncExpand( + (PackageEnumerationEntry entry) => getFilesForPackage(entry.package)); + } + + /// Returns the files contained, recursively, within [package]. + Stream getFilesForPackage(RepositoryPackage package) { + return package.directory + .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(); + } + + /// 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 baseSha = getStringArg(_kBaseSha); + + final GitVersionFinder gitVersionFinder = + GitVersionFinder(await gitDir, baseSha); + return gitVersionFinder; + } + + // Returns packages that have been changed given a list of changed files. + // + // The paths must use POSIX separators (e.g., as provided by git output). + Set _getChangedPackages(List changedFiles) { + final Set packages = {}; + for (final String path in changedFiles) { + final List pathComponents = p.posix.split(path); + final int packagesIndex = + pathComponents.indexWhere((String element) => element == 'packages'); + if (packagesIndex != -1) { + packages.add(pathComponents[packagesIndex + 1]); + } + } + if (packages.isEmpty) { + print('No changed packages.'); + } else { + final String changedPackages = packages.join(','); + print('Changed packages: $changedPackages'); + } + return packages; + } + + Future _getBranch() async { + final io.ProcessResult branchResult = await (await gitDir).runCommand( + ['rev-parse', '--abbrev-ref', 'HEAD'], + throwOnError: false); + if (branchResult.exitCode != 0) { + return null; + } + return (branchResult.stdout as String).trim(); + } + + // Returns true if one or more files changed that have the potential to affect + // any plugin (e.g., CI script changes). + bool _changesRequireFullTest(List changedFiles) { + 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('/'))); + + return changedFiles.any((String path) => + specialFiles.contains(path) || + specialDirectories.any((String dir) => path.startsWith(dir))); + } +} diff --git a/script/tool/lib/src/common/plugin_utils.dart b/script/tool/lib/src/common/plugin_utils.dart new file mode 100644 index 000000000000..6cfe9928d689 --- /dev/null +++ b/script/tool/lib/src/common/plugin_utils.dart @@ -0,0 +1,146 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; +import 'package:yaml/yaml.dart'; + +import 'core.dart'; + +/// Possible plugin support options for a platform. +enum PlatformSupport { + /// The platform has an implementation in the package. + inline, + + /// The platform has an endorsed federated implementation in another package. + federated, +} + +/// Returns true if [package] is a Flutter plugin. +bool isFlutterPlugin(RepositoryPackage package) { + return _readPluginPubspecSection(package) != null; +} + +/// Returns true if [package] is a Flutter [platform] plugin. +/// +/// It checks this by looking for the following pattern in the pubspec: +/// +/// flutter: +/// plugin: +/// platforms: +/// [platform]: +/// +/// If [requiredMode] is provided, the plugin must have the given type of +/// implementation in order to return true. +bool pluginSupportsPlatform( + String platform, + RepositoryPackage plugin, { + PlatformSupport? requiredMode, + String? variant, +}) { + assert(platform == kPlatformIos || + platform == kPlatformAndroid || + platform == kPlatformWeb || + platform == kPlatformMacos || + platform == kPlatformWindows || + platform == kPlatformLinux); + + final YamlMap? platformEntry = + _readPlatformPubspecSectionForPlugin(platform, plugin); + if (platformEntry == null) { + return false; + } + + // If the platform entry is present, then it supports the platform. Check + // for required mode if specified. + if (requiredMode != null) { + final bool federated = platformEntry.containsKey('default_package'); + if (federated != (requiredMode == PlatformSupport.federated)) { + return false; + } + } + + // If a variant is specified, check for that variant. + if (variant != null) { + const String variantsKey = 'supportedVariants'; + if (platformEntry.containsKey(variantsKey)) { + if (!(platformEntry['supportedVariants']! as YamlList) + .contains(variant)) { + return false; + } + } else { + // Platforms with variants have a default variant when unspecified for + // backward compatibility. Must match the flutter tool logic. + const Map defaultVariants = { + kPlatformWindows: platformVariantWin32, + }; + if (variant != defaultVariants[platform]) { + return false; + } + } + } + + return true; +} + +/// Returns true if [plugin] includes native code for [platform], as opposed to +/// being implemented entirely in Dart. +bool pluginHasNativeCodeForPlatform(String platform, RepositoryPackage plugin) { + if (platform == kPlatformWeb) { + // Web plugins are always Dart-only. + return false; + } + final YamlMap? platformEntry = + _readPlatformPubspecSectionForPlugin(platform, plugin); + if (platformEntry == null) { + return false; + } + // All other platforms currently use pluginClass for indicating the native + // code in the plugin. + final String? pluginClass = platformEntry['pluginClass'] as String?; + // TODO(stuartmorgan): Remove the check for 'none' once none of the plugins + // in the repository use that workaround. See + // https://github.com/flutter/flutter/issues/57497 for context. + return pluginClass != null && pluginClass != 'none'; +} + +/// Returns the +/// flutter: +/// plugin: +/// platforms: +/// [platform]: +/// section from [plugin]'s pubspec.yaml, or null if either it is not present, +/// or the pubspec couldn't be read. +YamlMap? _readPlatformPubspecSectionForPlugin( + String platform, RepositoryPackage plugin) { + final YamlMap? pluginSection = _readPluginPubspecSection(plugin); + if (pluginSection == null) { + return null; + } + final YamlMap? platforms = pluginSection['platforms'] as YamlMap?; + if (platforms == null) { + return null; + } + return platforms[platform] as YamlMap?; +} + +/// Returns the +/// flutter: +/// plugin: +/// platforms: +/// section from [plugin]'s pubspec.yaml, or null if either it is not present, +/// or the pubspec couldn't be read. +YamlMap? _readPluginPubspecSection(RepositoryPackage package) { + final File pubspecFile = package.pubspecFile; + if (!pubspecFile.existsSync()) { + return null; + } + final YamlMap pubspecYaml = + loadYaml(pubspecFile.readAsStringSync()) as YamlMap; + final YamlMap? flutterSection = pubspecYaml['flutter'] as YamlMap?; + if (flutterSection == null) { + return null; + } + return flutterSection['plugin'] as YamlMap?; +} diff --git a/script/tool/lib/src/common/process_runner.dart b/script/tool/lib/src/common/process_runner.dart new file mode 100644 index 000000000000..429761ead3b8 --- /dev/null +++ b/script/tool/lib/src/common/process_runner.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:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; + +import 'core.dart'; + +/// 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.'; + } +} diff --git a/script/tool/lib/src/common/pub_version_finder.dart b/script/tool/lib/src/common/pub_version_finder.dart new file mode 100644 index 000000000000..572cb913aa7d --- /dev/null +++ b/script/tool/lib/src/common/pub_version_finder.dart @@ -0,0 +1,103 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; + +import 'package:http/http.dart' as http; +import 'package:pub_semver/pub_semver.dart'; + +/// Finding version of [package] that is published on pub. +class PubVersionFinder { + /// Constructor. + /// + /// Note: you should manually close the [httpClient] when done using the finder. + PubVersionFinder({this.pubHost = defaultPubHost, required this.httpClient}); + + /// The default pub host to use. + static const String defaultPubHost = 'https://pub.dev'; + + /// The pub host url, defaults to `https://pub.dev`. + final String pubHost; + + /// The http client. + /// + /// You should manually close this client when done using this finder. + final http.Client httpClient; + + /// Get the package version on pub. + Future getPackageVersion( + {required String packageName}) async { + assert(packageName.isNotEmpty); + final Uri pubHostUri = Uri.parse(pubHost); + final Uri url = pubHostUri.replace(path: '/packages/$packageName.json'); + final http.Response response = await httpClient.get(url); + + if (response.statusCode == 404) { + return PubVersionFinderResponse( + versions: [], + result: PubVersionFinderResult.noPackageFound, + httpResponse: response); + } else if (response.statusCode != 200) { + return PubVersionFinderResponse( + versions: [], + result: PubVersionFinderResult.fail, + httpResponse: response); + } + final 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, +} diff --git a/script/tool/lib/src/common/repository_package.dart b/script/tool/lib/src/common/repository_package.dart new file mode 100644 index 000000000000..3b4417ac8182 --- /dev/null +++ b/script/tool/lib/src/common/repository_package.dart @@ -0,0 +1,98 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'core.dart'; + +/// A package in the repository. +// +// TODO(stuartmorgan): Add more package-related info here, such as an on-demand +// cache of the parsed pubspec. +class RepositoryPackage { + /// Creates a representation of the package at [directory]. + RepositoryPackage(this.directory); + + /// The location of the package. + final Directory directory; + + /// The path to the package. + String get path => directory.path; + + /// Returns the string to use when referring to the package in user-targeted + /// messages. + /// + /// Callers should not expect a specific format for this string, since + /// it uses heuristics to try to be precise without being overly verbose. If + /// an exact format (e.g., published name, or basename) is required, that + /// should be used instead. + String get displayName { + List components = directory.fileSystem.path.split(directory.path); + // Remove everything up to the packages directory. + final int packagesIndex = components.indexOf('packages'); + if (packagesIndex != -1) { + components = components.sublist(packagesIndex + 1); + } + // For the common federated plugin pattern of `foo/foo_subpackage`, drop + // the first part since it's not useful. + if (components.length >= 2 && + components[1].startsWith('${components[0]}_')) { + components = components.sublist(1); + } + return p.posix.joinAll(components); + } + + /// The package's top-level pubspec.yaml. + File get pubspecFile => directory.childFile('pubspec.yaml'); + + /// True if this appears to be a federated plugin package, according to + /// repository conventions. + bool get isFederated => + directory.parent.basename != 'packages' && + directory.basename.startsWith(directory.parent.basename); + + /// True if this appears to be a platform interface package, according to + /// repository conventions. + bool get isPlatformInterface => + directory.basename.endsWith('_platform_interface'); + + /// True if this appears to be a platform implementation package, according to + /// repository conventions. + bool get isPlatformImplementation => + // Any part of a federated plugin that isn't the platform interface and + // isn't the app-facing package should be an implementation package. + isFederated && + !isPlatformInterface && + directory.basename != directory.parent.basename; + + /// Returns the Flutter example packages contained in the package, if any. + Iterable getExamples() { + final Directory exampleDirectory = directory.childDirectory('example'); + if (!exampleDirectory.existsSync()) { + return []; + } + if (isFlutterPackage(exampleDirectory)) { + return [RepositoryPackage(exampleDirectory)]; + } + // 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 exampleDirectory + .listSync() + .where((FileSystemEntity entity) => isFlutterPackage(entity)) + // isFlutterPackage guarantees that the cast to Directory is safe. + .map((FileSystemEntity entity) => + RepositoryPackage(entity as Directory)); + } + + /// Returns the example directory, assuming there is only one. + /// + /// DO NOT USE THIS METHOD. It exists only to easily find code that was + /// written to use a single example and needs to be restructured to handle + /// multiple examples. New code should always use [getExamples]. + // TODO(stuartmorgan): Eliminate all uses of this. + RepositoryPackage getSingleExampleDeprecated() => + RepositoryPackage(directory.childDirectory('example')); +} diff --git a/script/tool/lib/src/common/xcode.dart b/script/tool/lib/src/common/xcode.dart new file mode 100644 index 000000000000..83f681bcb492 --- /dev/null +++ b/script/tool/lib/src/common/xcode.dart @@ -0,0 +1,159 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; + +import 'core.dart'; +import 'process_runner.dart'; + +const String _xcodeBuildCommand = 'xcodebuild'; +const String _xcRunCommand = 'xcrun'; + +/// A utility class for interacting with the installed version of Xcode. +class Xcode { + /// Creates an instance that runs commands with the given [processRunner]. + /// + /// If [log] is true, commands run by this instance will long various status + /// messages. + Xcode({ + this.processRunner = const ProcessRunner(), + this.log = false, + }); + + /// The [ProcessRunner] used to run commands. Overridable for testing. + final ProcessRunner processRunner; + + /// Whether or not to log when running commands. + final bool log; + + /// Runs an `xcodebuild` in [directory] with the given parameters. + Future runXcodeBuild( + Directory directory, { + List actions = const ['build'], + required String workspace, + required String scheme, + String? configuration, + List extraFlags = const [], + }) { + final List args = [ + _xcodeBuildCommand, + ...actions, + if (workspace != null) ...['-workspace', workspace], + if (scheme != null) ...['-scheme', scheme], + if (configuration != null) ...['-configuration', configuration], + ...extraFlags, + ]; + final String completeTestCommand = '$_xcRunCommand ${args.join(' ')}'; + if (log) { + print(completeTestCommand); + } + return processRunner.runAndStream(_xcRunCommand, args, + workingDir: directory); + } + + /// Returns true if [project], which should be an .xcodeproj directory, + /// contains a target called [target], false if it does not, and null if the + /// check fails (e.g., if [project] is not an Xcode project). + Future projectHasTarget(Directory project, String target) async { + final io.ProcessResult result = + await processRunner.run(_xcRunCommand, [ + _xcodeBuildCommand, + '-list', + '-json', + '-project', + project.path, + ]); + if (result.exitCode != 0) { + return null; + } + Map? projectInfo; + try { + projectInfo = (jsonDecode(result.stdout as String) + as Map)['project'] as Map?; + } on FormatException { + return null; + } + if (projectInfo == null) { + return null; + } + final List? targets = + (projectInfo['targets'] as List?)?.cast(); + return targets?.contains(target) ?? false; + } + + /// Returns the newest available simulator (highest OS version, with ties + /// broken in favor of newest device), if any. + Future findBestAvailableIphoneSimulator() async { + final List findSimulatorsArguments = [ + 'simctl', + 'list', + 'devices', + 'runtimes', + 'available', + '--json', + ]; + final String findSimulatorCompleteCommand = + '$_xcRunCommand ${findSimulatorsArguments.join(' ')}'; + if (log) { + print('Looking for available simulators...'); + print(findSimulatorCompleteCommand); + } + final io.ProcessResult findSimulatorsResult = + await processRunner.run(_xcRunCommand, findSimulatorsArguments); + if (findSimulatorsResult.exitCode != 0) { + if (log) { + printError( + 'Error occurred while running "$findSimulatorCompleteCommand":\n' + '${findSimulatorsResult.stderr}'); + } + return null; + } + final Map simulatorListJson = + jsonDecode(findSimulatorsResult.stdout as String) + as Map; + final List> runtimes = + (simulatorListJson['runtimes'] as List) + .cast>(); + final Map devices = + (simulatorListJson['devices'] as Map) + .cast(); + if (runtimes.isEmpty || devices.isEmpty) { + return null; + } + String? id; + // Looking for runtimes, trying to find one with highest OS version. + for (final Map rawRuntimeMap in runtimes.reversed) { + final Map runtimeMap = + rawRuntimeMap.cast(); + if ((runtimeMap['name'] as String?)?.contains('iOS') != true) { + continue; + } + final String? runtimeID = runtimeMap['identifier'] as String?; + if (runtimeID == null) { + continue; + } + final List>? devicesForRuntime = + (devices[runtimeID] as List?)?.cast>(); + if (devicesForRuntime == null || devicesForRuntime.isEmpty) { + continue; + } + // Looking for runtimes, trying to find latest version of device. + for (final Map rawDevice in devicesForRuntime.reversed) { + final Map device = rawDevice.cast(); + id = device['udid'] as String?; + if (id == null) { + continue; + } + if (log) { + print('device selected: $device'); + } + return id; + } + } + return null; + } +} diff --git a/script/tool/lib/src/create_all_plugins_app_command.dart b/script/tool/lib/src/create_all_plugins_app_command.dart new file mode 100644 index 000000000000..6dbebf2f5c74 --- /dev/null +++ b/script/tool/lib/src/create_all_plugins_app_command.dart @@ -0,0 +1,243 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; + +import 'common/core.dart'; +import 'common/plugin_command.dart'; +import 'common/repository_package.dart'; + +const String _outputDirectoryFlag = 'output-dir'; + +/// 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, + }) : super(packagesDir) { + final Directory defaultDir = + pluginsRoot ?? packagesDir.fileSystem.currentDirectory; + argParser.addOption(_outputDirectoryFlag, + defaultsTo: defaultDir.path, + help: 'The path the directory to create the "all_plugins" project in.\n' + 'Defaults to the repository root.'); + } + + /// The location of the synthesized app project. + Directory get appDirectory => packagesDir.fileSystem + .directory(getStringArg(_outputDirectoryFlag)) + .childDirectory('all_plugins'); + + @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); + } + + final Set excluded = getExcludedPackageNames(); + if (excluded.isNotEmpty) { + print('Exluding the following plugins from the combined build:'); + for (final String plugin in excluded) { + print(' $plugin'); + } + print(''); + } + + await Future.wait(>[ + _genPubspecWithAllPlugins(), + _updateAppGradle(), + _updateManifest(), + ]); + } + + Future _createApp() async { + final io.ProcessResult result = io.Process.runSync( + flutterCommand, + [ + '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 PackageEnumerationEntry entry in getTargetPackages()) { + final RepositoryPackage package = entry.package; + final Directory pluginDirectory = package.directory; + final String pluginName = pluginDirectory.basename; + final File pubspecFile = package.pubspecFile; + final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + + if (pubspec.publishTo != 'none') { + pathDependencies[pluginName] = PathDependency(pluginDirectory.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} +publish_to: none + +version: ${pubspec.version} + +environment:${_pubspecMapString(pubspec.environment!)} + +dependencies:${_pubspecMapString(pubspec.dependencies)} + +dependency_overrides:${_pubspecMapString(pubspec.dependencyOverrides)} + +dev_dependencies:${_pubspecMapString(pubspec.devDependencies)} +###'''; + } + + String _pubspecMapString(Map values) { + final StringBuffer buffer = StringBuffer(); + + for (final MapEntry entry in values.entries) { + buffer.writeln(); + 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; + String depPath = dep.path; + if (path.style == p.Style.windows) { + // Posix-style path separators are preferred in pubspec.yaml (and + // using a consistent format makes unit testing simpler), so convert. + final List components = path.split(depPath); + final String firstComponent = components.first; + // path.split leaves a \ on drive components that isn't necessary, + // and confuses pub, so remove it. + if (firstComponent.endsWith(r':\')) { + components[0] = + firstComponent.substring(0, firstComponent.length - 1); + } + depPath = p.posix.joinAll(components); + } + buffer.write(' ${entry.key}: \n path: $depPath'); + } else { + throw UnimplementedError( + 'Not available for type: ${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..593e557fa395 --- /dev/null +++ b/script/tool/lib/src/drive_examples_command.dart @@ -0,0 +1,334 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:file/file.dart'; +import 'package:platform/platform.dart'; + +import 'common/core.dart'; +import 'common/package_looping_command.dart'; +import 'common/plugin_utils.dart'; +import 'common/process_runner.dart'; +import 'common/repository_package.dart'; + +const int _exitNoPlatformFlags = 2; +const int _exitNoAvailableDevice = 3; + +/// A command to run the example applications for packages via Flutter driver. +class DriveExamplesCommand extends PackageLoopingCommand { + /// Creates an instance of the drive command. + DriveExamplesCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + }) : super(packagesDir, processRunner: processRunner, platform: platform) { + argParser.addFlag(kPlatformAndroid, + help: 'Runs the Android implementation of the examples'); + argParser.addFlag(kPlatformIos, + help: 'Runs the iOS implementation of the examples'); + argParser.addFlag(kPlatformLinux, + help: 'Runs the Linux implementation of the examples'); + argParser.addFlag(kPlatformMacos, + help: 'Runs the macOS implementation of the examples'); + argParser.addFlag(kPlatformWeb, + help: 'Runs the web implementation of the examples'); + argParser.addFlag(kPlatformWindows, + help: 'Runs the Windows (Win32) implementation of the examples'); + argParser.addFlag(kPlatformWinUwp, + help: + 'Runs the UWP implementation of the examples [currently a no-op]'); + argParser.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 ' + 'either the corresponding test in test_driver (for example, ' + 'test_driver/app_test.dart would match test_driver/app.dart), or the ' + '*_test.dart files in integration_test/.\n\n' + 'This command requires "flutter" to be in your path.'; + + Map> _targetDeviceFlags = const >{}; + + @override + Future initializeRun() async { + final List platformSwitches = [ + kPlatformAndroid, + kPlatformIos, + kPlatformLinux, + kPlatformMacos, + kPlatformWeb, + kPlatformWindows, + kPlatformWinUwp, + ]; + final int platformCount = platformSwitches + .where((String platform) => getBoolArg(platform)) + .length; + // The flutter tool currently doesn't accept multiple device arguments: + // https://github.com/flutter/flutter/issues/35733 + // If that is implemented, this check can be relaxed. + if (platformCount != 1) { + printError( + 'Exactly one of ${platformSwitches.map((String platform) => '--$platform').join(', ')} ' + 'must be specified.'); + throw ToolExit(_exitNoPlatformFlags); + } + + if (getBoolArg(kPlatformWinUwp)) { + logWarning('Driving UWP applications is not yet supported'); + } + + String? androidDevice; + if (getBoolArg(kPlatformAndroid)) { + final List devices = await _getDevicesForPlatform('android'); + if (devices.isEmpty) { + printError('No Android devices available'); + throw ToolExit(_exitNoAvailableDevice); + } + androidDevice = devices.first; + } + + String? iosDevice; + if (getBoolArg(kPlatformIos)) { + final List devices = await _getDevicesForPlatform('ios'); + if (devices.isEmpty) { + printError('No iOS devices available'); + throw ToolExit(_exitNoAvailableDevice); + } + iosDevice = devices.first; + } + + _targetDeviceFlags = >{ + if (getBoolArg(kPlatformAndroid)) + kPlatformAndroid: ['-d', androidDevice!], + if (getBoolArg(kPlatformIos)) kPlatformIos: ['-d', iosDevice!], + if (getBoolArg(kPlatformLinux)) kPlatformLinux: ['-d', 'linux'], + if (getBoolArg(kPlatformMacos)) kPlatformMacos: ['-d', 'macos'], + if (getBoolArg(kPlatformWeb)) + kPlatformWeb: [ + '-d', + 'web-server', + '--web-port=7357', + '--browser-name=chrome' + ], + if (getBoolArg(kPlatformWindows)) + kPlatformWindows: ['-d', 'windows'], + // TODO(stuartmorgan): Check these flags once drive supports UWP: + // https://github.com/flutter/flutter/issues/82821 + if (getBoolArg(kPlatformWinUwp)) + kPlatformWinUwp: ['-d', 'winuwp'], + }; + } + + @override + Future runForPackage(RepositoryPackage package) async { + if (package.isPlatformInterface && + !package.getSingleExampleDeprecated().directory.existsSync()) { + // Platform interface packages generally aren't intended to have + // examples, and don't need integration tests, so skip rather than fail. + return PackageResult.skip( + 'Platform interfaces are not expected to have integration tests.'); + } + + final List deviceFlags = []; + for (final MapEntry> entry + in _targetDeviceFlags.entries) { + final String platform = entry.key; + String? variant; + if (platform == kPlatformWindows) { + variant = platformVariantWin32; + } else if (platform == kPlatformWinUwp) { + variant = platformVariantWinUwp; + // TODO(stuartmorgan): Remove this once drive supports UWP. + // https://github.com/flutter/flutter/issues/82821 + return PackageResult.skip('Drive does not yet support UWP'); + } + if (pluginSupportsPlatform(platform, package, variant: variant)) { + deviceFlags.addAll(entry.value); + } else { + print('Skipping unsupported platform ${entry.key}...'); + } + } + // If there is no supported target platform, skip the plugin. + if (deviceFlags.isEmpty) { + return PackageResult.skip( + '${package.displayName} does not support any requested platform.'); + } + + int examplesFound = 0; + bool testsRan = false; + final List errors = []; + for (final RepositoryPackage example in package.getExamples()) { + ++examplesFound; + final String exampleName = + getRelativePosixPath(example.directory, from: packagesDir); + + final List drivers = await _getDrivers(example); + if (drivers.isEmpty) { + print('No driver tests found for $exampleName'); + continue; + } + + for (final File driver in drivers) { + final List testTargets = []; + + // Try to find a matching app to drive without the _test.dart + // TODO(stuartmorgan): Migrate all remaining uses of this legacy + // approach (currently only video_player) and remove support for it: + // https://github.com/flutter/flutter/issues/85224. + final File? legacyTestFile = _getLegacyTestFileForTestDriver(driver); + if (legacyTestFile != null) { + testTargets.add(legacyTestFile); + } else { + (await _getIntegrationTests(example)).forEach(testTargets.add); + } + + if (testTargets.isEmpty) { + final String driverRelativePath = + getRelativePosixPath(driver, from: package.directory); + printError( + 'Found $driverRelativePath, but no integration_test/*_test.dart files.'); + errors.add('No test files for $driverRelativePath'); + continue; + } + + testsRan = true; + final List failingTargets = await _driveTests( + example, driver, testTargets, + deviceFlags: deviceFlags); + for (final File failingTarget in failingTargets) { + errors.add( + getRelativePosixPath(failingTarget, from: package.directory)); + } + } + } + if (!testsRan) { + printError('No driver tests were run ($examplesFound example(s) found).'); + errors.add('No tests ran (use --exclude if this is intentional).'); + } + return errors.isEmpty + ? PackageResult.success() + : PackageResult.fail(errors); + } + + Future> _getDevicesForPlatform(String platform) async { + final List deviceIds = []; + + final ProcessResult result = await processRunner.run( + flutterCommand, ['devices', '--machine'], + stdoutEncoding: utf8); + if (result.exitCode != 0) { + return deviceIds; + } + + String output = result.stdout as String; + // --machine doesn't currently prevent the tool from printing banners; + // see https://github.com/flutter/flutter/issues/86055. This workaround + // can be removed once that is fixed. + output = output.substring(output.indexOf('[')); + + final List> devices = + (jsonDecode(output) as List).cast>(); + for (final Map deviceInfo in devices) { + final String targetPlatform = + (deviceInfo['targetPlatform'] as String?) ?? ''; + if (targetPlatform.startsWith(platform)) { + final String? deviceId = deviceInfo['id'] as String?; + if (deviceId != null) { + deviceIds.add(deviceId); + } + } + } + return deviceIds; + } + + Future> _getDrivers(RepositoryPackage example) async { + final List drivers = []; + + final Directory driverDir = example.directory.childDirectory('test_driver'); + if (driverDir.existsSync()) { + await for (final FileSystemEntity driver in driverDir.list()) { + if (driver is File && driver.basename.endsWith('_test.dart')) { + drivers.add(driver); + } + } + } + return drivers; + } + + File? _getLegacyTestFileForTestDriver(File testDriver) { + final String testName = testDriver.basename.replaceAll( + RegExp(r'_test.dart$'), + '.dart', + ); + final File testFile = testDriver.parent.childFile(testName); + + return testFile.existsSync() ? testFile : null; + } + + Future> _getIntegrationTests(RepositoryPackage example) async { + final List tests = []; + final Directory integrationTestDir = + example.directory.childDirectory('integration_test'); + + if (integrationTestDir.existsSync()) { + await for (final FileSystemEntity file in integrationTestDir.list()) { + if (file is File && file.basename.endsWith('_test.dart')) { + tests.add(file); + } + } + } + return tests; + } + + /// For each file in [targets], uses + /// `flutter drive --driver [driver] --target ` + /// to drive [example], returning a list of any failing test targets. + /// + /// [deviceFlags] should contain the flags to run the test on a specific + /// target device (plus any supporting device-specific flags). E.g.: + /// - `['-d', 'macos']` for driving for macOS. + /// - `['-d', 'web-server', '--web-port=', '--browser-name=]` + /// for web + Future> _driveTests( + RepositoryPackage example, + File driver, + List targets, { + required List deviceFlags, + }) async { + final List failures = []; + + final String enableExperiment = getStringArg(kEnableExperiment); + + for (final File target in targets) { + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'drive', + ...deviceFlags, + if (enableExperiment.isNotEmpty) + '--enable-experiment=$enableExperiment', + '--driver', + getRelativePosixPath(driver, from: example.directory), + '--target', + getRelativePosixPath(target, from: example.directory), + ], + workingDir: example.directory); + if (exitCode != 0) { + failures.add(target); + } + } + return failures; + } +} diff --git a/script/tool/lib/src/federation_safety_check_command.dart b/script/tool/lib/src/federation_safety_check_command.dart new file mode 100644 index 000000000000..200f9c3f48cb --- /dev/null +++ b/script/tool/lib/src/federation_safety_check_command.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:file/file.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:git/git.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; + +import 'common/core.dart'; +import 'common/file_utils.dart'; +import 'common/git_version_finder.dart'; +import 'common/package_looping_command.dart'; +import 'common/process_runner.dart'; +import 'common/repository_package.dart'; + +/// A command to check that PRs don't violate repository best practices that +/// have been established to avoid breakages that building and testing won't +/// catch. +class FederationSafetyCheckCommand extends PackageLoopingCommand { + /// Creates an instance of the safety check command. + FederationSafetyCheckCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + GitDir? gitDir, + }) : super( + packagesDir, + processRunner: processRunner, + platform: platform, + gitDir: gitDir, + ); + + // A map of package name (as defined by the directory name of the package) + // to a list of changed Dart files in that package, as Posix paths relative to + // the package root. + // + // This only considers top-level packages, not subpackages such as example/. + final Map> _changedDartFiles = >{}; + + // The set of *_platform_interface packages that will have public code changes + // published. + final Set _modifiedAndPublishedPlatformInterfacePackages = {}; + + // The set of conceptual plugins (not packages) that have changes. + final Set _changedPlugins = {}; + + static const String _platformInterfaceSuffix = '_platform_interface'; + + @override + final String name = 'federation-safety-check'; + + @override + final String description = + 'Checks that the change does not violate repository rules around changes ' + 'to federated plugin packages.'; + + @override + bool get hasLongOutput => false; + + @override + Future initializeRun() async { + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + final String baseSha = await gitVersionFinder.getBaseSha(); + print('Validating changes relative to "$baseSha"\n'); + for (final String path in await gitVersionFinder.getChangedFiles()) { + // Git output always uses Posix paths. + final List allComponents = p.posix.split(path); + final int packageIndex = allComponents.indexOf('packages'); + if (packageIndex == -1) { + continue; + } + final List relativeComponents = + allComponents.sublist(packageIndex + 1); + // The package name is either the directory directly under packages/, or + // the directory under that in the case of a federated plugin. + String packageName = relativeComponents.removeAt(0); + // Count the top-level plugin as changed. + _changedPlugins.add(packageName); + if (relativeComponents[0] == packageName || + relativeComponents[0].startsWith('${packageName}_')) { + packageName = relativeComponents.removeAt(0); + } + + if (relativeComponents.last.endsWith('.dart')) { + _changedDartFiles[packageName] ??= []; + _changedDartFiles[packageName]! + .add(p.posix.joinAll(relativeComponents)); + } + + if (packageName.endsWith(_platformInterfaceSuffix) && + relativeComponents.first == 'pubspec.yaml' && + await _packageWillBePublished(path)) { + _modifiedAndPublishedPlatformInterfacePackages.add(packageName); + } + } + } + + @override + Future runForPackage(RepositoryPackage package) async { + if (!isFlutterPlugin(package)) { + return PackageResult.skip('Not a plugin.'); + } + + if (!package.isFederated) { + return PackageResult.skip('Not a federated plugin.'); + } + + if (package.isPlatformInterface) { + // As the leaf nodes in the graph, a published package interface change is + // assumed to be correct, and other changes are validated against that. + return PackageResult.skip( + 'Platform interface changes are not validated.'); + } + + // Uses basename to match _changedPackageFiles. + final String basePackageName = package.directory.parent.basename; + final String platformInterfacePackageName = + '$basePackageName$_platformInterfaceSuffix'; + final List changedPlatformInterfaceFiles = + _changedDartFiles[platformInterfacePackageName] ?? []; + + if (!_modifiedAndPublishedPlatformInterfacePackages + .contains(platformInterfacePackageName)) { + print('No published changes for $platformInterfacePackageName.'); + return PackageResult.success(); + } + + if (!changedPlatformInterfaceFiles + .any((String path) => path.startsWith('lib/'))) { + print('No public code changes for $platformInterfacePackageName.'); + return PackageResult.success(); + } + + final List changedPackageFiles = + _changedDartFiles[package.directory.basename] ?? []; + if (changedPackageFiles.isEmpty) { + print('No Dart changes.'); + return PackageResult.success(); + } + + // If the change would be flagged, but it appears to be a mass change + // rather than a plugin-specific change, allow it with a warning. + // + // This is a tradeoff between safety and convenience; forcing mass changes + // to be split apart is not ideal, and the assumption is that reviewers are + // unlikely to accidentally approve a PR that is supposed to be changing a + // single plugin, but touches other plugins (vs accidentally approving a + // PR that changes multiple parts of a single plugin, which is a relatively + // easy mistake to make). + // + // 3 is chosen to minimize the chances of accidentally letting something + // through (vs 2, which could be a single-plugin change with one stray + // change to another file accidentally included), while not setting too + // high a bar for detecting mass changes. This can be tuned if there are + // issues with false positives or false negatives. + const int massChangePluginThreshold = 3; + if (_changedPlugins.length >= massChangePluginThreshold) { + logWarning('Ignoring potentially dangerous change, as this appears ' + 'to be a mass change.'); + return PackageResult.success(); + } + + printError('Dart changes are not allowed to other packages in ' + '$basePackageName in the same PR as changes to public Dart code in ' + '$platformInterfacePackageName, as this can cause accidental breaking ' + 'changes to be missed by automated checks. Please split the changes to ' + 'these two packages into separate PRs.\n\n' + 'If you believe that this is a false positive, please file a bug.'); + return PackageResult.fail( + ['$platformInterfacePackageName changed.']); + } + + Future _packageWillBePublished( + String pubspecRepoRelativePosixPath) async { + final File pubspecFile = childFileWithSubcomponents( + packagesDir.parent, p.posix.split(pubspecRepoRelativePosixPath)); + final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + if (pubspec.publishTo == 'none') { + return false; + } + + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + final Version? previousVersion = + await gitVersionFinder.getPackageVersion(pubspecRepoRelativePosixPath); + if (previousVersion == null) { + // The plugin is new, so it will be published. + return true; + } + return pubspec.version != previousVersion; + } +} diff --git a/script/tool/lib/src/firebase_test_lab_command.dart b/script/tool/lib/src/firebase_test_lab_command.dart new file mode 100644 index 000000000000..941cba3a6945 --- /dev/null +++ b/script/tool/lib/src/firebase_test_lab_command.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 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:platform/platform.dart'; +import 'package:uuid/uuid.dart'; + +import 'common/core.dart'; +import 'common/gradle.dart'; +import 'common/package_looping_command.dart'; +import 'common/process_runner.dart'; +import 'common/repository_package.dart'; + +const int _exitGcloudAuthFailed = 2; + +/// A command to run tests via Firebase test lab. +class FirebaseTestLabCommand extends PackageLoopingCommand { + /// Creates an instance of the test runner command. + FirebaseTestLabCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + }) : super(packagesDir, processRunner: processRunner, platform: platform) { + argParser.addOption( + 'project', + defaultsTo: 'flutter-cirrus', + help: 'The Firebase project name.', + ); + final String? homeDir = io.Platform.environment['HOME']; + argParser.addOption('service-key', + defaultsTo: homeDir == null + ? null + : path.join(homeDir, 'gcloud-service-key.json'), + help: 'The path to the service key for gcloud authentication.\n' + r'If not provided, \$HOME/gcloud-service-key.json will be ' + r'assumed if $HOME is set.'); + argParser.addOption('test-run-id', + defaultsTo: const Uuid().v4(), + help: + 'Optional string to append to the results path, to avoid conflicts. ' + 'Randomly chosen on each invocation if none is provided. ' + 'The default shown here is just an example.'); + argParser.addOption('build-id', + defaultsTo: + io.Platform.environment['CIRRUS_BUILD_ID'] ?? 'unknown_build', + help: + 'Optional string to append to the results path, to avoid conflicts. ' + r'Defaults to $CIRRUS_BUILD_ID if that is set.'); + argParser.addMultiOption('device', + splitCommas: false, + defaultsTo: [ + 'model=walleye,version=26', + 'model=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.'; + + bool _firebaseProjectConfigured = false; + + Future _configureFirebaseProject() async { + if (_firebaseProjectConfigured) { + return; + } + + final String serviceKey = getStringArg('service-key'); + if (serviceKey.isEmpty) { + print('No --service-key provided; skipping gcloud authorization'); + } else { + final io.ProcessResult result = await processRunner.run( + 'gcloud', + [ + 'auth', + 'activate-service-account', + '--key-file=$serviceKey', + ], + logOnError: true, + ); + if (result.exitCode != 0) { + printError('Unable to activate gcloud account.'); + throw ToolExit(_exitGcloudAuthFailed); + } + final int exitCode = await processRunner.runAndStream('gcloud', [ + 'config', + 'set', + 'project', + getStringArg('project'), + ]); + print(''); + if (exitCode == 0) { + print('Firebase project configured.'); + } else { + logWarning( + 'Warning: gcloud config set returned a non-zero exit code. Continuing anyway.'); + } + } + _firebaseProjectConfigured = true; + } + + @override + Future runForPackage(RepositoryPackage package) async { + final RepositoryPackage example = package.getSingleExampleDeprecated(); + final Directory androidDirectory = + example.directory.childDirectory('android'); + if (!androidDirectory.existsSync()) { + return PackageResult.skip( + '${example.displayName} does not support Android.'); + } + + if (!androidDirectory + .childDirectory('app') + .childDirectory('src') + .childDirectory('androidTest') + .existsSync()) { + printError('No androidTest directory found.'); + return PackageResult.fail( + ['No tests ran (use --exclude if this is intentional).']); + } + + // Ensures that gradle wrapper exists + final GradleProject project = GradleProject(example.directory, + processRunner: processRunner, platform: platform); + if (!await _ensureGradleWrapperExists(project)) { + return PackageResult.fail(['Unable to build example apk']); + } + + await _configureFirebaseProject(); + + if (!await _runGradle(project, 'app:assembleAndroidTest')) { + return PackageResult.fail(['Unable to assemble androidTest']); + } + + final List errors = []; + + // Used within the loop to ensure a unique GCS output location for each + // test file's run. + int resultsCounter = 0; + for (final File test in _findIntegrationTestFiles(package)) { + final String testName = + getRelativePosixPath(test, from: package.directory); + print('Testing $testName...'); + if (!await _runGradle(project, 'app:assembleDebug', testFile: test)) { + printError('Could not build $testName'); + errors.add('$testName failed to build'); + continue; + } + final String buildId = getStringArg('build-id'); + final String testRunId = getStringArg('test-run-id'); + final String resultsDir = + 'plugins_android_test/${package.displayName}/$buildId/$testRunId/${resultsCounter++}/'; + final List args = [ + 'firebase', + 'test', + 'android', + 'run', + '--type', + 'instrumentation', + '--app', + 'build/app/outputs/apk/debug/app-debug.apk', + '--test', + 'build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk', + '--timeout', + '7m', + '--results-bucket=${getStringArg('results-bucket')}', + '--results-dir=$resultsDir', + ]; + for (final String device in getStringListArg('device')) { + args.addAll(['--device', device]); + } + final int exitCode = await processRunner.runAndStream('gcloud', args, + workingDir: example.directory); + + if (exitCode != 0) { + printError('Test failure for $testName'); + errors.add('$testName failed tests'); + } + } + + if (errors.isEmpty && resultsCounter == 0) { + printError('No integration tests were run.'); + errors.add('No tests ran (use --exclude if this is intentional).'); + } + + return errors.isEmpty + ? PackageResult.success() + : PackageResult.fail(errors); + } + + /// Checks that Gradle has been configured for [project], and if not runs a + /// Flutter build to generate it. + /// + /// Returns true if either gradlew was already present, or the build succeeds. + Future _ensureGradleWrapperExists(GradleProject project) async { + if (!project.isConfigured()) { + print('Running flutter build apk...'); + final String experiment = getStringArg(kEnableExperiment); + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'build', + 'apk', + if (experiment.isNotEmpty) '--enable-experiment=$experiment', + ], + workingDir: project.androidDirectory); + + if (exitCode != 0) { + return false; + } + } + return true; + } + + /// Builds [target] using Gradle in the given [project]. Assumes Gradle is + /// already configured. + /// + /// [testFile] optionally does the Flutter build with the given test file as + /// the build target. + /// + /// Returns true if the command succeeds. + Future _runGradle( + GradleProject project, + String target, { + File? testFile, + }) async { + final String experiment = getStringArg(kEnableExperiment); + final String? extraOptions = experiment.isNotEmpty + ? Uri.encodeComponent('--enable-experiment=$experiment') + : null; + + final int exitCode = await project.runCommand( + target, + arguments: [ + '-Pverbose=true', + if (testFile != null) '-Ptarget=${testFile.path}', + if (extraOptions != null) '-Pextra-front-end-options=$extraOptions', + if (extraOptions != null) '-Pextra-gen-snapshot-options=$extraOptions', + ], + ); + + if (exitCode != 0) { + return false; + } + return true; + } + + /// Finds and returns all integration test files for [package]. + Iterable _findIntegrationTestFiles(RepositoryPackage package) sync* { + final Directory integrationTestDir = package + .getSingleExampleDeprecated() + .directory + .childDirectory('integration_test'); + + if (!integrationTestDir.existsSync()) { + return; + } + + yield* integrationTestDir + .listSync(recursive: true, followLinks: true) + .where((FileSystemEntity file) => + file is File && file.basename.endsWith('_test.dart')) + .cast(); + } +} diff --git a/script/tool/lib/src/format_command.dart b/script/tool/lib/src/format_command.dart new file mode 100644 index 000000000000..f24a99436c87 --- /dev/null +++ b/script/tool/lib/src/format_command.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 'dart:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:http/http.dart' as http; +import 'package:meta/meta.dart'; +import 'package:platform/platform.dart'; + +import 'common/core.dart'; +import 'common/plugin_command.dart'; +import 'common/process_runner.dart'; + +/// In theory this should be 8191, but in practice that was still resulting in +/// "The input line is too long" errors. This was chosen as a value that worked +/// in practice in testing with flutter/plugins, but may need to be adjusted +/// based on further experience. +@visibleForTesting +const int windowsCommandLineMax = 8000; + +/// This value is picked somewhat arbitrarily based on checking `ARG_MAX` on a +/// macOS and Linux machine. If anyone encounters a lower limit in pratice, it +/// can be lowered accordingly. +@visibleForTesting +const int nonWindowsCommandLineMax = 1000000; + +const int _exitClangFormatFailed = 3; +const int _exitFlutterFormatFailed = 4; +const int _exitJavaFormatFailed = 5; +const int _exitGitFailed = 6; +const int _exitDependencyMissing = 7; + +final Uri _googleFormatterUrl = Uri.https('github.com', + '/google/google-java-format/releases/download/google-java-format-1.3/google-java-format-1.3-all-deps.jar'); + +/// A command to format all package code. +class FormatCommand extends PluginCommand { + /// Creates an instance of the format command. + FormatCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + }) : super(packagesDir, processRunner: processRunner, platform: platform) { + argParser.addFlag('fail-on-change', hide: true); + argParser.addOption('clang-format', + defaultsTo: 'clang-format', help: 'Path to "clang-format" executable.'); + argParser.addOption('java', + defaultsTo: 'java', help: 'Path to "java" executable.'); + } + + @override + final String name = 'format'; + + @override + final String description = + 'Formats the code of all packages (Java, Objective-C, C++, and Dart).\n\n' + 'This command requires "git", "flutter" and "clang-format" v5 to be in ' + 'your path.'; + + @override + Future run() async { + final String googleFormatterPath = await _getGoogleFormatterPath(); + + // This class is not based on PackageLoopingCommand because running the + // formatters separately for each package is an order of magnitude slower, + // due to the startup overhead of the formatters. + final Iterable files = + await _getFilteredFilePaths(getFiles(), relativeTo: packagesDir); + await _formatDart(files); + await _formatJava(files, googleFormatterPath); + await _formatCppAndObjectiveC(files); + + if (getBoolArg('fail-on-change')) { + final bool modified = await _didModifyAnything(); + if (modified) { + throw ToolExit(exitCommandFoundErrors); + } + } + } + + Future _didModifyAnything() async { + final io.ProcessResult modifiedFiles = await processRunner.run( + 'git', + ['ls-files', '--modified'], + workingDir: packagesDir, + logOnError: true, + ); + if (modifiedFiles.exitCode != 0) { + printError('Unable to determine changed files.'); + throw ToolExit(_exitGitFailed); + } + + print('\n\n'); + + final String stdout = modifiedFiles.stdout as String; + if (stdout.isEmpty) { + print('All files formatted correctly.'); + return false; + } + + print('These files are not formatted correctly (see diff below):'); + LineSplitter.split(stdout).map((String line) => ' $line').forEach(print); + + print('\nTo fix run "pub global activate flutter_plugin_tools && ' + 'pub global run flutter_plugin_tools format" or copy-paste ' + 'this command into your terminal:'); + + final io.ProcessResult diff = await processRunner.run( + 'git', + ['diff'], + workingDir: packagesDir, + logOnError: true, + ); + if (diff.exitCode != 0) { + printError('Unable to determine diff.'); + throw ToolExit(_exitGitFailed); + } + print('patch -p1 < _formatCppAndObjectiveC(Iterable files) async { + final Iterable clangFiles = _getPathsWithExtensions( + files, {'.h', '.m', '.mm', '.cc', '.cpp'}); + if (clangFiles.isNotEmpty) { + final String clangFormat = getStringArg('clang-format'); + if (!await _hasDependency(clangFormat)) { + printError( + 'Unable to run \'clang-format\'. Make sure that it is in your ' + 'path, or provide a full path with --clang-format.'); + throw ToolExit(_exitDependencyMissing); + } + + print('Formatting .cc, .cpp, .h, .m, and .mm files...'); + final int exitCode = await _runBatched( + getStringArg('clang-format'), ['-i', '--style=Google'], + files: clangFiles); + if (exitCode != 0) { + printError( + 'Failed to format C, C++, and Objective-C files: exit code $exitCode.'); + throw ToolExit(_exitClangFormatFailed); + } + } + } + + Future _formatJava( + Iterable files, String googleFormatterPath) async { + final Iterable javaFiles = + _getPathsWithExtensions(files, {'.java'}); + if (javaFiles.isNotEmpty) { + final String java = getStringArg('java'); + if (!await _hasDependency(java)) { + printError( + 'Unable to run \'java\'. Make sure that it is in your path, or ' + 'provide a full path with --java.'); + throw ToolExit(_exitDependencyMissing); + } + + print('Formatting .java files...'); + final int exitCode = await _runBatched( + java, ['-jar', googleFormatterPath, '--replace'], + files: javaFiles); + if (exitCode != 0) { + printError('Failed to format Java files: exit code $exitCode.'); + throw ToolExit(_exitJavaFormatFailed); + } + } + } + + Future _formatDart(Iterable files) async { + final Iterable dartFiles = + _getPathsWithExtensions(files, {'.dart'}); + if (dartFiles.isNotEmpty) { + print('Formatting .dart files...'); + // `flutter format` doesn't require the project to actually be a Flutter + // project. + final int exitCode = await _runBatched(flutterCommand, ['format'], + files: dartFiles); + if (exitCode != 0) { + printError('Failed to format Dart files: exit code $exitCode.'); + throw ToolExit(_exitFlutterFormatFailed); + } + } + } + + /// Given a stream of [files], returns the paths of any that are not in known + /// locations to ignore, relative to [relativeTo]. + Future> _getFilteredFilePaths( + Stream files, { + required Directory relativeTo, + }) async { + // Returns a pattern to check for [directories] as a subset of a file path. + RegExp pathFragmentForDirectories(List directories) { + String s = path.separator; + // Escape the separator for use in the regex. + if (s == r'\') { + s = r'\\'; + } + return RegExp('(?:^|$s)${path.joinAll(directories)}$s'); + } + + final String fromPath = relativeTo.path; + + // Dart files are allowed to have a pragma to disable auto-formatting. This + // was added because Hixie hurts when dealing with what dartfmt does to + // artisanally-formatted Dart, while Stuart gets really frustrated when + // dealing with PRs from newer contributors who don't know how to make Dart + // readable. After much discussion, it was decided that files in the plugins + // and packages repos that really benefit from hand-formatting (e.g. files + // with large blobs of hex literals) could be opted-out of the requirement + // that they be autoformatted, so long as the code's owner was willing to + // bear the cost of this during code reviews. + // In the event that code ownership moves to someone who does not hold the + // same views as the original owner, the pragma can be removed and the file + // auto-formatted. + const String handFormattedExtension = '.dart'; + const String handFormattedPragma = '// This file is hand-formatted.'; + + return files + .where((File file) { + // See comment above near [handFormattedPragma]. + return path.extension(file.path) != handFormattedExtension || + !file.readAsLinesSync().contains(handFormattedPragma); + }) + .map((File file) => path.relative(file.path, from: fromPath)) + .where((String path) => + // Ignore files in build/ directories (e.g., headers of frameworks) + // to avoid useless extra work in local repositories. + !path.contains( + pathFragmentForDirectories(['example', 'build'])) && + // Ignore files in Pods, which are not part of the repository. + !path.contains(pathFragmentForDirectories(['Pods'])) && + // Ignore .dart_tool/, which can have various intermediate files. + !path.contains(pathFragmentForDirectories(['.dart_tool']))) + .toList(); + } + + Iterable _getPathsWithExtensions( + Iterable files, Set extensions) { + return files.where( + (String filePath) => extensions.contains(path.extension(filePath))); + } + + Future _getGoogleFormatterPath() async { + final String javaFormatterPath = path.join( + path.dirname(path.fromUri(platform.script)), + 'google-java-format-1.3-all-deps.jar'); + final File javaFormatterFile = + packagesDir.fileSystem.file(javaFormatterPath); + + if (!javaFormatterFile.existsSync()) { + print('Downloading Google Java Format...'); + final http.Response response = await http.get(_googleFormatterUrl); + javaFormatterFile.writeAsBytesSync(response.bodyBytes); + } + + return javaFormatterPath; + } + + /// Returns true if [command] can be run successfully. + Future _hasDependency(String command) async { + // Some versions of Java accept both -version and --version, but some only + // accept -version. + final String versionFlag = command == 'java' ? '-version' : '--version'; + try { + final io.ProcessResult result = + await processRunner.run(command, [versionFlag]); + if (result.exitCode != 0) { + return false; + } + } on io.ProcessException { + // Thrown when the binary is missing entirely. + return false; + } + return true; + } + + /// Runs [command] on [arguments] on all of the files in [files], batched as + /// necessary to avoid OS command-line length limits. + /// + /// Returns the exit code of the first failure, which stops the run, or 0 + /// on success. + Future _runBatched( + String command, + List arguments, { + required Iterable files, + }) async { + final int commandLineMax = + platform.isWindows ? windowsCommandLineMax : nonWindowsCommandLineMax; + + // Compute the max length of the file argument portion of a batch. + // Add one to each argument's length for the space before it. + final int argumentTotalLength = + arguments.fold(0, (int sum, String arg) => sum + arg.length + 1); + final int batchMaxTotalLength = + commandLineMax - command.length - argumentTotalLength; + + // Run the command in batches. + final List> batches = + _partitionFileList(files, maxStringLength: batchMaxTotalLength); + for (final List batch in batches) { + batch.sort(); // For ease of testing. + final int exitCode = await processRunner.runAndStream( + command, [...arguments, ...batch], + workingDir: packagesDir); + if (exitCode != 0) { + return exitCode; + } + } + return 0; + } + + /// Partitions [files] into batches whose max string length as parameters to + /// a command (including the spaces between them, and between the list and + /// the command itself) is no longer than [maxStringLength]. + List> _partitionFileList(Iterable files, + {required int maxStringLength}) { + final List> batches = >[[]]; + int currentBatchTotalLength = 0; + for (final String file in files) { + final int length = file.length + 1 /* for the space */; + if (currentBatchTotalLength + length > maxStringLength) { + // Start a new batch. + batches.add([]); + currentBatchTotalLength = 0; + } + batches.last.add(file); + currentBatchTotalLength += length; + } + return batches; + } +} diff --git a/script/tool/lib/src/license_check_command.dart b/script/tool/lib/src/license_check_command.dart new file mode 100644 index 000000000000..7165e985c059 --- /dev/null +++ b/script/tool/lib/src/license_check_command.dart @@ -0,0 +1,274 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; + +import 'common/core.dart'; +import 'common/plugin_command.dart'; + +const Set _codeFileExtensions = { + '.c', + '.cc', + '.cpp', + '.dart', + '.h', + '.html', + '.java', + '.kt', + '.m', + '.mm', + '.swift', + '.sh', +}; + +// Basenames without extensions of files to ignore. +const Set _ignoreBasenameList = { + 'flutter_export_environment', + 'GeneratedPluginRegistrant', + 'generated_plugin_registrant', +}; + +// File suffixes that otherwise match _codeFileExtensions to ignore. +const Set _ignoreSuffixList = { + '.g.dart', // Generated API code. + '.mocks.dart', // Generated by Mockito. +}; + +// Full basenames of files to ignore. +const Set _ignoredFullBasenameList = { + 'resource.h', // Generated by VS. +}; + +// Copyright and license regexes for third-party code. +// +// These are intentionally very simple, since there is very little third-party +// code in this repository. Complexity can be added as-needed on a case-by-case +// basis. +// +// When adding license regexes here, include the copyright info to ensure that +// any new additions are flagged for added scrutiny in review. +final List _thirdPartyLicenseBlockRegexes = [ + // Third-party code used in url_launcher_web. + RegExp( + r'^// Copyright 2017 Workiva Inc\..*' + r'^// Licensed under the Apache License, Version 2\.0', + multiLine: true, + dotAll: true, + ), + // Third-party code used in google_maps_flutter_web. + RegExp( + r'^// The MIT License [^C]+ Copyright \(c\) 2008 Krasimir Tsonev', + multiLine: true, + ), + // bsdiff in flutter/packages. + RegExp( + r'// Copyright 2003-2005 Colin Percival\. All rights reserved\.\n' + r'// Use of this source code is governed by a BSD-style license that can be\n' + r'// found in the LICENSE file\.\n', + ), +]; + +// The exact format of the BSD license that our license files should contain. +// Slight variants are not accepted because they may prevent consolidation in +// tools that assemble all licenses used in distributed applications. +// standardized. +const String _fullBsdLicenseText = ''' +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +'''; + +/// Validates that code files have copyright and license blocks. +class LicenseCheckCommand extends PluginCommand { + /// Creates a new license check command for [packagesDir]. + LicenseCheckCommand(Directory packagesDir) : super(packagesDir); + + @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 allFiles = await _getAllFiles(); + + final Iterable codeFiles = allFiles.where((File file) => + _codeFileExtensions.contains(p.extension(file.path)) && + !_shouldIgnoreFile(file)); + final Iterable firstPartyLicenseFiles = allFiles.where((File file) => + path.basename(file.basename) == 'LICENSE' && !_isThirdParty(file)); + + final List licenseFileFailures = + await _checkLicenseFiles(firstPartyLicenseFiles); + final Map<_LicenseFailureType, List> codeFileFailures = + await _checkCodeLicenses(codeFiles); + + bool passed = true; + + print('\n=======================================\n'); + + if (licenseFileFailures.isNotEmpty) { + passed = false; + printError( + 'The following LICENSE files do not follow the expected format:'); + for (final File file in licenseFileFailures) { + printError(' ${file.path}'); + } + printError('Please ensure that they use the exact format used in this ' + 'repository".\n'); + } + + if (codeFileFailures[_LicenseFailureType.incorrectFirstParty]!.isNotEmpty) { + passed = false; + printError('The license block for these files is missing or incorrect:'); + for (final File file + in codeFileFailures[_LicenseFailureType.incorrectFirstParty]!) { + printError(' ${file.path}'); + } + printError( + 'If this third-party code, move it to a "third_party/" directory, ' + 'otherwise ensure that you are using the exact copyright and license ' + 'text used by all first-party files in this repository.\n'); + } + + if (codeFileFailures[_LicenseFailureType.unknownThirdParty]!.isNotEmpty) { + passed = false; + printError( + 'No recognized license was found for the following third-party files:'); + for (final File file + in codeFileFailures[_LicenseFailureType.unknownThirdParty]!) { + printError(' ${file.path}'); + } + print('Please check that they have a license at the top of the file. ' + 'If they do, the license check needs to be updated to recognize ' + 'the new third-party license block.\n'); + } + + if (!passed) { + throw ToolExit(1); + } + + printSuccess('All files passed validation!'); + } + + // Creates the expected copyright+license block for first-party code. + String _generateLicenseBlock( + String comment, { + String prefix = '', + String suffix = '', + }) { + return '$prefix${comment}Copyright 2013 The Flutter Authors. All rights reserved.\n' + '${comment}Use of this source code is governed by a BSD-style license that can be\n' + '${comment}found in the LICENSE file.$suffix\n'; + } + + /// Checks all license blocks for [codeFiles], returning any that fail + /// validation. + Future>> _checkCodeLicenses( + Iterable codeFiles) async { + final List incorrectFirstPartyFiles = []; + final List unrecognizedThirdPartyFiles = []; + + // Most code file types in the repository use '//' comments. + final String defaultFirstParyLicenseBlock = _generateLicenseBlock('// '); + // A few file types have a different comment structure. + final Map firstPartyLicenseBlockByExtension = + { + '.sh': _generateLicenseBlock('# '), + '.html': _generateLicenseBlock('', prefix: ''), + }; + + for (final File file in codeFiles) { + print('Checking ${file.path}'); + 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); + } + } + } + + // 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); + + return <_LicenseFailureType, List>{ + _LicenseFailureType.incorrectFirstParty: incorrectFirstPartyFiles, + _LicenseFailureType.unknownThirdParty: unrecognizedThirdPartyFiles, + }; + } + + /// Checks all provided LICENSE [files], returning any that fail validation. + Future> _checkLicenseFiles(Iterable files) async { + final List incorrectLicenseFiles = []; + + for (final File file in files) { + print('Checking ${file.path}'); + if (!file.readAsStringSync().contains(_fullBsdLicenseText)) { + incorrectLicenseFiles.add(file); + } + } + + return incorrectLicenseFiles; + } + + bool _shouldIgnoreFile(File file) { + final String path = file.path; + return _ignoreBasenameList.contains(p.basenameWithoutExtension(path)) || + _ignoreSuffixList.any((String suffix) => + path.endsWith(suffix) || + _ignoredFullBasenameList.contains(p.basename(path))); + } + + bool _isThirdParty(File file) { + return path.split(file.path).contains('third_party'); + } + + Future> _getAllFiles() => packagesDir.parent + .list(recursive: true, followLinks: false) + .where((FileSystemEntity entity) => entity is File) + .map((FileSystemEntity file) => file as File) + .toList(); +} + +enum _LicenseFailureType { incorrectFirstParty, unknownThirdParty } diff --git a/script/tool/lib/src/lint_android_command.dart b/script/tool/lib/src/lint_android_command.dart new file mode 100644 index 000000000000..a7b5c4f2e8bf --- /dev/null +++ b/script/tool/lib/src/lint_android_command.dart @@ -0,0 +1,62 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:platform/platform.dart'; + +import 'common/core.dart'; +import 'common/gradle.dart'; +import 'common/package_looping_command.dart'; +import 'common/process_runner.dart'; +import 'common/repository_package.dart'; + +/// Lint the CocoaPod podspecs and run unit tests. +/// +/// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint. +class LintAndroidCommand extends PackageLoopingCommand { + /// Creates an instance of the linter command. + LintAndroidCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + }) : super(packagesDir, processRunner: processRunner, platform: platform); + + @override + final String name = 'lint-android'; + + @override + final String description = 'Runs "gradlew lint" on Android plugins.\n\n' + 'Requires the example to have been build at least once before running.'; + + @override + Future runForPackage(RepositoryPackage package) async { + if (!pluginSupportsPlatform(kPlatformAndroid, package, + requiredMode: PlatformSupport.inline)) { + return PackageResult.skip( + 'Plugin does not have an Android implemenatation.'); + } + + final RepositoryPackage example = package.getSingleExampleDeprecated(); + final GradleProject project = GradleProject(example.directory, + processRunner: processRunner, platform: platform); + + if (!project.isConfigured()) { + return PackageResult.fail(['Build example before linting']); + } + + final String packageName = package.directory.basename; + + // Only lint one build mode to avoid extra work. + // Only lint the plugin project itself, to avoid failing due to errors in + // dependencies. + // + // TODO(stuartmorgan): Consider adding an XML parser to read and summarize + // all results. Currently, only the first three errors will be shown inline, + // and the rest have to be checked via the CI-uploaded artifact. + final int exitCode = await project.runCommand('$packageName:lintDebug'); + + return exitCode == 0 ? PackageResult.success() : PackageResult.fail(); + } +} 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..ee44a82da5b9 --- /dev/null +++ b/script/tool/lib/src/lint_podspecs_command.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:convert'; +import 'dart:io'; + +import 'package:file/file.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; + +import 'common/core.dart'; +import 'common/package_looping_command.dart'; +import 'common/process_runner.dart'; +import 'common/repository_package.dart'; + +const int _exitUnsupportedPlatform = 2; +const int _exitPodNotInstalled = 3; + +/// Lint the CocoaPod podspecs and run unit tests. +/// +/// See https://guides.cocoapods.org/terminal/commands.html#pod_lib_lint. +class LintPodspecsCommand extends PackageLoopingCommand { + /// Creates an instance of the linter command. + LintPodspecsCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + }) : super(packagesDir, processRunner: processRunner, platform: platform) { + argParser.addMultiOption('ignore-warnings', + help: + 'Do not pass --allow-warnings flag to "pod lib lint" for podspecs ' + 'with this basename (example: plugins with known warnings)', + valueHelp: 'podspec_file_name'); + } + + @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.'; + + @override + Future initializeRun() async { + if (!platform.isMacOS) { + printError('This command is only supported on macOS'); + throw ToolExit(_exitUnsupportedPlatform); + } + + final ProcessResult result = await processRunner.run( + 'which', + ['pod'], + workingDir: packagesDir, + logOnError: true, + ); + if (result.exitCode != 0) { + printError('Unable to find "pod". Make sure it is in your path.'); + throw ToolExit(_exitPodNotInstalled); + } + } + + @override + Future runForPackage(RepositoryPackage package) async { + final List errors = []; + + final List podspecs = await _podspecsToLint(package); + if (podspecs.isEmpty) { + return PackageResult.skip('No podspecs.'); + } + + for (final File podspec in podspecs) { + if (!await _lintPodspec(podspec)) { + errors.add(p.basename(podspec.path)); + } + } + return errors.isEmpty + ? PackageResult.success() + : PackageResult.fail(errors); + } + + Future> _podspecsToLint(RepositoryPackage package) async { + final List podspecs = + await getFilesForPackage(package).where((File entity) { + final String filePath = entity.path; + return path.extension(filePath) == '.podspec'; + }).toList(); + + podspecs.sort((File a, File b) => a.basename.compareTo(b.basename)); + return podspecs; + } + + Future _lintPodspec(File podspec) async { + // Do not run the static analyzer on plugins with known analyzer issues. + final String podspecPath = podspec.path; + + final String podspecBasename = 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..e45c09bfd2ef --- /dev/null +++ b/script/tool/lib/src/list_command.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:file/file.dart'; +import 'package:platform/platform.dart'; + +import 'common/plugin_command.dart'; +import 'common/repository_package.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, { + Platform platform = const LocalPlatform(), + }) : super(packagesDir, platform: platform) { + 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 PackageEnumerationEntry entry in getTargetPackages()) { + print(entry.package.path); + } + break; + case _example: + final Stream examples = getTargetPackages() + .expand( + (PackageEnumerationEntry entry) => entry.package.getExamples()); + await for (final RepositoryPackage package in examples) { + print(package.path); + } + break; + case _package: + await for (final PackageEnumerationEntry entry + in getTargetPackagesAndSubpackages()) { + print(entry.package.path); + } + break; + case _file: + await for (final File file in getFiles()) { + print(file.path); + } + break; + } + } +} diff --git a/script/tool/lib/src/main.dart b/script/tool/lib/src/main.dart new file mode 100644 index 000000000000..70a6ab516037 --- /dev/null +++ b/script/tool/lib/src/main.dart @@ -0,0 +1,79 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import '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/core.dart'; +import 'create_all_plugins_app_command.dart'; +import 'drive_examples_command.dart'; +import 'federation_safety_check_command.dart'; +import 'firebase_test_lab_command.dart'; +import 'format_command.dart'; +import 'license_check_command.dart'; +import 'lint_android_command.dart'; +import 'lint_podspecs_command.dart'; +import 'list_command.dart'; +import 'native_test_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 'xcode_analyze_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(FederationSafetyCheckCommand(packagesDir)) + ..addCommand(FirebaseTestLabCommand(packagesDir)) + ..addCommand(FormatCommand(packagesDir)) + ..addCommand(LicenseCheckCommand(packagesDir)) + ..addCommand(LintAndroidCommand(packagesDir)) + ..addCommand(LintPodspecsCommand(packagesDir)) + ..addCommand(ListCommand(packagesDir)) + ..addCommand(NativeTestCommand(packagesDir)) + ..addCommand(PublishCheckCommand(packagesDir)) + ..addCommand(PublishPluginCommand(packagesDir)) + ..addCommand(PubspecCheckCommand(packagesDir)) + ..addCommand(TestCommand(packagesDir)) + ..addCommand(VersionCheckCommand(packagesDir)) + ..addCommand(XcodeAnalyzeCommand(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/native_test_command.dart b/script/tool/lib/src/native_test_command.dart new file mode 100644 index 000000000000..4911b4aeb156 --- /dev/null +++ b/script/tool/lib/src/native_test_command.dart @@ -0,0 +1,587 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:platform/platform.dart'; + +import 'common/core.dart'; +import 'common/gradle.dart'; +import 'common/package_looping_command.dart'; +import 'common/plugin_utils.dart'; +import 'common/process_runner.dart'; +import 'common/repository_package.dart'; +import 'common/xcode.dart'; + +const String _unitTestFlag = 'unit'; +const String _integrationTestFlag = 'integration'; + +const String _iosDestinationFlag = 'ios-destination'; + +const int _exitNoIosSimulators = 3; + +/// The command to run native tests for plugins: +/// - iOS and macOS: XCTests (XCUnitTest and XCUITest) +/// - Android: JUnit tests +/// - Windows and Linux: GoogleTest tests +class NativeTestCommand extends PackageLoopingCommand { + /// Creates an instance of the test command. + NativeTestCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + }) : _xcode = Xcode(processRunner: processRunner, log: true), + super(packagesDir, processRunner: processRunner, platform: platform) { + argParser.addOption( + _iosDestinationFlag, + help: 'Specify the destination when running iOS tests.\n' + 'This is passed to the `-destination` argument in the xcodebuild command.\n' + 'See https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-UNIT ' + 'for details on how to specify the destination.', + ); + argParser.addFlag(kPlatformAndroid, help: 'Runs Android tests'); + argParser.addFlag(kPlatformIos, help: 'Runs iOS tests'); + argParser.addFlag(kPlatformLinux, help: 'Runs Linux tests'); + argParser.addFlag(kPlatformMacos, help: 'Runs macOS tests'); + argParser.addFlag(kPlatformWindows, help: 'Runs Windows tests'); + + // By default, both unit tests and integration tests are run, but provide + // flags to disable one or the other. + argParser.addFlag(_unitTestFlag, + help: 'Runs native unit tests', defaultsTo: true); + argParser.addFlag(_integrationTestFlag, + help: 'Runs native integration (UI) tests', defaultsTo: true); + } + + // The device destination flags for iOS tests. + List _iosDestinationFlags = []; + + final Xcode _xcode; + + @override + final String name = 'native-test'; + + @override + final String description = ''' +Runs native unit tests and native integration tests. + +Currently supported platforms: +- Android +- iOS: requires 'xcrun' to be in your path. +- Linux (unit tests only) +- macOS: requires 'xcrun' to be in your path. +- Windows (unit tests only) + +The example app(s) must be built for all targeted platforms before running +this command. +'''; + + Map _platforms = {}; + + List _requestedPlatforms = []; + + @override + Future initializeRun() async { + _platforms = { + kPlatformAndroid: _PlatformDetails('Android', _testAndroid), + kPlatformIos: _PlatformDetails('iOS', _testIos), + kPlatformLinux: _PlatformDetails('Linux', _testLinux), + kPlatformMacos: _PlatformDetails('macOS', _testMacOS), + kPlatformWindows: _PlatformDetails('Windows', _testWindows), + }; + _requestedPlatforms = _platforms.keys + .where((String platform) => getBoolArg(platform)) + .toList(); + _requestedPlatforms.sort(); + + if (_requestedPlatforms.isEmpty) { + printError('At least one platform flag must be provided.'); + throw ToolExit(exitInvalidArguments); + } + + if (!(getBoolArg(_unitTestFlag) || getBoolArg(_integrationTestFlag))) { + printError('At least one test type must be enabled.'); + throw ToolExit(exitInvalidArguments); + } + + if (getBoolArg(kPlatformWindows) && getBoolArg(_integrationTestFlag)) { + logWarning('This command currently only supports unit tests for Windows. ' + 'See https://github.com/flutter/flutter/issues/70233.'); + } + + if (getBoolArg(kPlatformLinux) && getBoolArg(_integrationTestFlag)) { + logWarning('This command currently only supports unit tests for Linux. ' + 'See https://github.com/flutter/flutter/issues/70235.'); + } + + // iOS-specific run-level state. + if (_requestedPlatforms.contains('ios')) { + String destination = getStringArg(_iosDestinationFlag); + if (destination.isEmpty) { + final String? simulatorId = + await _xcode.findBestAvailableIphoneSimulator(); + if (simulatorId == null) { + printError('Cannot find any available iOS simulators.'); + throw ToolExit(_exitNoIosSimulators); + } + destination = 'id=$simulatorId'; + } + _iosDestinationFlags = [ + '-destination', + destination, + ]; + } + } + + @override + Future runForPackage(RepositoryPackage package) async { + final List testPlatforms = []; + for (final String platform in _requestedPlatforms) { + if (!pluginSupportsPlatform(platform, package, + requiredMode: PlatformSupport.inline)) { + print('No implementation for ${_platforms[platform]!.label}.'); + continue; + } + if (!pluginHasNativeCodeForPlatform(platform, package)) { + print('No native code for ${_platforms[platform]!.label}.'); + continue; + } + testPlatforms.add(platform); + } + + if (testPlatforms.isEmpty) { + return PackageResult.skip('Nothing to test for target platform(s).'); + } + + final _TestMode mode = _TestMode( + unit: getBoolArg(_unitTestFlag), + integration: getBoolArg(_integrationTestFlag), + ); + + bool ranTests = false; + bool failed = false; + final List failureMessages = []; + for (final String platform in testPlatforms) { + final _PlatformDetails platformInfo = _platforms[platform]!; + print('Running tests for ${platformInfo.label}...'); + print('----------------------------------------'); + final _PlatformResult result = + await platformInfo.testFunction(package, mode); + ranTests |= result.state != RunState.skipped; + if (result.state == RunState.failed) { + failed = true; + + final String? error = result.error; + // Only provide the failing platforms in the failure details if testing + // multiple platforms, otherwise it's just noise. + if (_requestedPlatforms.length > 1) { + failureMessages.add(error != null + ? '${platformInfo.label}: $error' + : platformInfo.label); + } else if (error != null) { + // If there's only one platform, only provide error details in the + // summary if the platform returned a message. + failureMessages.add(error); + } + } + } + + if (!ranTests) { + return PackageResult.skip('No tests found.'); + } + return failed + ? PackageResult.fail(failureMessages) + : PackageResult.success(); + } + + Future<_PlatformResult> _testAndroid( + RepositoryPackage plugin, _TestMode mode) async { + bool exampleHasUnitTests(RepositoryPackage example) { + return example.directory + .childDirectory('android') + .childDirectory('app') + .childDirectory('src') + .childDirectory('test') + .existsSync() || + example.directory.parent + .childDirectory('android') + .childDirectory('src') + .childDirectory('test') + .existsSync(); + } + + bool exampleHasNativeIntegrationTests(RepositoryPackage example) { + final Directory integrationTestDirectory = example.directory + .childDirectory('android') + .childDirectory('app') + .childDirectory('src') + .childDirectory('androidTest'); + // There are two types of integration tests that can be in the androidTest + // directory: + // - FlutterTestRunner.class tests, which bridge to Dart integration tests + // - Purely native tests + // Only the latter is supported by this command; the former will hang if + // run here because they will wait for a Dart call that will never come. + // + // This repository uses a convention of putting the former in a + // *ActivityTest.java file, so ignore that file when checking for tests. + // Also ignore DartIntegrationTest.java, which defines the annotation used + // below for filtering the former out when running tests. + // + // If those are the only files, then there are no tests to run here. + return integrationTestDirectory.existsSync() && + integrationTestDirectory + .listSync(recursive: true) + .whereType() + .any((File file) { + final String basename = file.basename; + return !basename.endsWith('ActivityTest.java') && + basename != 'DartIntegrationTest.java'; + }); + } + + final Iterable examples = plugin.getExamples(); + + bool ranUnitTests = false; + bool ranAnyTests = false; + bool failed = false; + bool hasMissingBuild = false; + for (final RepositoryPackage example in examples) { + final bool hasUnitTests = exampleHasUnitTests(example); + final bool hasIntegrationTests = + exampleHasNativeIntegrationTests(example); + + if (mode.unit && !hasUnitTests) { + _printNoExampleTestsMessage(example, 'Android unit'); + } + if (mode.integration && !hasIntegrationTests) { + _printNoExampleTestsMessage(example, 'Android integration'); + } + + final bool runUnitTests = mode.unit && hasUnitTests; + final bool runIntegrationTests = mode.integration && hasIntegrationTests; + if (!runUnitTests && !runIntegrationTests) { + continue; + } + + final String exampleName = example.displayName; + _printRunningExampleTestsMessage(example, 'Android'); + + final GradleProject project = GradleProject( + example.directory, + processRunner: processRunner, + platform: platform, + ); + if (!project.isConfigured()) { + printError('ERROR: Run "flutter build apk" on $exampleName, or run ' + 'this tool\'s "build-examples --apk" command, ' + 'before executing tests.'); + failed = true; + hasMissingBuild = true; + continue; + } + + if (runUnitTests) { + print('Running unit tests...'); + final int exitCode = await project.runCommand('testDebugUnitTest'); + if (exitCode != 0) { + printError('$exampleName unit tests failed.'); + failed = true; + } + ranUnitTests = true; + ranAnyTests = true; + } + + if (runIntegrationTests) { + // FlutterTestRunner-based tests will hang forever if run in a normal + // app build, since they wait for a Dart call from integration_test that + // will never come. Those tests have an extra annotation to allow + // filtering them out. + const String filter = + 'notAnnotation=io.flutter.plugins.DartIntegrationTest'; + + print('Running integration tests...'); + final int exitCode = await project.runCommand( + 'app:connectedAndroidTest', + arguments: [ + '-Pandroid.testInstrumentationRunnerArguments.$filter', + ], + ); + if (exitCode != 0) { + printError('$exampleName integration tests failed.'); + failed = true; + } + ranAnyTests = true; + } + } + + if (failed) { + return _PlatformResult(RunState.failed, + error: hasMissingBuild + ? 'Examples must be built before testing.' + : null); + } + if (!mode.integrationOnly && !ranUnitTests) { + printError('No unit tests ran. Plugins are required to have unit tests.'); + return _PlatformResult(RunState.failed, + error: 'No unit tests ran (use --exclude if this is intentional).'); + } + if (!ranAnyTests) { + return _PlatformResult(RunState.skipped); + } + return _PlatformResult(RunState.succeeded); + } + + Future<_PlatformResult> _testIos(RepositoryPackage plugin, _TestMode mode) { + return _runXcodeTests(plugin, 'iOS', mode, + extraFlags: _iosDestinationFlags); + } + + Future<_PlatformResult> _testMacOS(RepositoryPackage plugin, _TestMode mode) { + return _runXcodeTests(plugin, 'macOS', mode); + } + + /// Runs all applicable tests for [plugin], printing status and returning + /// the test result. + /// + /// The tests targets must be added to the Xcode project of the example app, + /// usually at "example/{ios,macos}/Runner.xcworkspace". + Future<_PlatformResult> _runXcodeTests( + RepositoryPackage plugin, + String platform, + _TestMode mode, { + List extraFlags = const [], + }) async { + String? testTarget; + const String unitTestTarget = 'RunnerTests'; + if (mode.unitOnly) { + testTarget = unitTestTarget; + } else if (mode.integrationOnly) { + testTarget = 'RunnerUITests'; + } + + bool ranUnitTests = false; + // Assume skipped until at least one test has run. + RunState overallResult = RunState.skipped; + for (final RepositoryPackage example in plugin.getExamples()) { + final String exampleName = example.displayName; + + // If running a specific target, check that. Otherwise, check if there + // are unit tests, since having no unit tests for a plugin is fatal + // (by repo policy) even if there are integration tests. + bool exampleHasUnitTests = false; + final String? targetToCheck = + testTarget ?? (mode.unit ? unitTestTarget : null); + final Directory xcodeProject = example.directory + .childDirectory(platform.toLowerCase()) + .childDirectory('Runner.xcodeproj'); + if (targetToCheck != null) { + final bool? hasTarget = + await _xcode.projectHasTarget(xcodeProject, targetToCheck); + if (hasTarget == null) { + printError('Unable to check targets for $exampleName.'); + overallResult = RunState.failed; + continue; + } else if (!hasTarget) { + print('No "$targetToCheck" target in $exampleName; skipping.'); + continue; + } else if (targetToCheck == unitTestTarget) { + exampleHasUnitTests = true; + } + } + + _printRunningExampleTestsMessage(example, platform); + final int exitCode = await _xcode.runXcodeBuild( + example.directory, + actions: ['test'], + workspace: '${platform.toLowerCase()}/Runner.xcworkspace', + scheme: 'Runner', + configuration: 'Debug', + extraFlags: [ + if (testTarget != null) '-only-testing:$testTarget', + ...extraFlags, + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + ); + + // The exit code from 'xcodebuild test' when there are no tests. + const int _xcodebuildNoTestExitCode = 66; + switch (exitCode) { + case _xcodebuildNoTestExitCode: + _printNoExampleTestsMessage(example, platform); + break; + case 0: + printSuccess('Successfully ran $platform xctest for $exampleName'); + // If this is the first test, assume success until something fails. + if (overallResult == RunState.skipped) { + overallResult = RunState.succeeded; + } + if (exampleHasUnitTests) { + ranUnitTests = true; + } + break; + default: + // Any failure means a failure overall. + overallResult = RunState.failed; + // If unit tests ran, note that even if they failed. + if (exampleHasUnitTests) { + ranUnitTests = true; + } + break; + } + } + + if (!mode.integrationOnly && !ranUnitTests) { + printError('No unit tests ran. Plugins are required to have unit tests.'); + // Only return a specific summary error message about the missing unit + // tests if there weren't also failures, to avoid having a misleadingly + // specific message. + if (overallResult != RunState.failed) { + return _PlatformResult(RunState.failed, + error: 'No unit tests ran (use --exclude if this is intentional).'); + } + } + + return _PlatformResult(overallResult); + } + + Future<_PlatformResult> _testWindows( + RepositoryPackage plugin, _TestMode mode) async { + if (mode.integrationOnly) { + return _PlatformResult(RunState.skipped); + } + + bool isTestBinary(File file) { + return file.basename.endsWith('_test.exe') || + file.basename.endsWith('_tests.exe'); + } + + return _runGoogleTestTests(plugin, + buildDirectoryName: 'windows', isTestBinary: isTestBinary); + } + + Future<_PlatformResult> _testLinux( + RepositoryPackage plugin, _TestMode mode) async { + if (mode.integrationOnly) { + return _PlatformResult(RunState.skipped); + } + + bool isTestBinary(File file) { + return file.basename.endsWith('_test') || + file.basename.endsWith('_tests'); + } + + return _runGoogleTestTests(plugin, + buildDirectoryName: 'linux', isTestBinary: isTestBinary); + } + + /// Finds every file in the [buildDirectoryName] subdirectory of [plugin]'s + /// build directory for which [isTestBinary] is true, and runs all of them, + /// returning the overall result. + /// + /// The binaries are assumed to be Google Test test binaries, thus returning + /// zero for success and non-zero for failure. + Future<_PlatformResult> _runGoogleTestTests( + RepositoryPackage plugin, { + required String buildDirectoryName, + required bool Function(File) isTestBinary, + }) async { + final List testBinaries = []; + for (final RepositoryPackage example in plugin.getExamples()) { + final Directory buildDir = example.directory + .childDirectory('build') + .childDirectory(buildDirectoryName); + if (!buildDir.existsSync()) { + continue; + } + testBinaries.addAll(buildDir + .listSync(recursive: true) + .whereType() + .where(isTestBinary) + .where((File file) { + // Only run the release build of the unit tests, to avoid running the + // same tests multiple times. Release is used rather than debug since + // `build-examples` builds release versions. + final List components = path.split(file.path); + return components.contains('release') || components.contains('Release'); + })); + } + + if (testBinaries.isEmpty) { + final String binaryExtension = platform.isWindows ? '.exe' : ''; + printError( + 'No test binaries found. At least one *_test(s)$binaryExtension ' + 'binary should be built by the example(s)'); + return _PlatformResult(RunState.failed, + error: 'No $buildDirectoryName unit tests found'); + } + + bool passing = true; + for (final File test in testBinaries) { + print('Running ${test.basename}...'); + final int exitCode = + await processRunner.runAndStream(test.path, []); + passing &= exitCode == 0; + } + return _PlatformResult(passing ? RunState.succeeded : RunState.failed); + } + + /// Prints a standard format message indicating that [platform] tests for + /// [plugin]'s [example] are about to be run. + void _printRunningExampleTestsMessage( + RepositoryPackage example, String platform) { + print('Running $platform tests for ${example.displayName}...'); + } + + /// Prints a standard format message indicating that no tests were found for + /// [plugin]'s [example] for [platform]. + void _printNoExampleTestsMessage(RepositoryPackage example, String platform) { + print('No $platform tests found for ${example.displayName}'); + } +} + +// The type for a function that takes a plugin directory and runs its native +// tests for a specific platform. +typedef _TestFunction = Future<_PlatformResult> Function( + RepositoryPackage, _TestMode); + +/// A collection of information related to a specific platform. +class _PlatformDetails { + const _PlatformDetails( + this.label, + this.testFunction, + ); + + /// The name to use in output. + final String label; + + /// The function to call to run tests. + final _TestFunction testFunction; +} + +/// Enabled state for different test types. +class _TestMode { + const _TestMode({required this.unit, required this.integration}); + + final bool unit; + final bool integration; + + bool get integrationOnly => integration && !unit; + bool get unitOnly => unit && !integration; +} + +/// The result of running a single platform's tests. +class _PlatformResult { + _PlatformResult(this.state, {this.error}); + + /// The overall state of the platform's tests. This should be: + /// - failed if any tests failed. + /// - succeeded if at least one test ran, and all tests passed. + /// - skipped if no tests ran. + final RunState state; + + /// An optional error string to include in the summary for this platform. + /// + /// Ignored unless [state] is `failed`. + final String? error; +} diff --git a/script/tool/lib/src/publish_check_command.dart b/script/tool/lib/src/publish_check_command.dart new file mode 100644 index 000000000000..563e0904552a --- /dev/null +++ b/script/tool/lib/src/publish_check_command.dart @@ -0,0 +1,278 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:http/http.dart' as http; +import 'package:platform/platform.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; + +import 'common/core.dart'; +import 'common/package_looping_command.dart'; +import 'common/process_runner.dart'; +import 'common/pub_version_finder.dart'; +import 'common/repository_package.dart'; + +/// A command to check that packages are publishable via 'dart publish'. +class PublishCheckCommand extends PackageLoopingCommand { + /// Creates an instance of the publish command. + PublishCheckCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + http.Client? httpClient, + }) : _pubVersionFinder = + PubVersionFinder(httpClient: httpClient ?? http.Client()), + super(packagesDir, processRunner: processRunner, platform: platform) { + argParser.addFlag( + _allowPrereleaseFlag, + help: 'Allows the pre-release SDK warning to pass.\n' + 'When enabled, a pub warning, which asks to publish the package as a pre-release version when ' + 'the SDK constraint is a pre-release version, is ignored.', + 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'; + + @override + final String name = 'publish-check'; + + @override + final String description = + 'Checks to make sure that a plugin *could* be published.'; + + final PubVersionFinder _pubVersionFinder; + + /// The overall result of the run for machine-readable output. This is the + /// highest value that occurs during the run. + _PublishCheckResult _overallResult = _PublishCheckResult.nothingToPublish; + + @override + bool get captureOutput => getBoolArg(_machineFlag); + + @override + Future initializeRun() async { + _overallResult = _PublishCheckResult.nothingToPublish; + } + + @override + Future runForPackage(RepositoryPackage package) async { + _PublishCheckResult? result = await _passesPublishCheck(package); + if (result == null) { + return PackageResult.skip('Package is marked as unpublishable.'); + } + if (!_passesAuthorsCheck(package)) { + _printImportantStatusMessage( + 'No AUTHORS file found. Packages must include an AUTHORS file.', + isError: true); + result = _PublishCheckResult.error; + } + + if (result.index > _overallResult.index) { + _overallResult = result; + } + return result == _PublishCheckResult.error + ? PackageResult.fail() + : PackageResult.success(); + } + + @override + Future completeRun() async { + _pubVersionFinder.httpClient.close(); + } + + @override + Future handleCapturedOutput(List output) async { + final Map machineOutput = { + _statusKey: _statusStringForResult(_overallResult), + _humanMessageKey: output, + }; + + print(const JsonEncoder.withIndent(' ').convert(machineOutput)); + } + + String _statusStringForResult(_PublishCheckResult result) { + switch (result) { + case _PublishCheckResult.nothingToPublish: + return _statusMessageNoPublish; + case _PublishCheckResult.needsPublishing: + return _statusNeedsPublish; + case _PublishCheckResult.error: + return _statusMessageError; + } + } + + Pubspec? _tryParsePubspec(RepositoryPackage package) { + final File pubspecFile = package.pubspecFile; + + try { + return Pubspec.parse(pubspecFile.readAsStringSync()); + } on Exception catch (exception) { + print( + 'Failed to parse `pubspec.yaml` at ${pubspecFile.path}: $exception}', + ); + return null; + } + } + + Future _hasValidPublishCheckRun(RepositoryPackage package) async { + print('Running pub publish --dry-run:'); + final io.Process process = await processRunner.start( + flutterCommand, + ['pub', 'publish', '--', '--dry-run'], + workingDirectory: package.directory, + ); + + final StringBuffer outputBuffer = StringBuffer(); + + final Completer stdOutCompleter = Completer(); + process.stdout.listen( + (List event) { + final String output = String.fromCharCodes(event); + if (output.isNotEmpty) { + print(output); + outputBuffer.write(output); + } + }, + onDone: () => stdOutCompleter.complete(), + ); + + final Completer stdInCompleter = Completer(); + process.stderr.listen( + (List event) { + final String output = String.fromCharCodes(event); + if (output.isNotEmpty) { + // The final result is always printed on stderr, whether success or + // failure. + final bool isError = !output.contains('has 0 warnings'); + _printImportantStatusMessage(output, isError: isError); + outputBuffer.write(output); + } + }, + onDone: () => stdInCompleter.complete(), + ); + + if (await process.exitCode == 0) { + return true; + } + + if (!getBoolArg(_allowPrereleaseFlag)) { + return false; + } + + await stdOutCompleter.future; + await stdInCompleter.future; + + final String output = outputBuffer.toString(); + return output.contains('Package has 1 warning') && + output.contains( + 'Packages with an SDK constraint on a pre-release of the Dart SDK should themselves be published as a pre-release version.'); + } + + /// Returns the result of the publish check, or null if the package is marked + /// as unpublishable. + Future<_PublishCheckResult?> _passesPublishCheck( + RepositoryPackage package) async { + final String packageName = package.directory.basename; + final Pubspec? pubspec = _tryParsePubspec(package); + if (pubspec == null) { + print('No valid pubspec found.'); + return _PublishCheckResult.error; + } else if (pubspec.publishTo == 'none') { + return null; + } + + final Version? version = pubspec.version; + final _PublishCheckResult alreadyPublishedResult = + await _checkPublishingStatus( + packageName: packageName, version: version); + if (alreadyPublishedResult == _PublishCheckResult.nothingToPublish) { + print( + 'Package $packageName version: $version has already be published on pub.'); + return alreadyPublishedResult; + } else if (alreadyPublishedResult == _PublishCheckResult.error) { + print('Check pub version failed $packageName'); + return _PublishCheckResult.error; + } + + if (await _hasValidPublishCheckRun(package)) { + print('Package $packageName is able to be published.'); + return _PublishCheckResult.needsPublishing; + } else { + print('Unable to publish $packageName'); + return _PublishCheckResult.error; + } + } + + // Check if `packageName` already has `version` published on pub. + Future<_PublishCheckResult> _checkPublishingStatus( + {required String packageName, required Version? version}) async { + final PubVersionFinderResponse pubVersionFinderResponse = + await _pubVersionFinder.getPackageVersion(packageName: packageName); + switch (pubVersionFinderResponse.result) { + case PubVersionFinderResult.success: + return pubVersionFinderResponse.versions.contains(version) + ? _PublishCheckResult.nothingToPublish + : _PublishCheckResult.needsPublishing; + case PubVersionFinderResult.fail: + print(''' +Error fetching version on pub for $packageName. +HTTP Status ${pubVersionFinderResponse.httpResponse.statusCode} +HTTP response: ${pubVersionFinderResponse.httpResponse.body} +'''); + return _PublishCheckResult.error; + case PubVersionFinderResult.noPackageFound: + return _PublishCheckResult.needsPublishing; + } + } + + bool _passesAuthorsCheck(RepositoryPackage package) { + final List pathComponents = + package.directory.fileSystem.path.split(package.directory.path); + if (pathComponents.contains('third_party')) { + // Third-party packages aren't required to have an AUTHORS file. + return true; + } + return package.directory.childFile('AUTHORS').existsSync(); + } + + void _printImportantStatusMessage(String message, {required bool isError}) { + final String statusMessage = '${isError ? 'ERROR' : 'SUCCESS'}: $message'; + if (getBoolArg(_machineFlag)) { + print(statusMessage); + } else { + if (isError) { + printError(statusMessage); + } else { + printSuccess(statusMessage); + } + } + } +} + +/// Possible outcomes of of a publishing check. +enum _PublishCheckResult { + nothingToPublish, + needsPublishing, + error, +} diff --git a/script/tool/lib/src/publish_plugin_command.dart b/script/tool/lib/src/publish_plugin_command.dart new file mode 100644 index 000000000000..4fdecf603eec --- /dev/null +++ b/script/tool/lib/src/publish_plugin_command.dart @@ -0,0 +1,460 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:http/http.dart' as http; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; +import 'package:yaml/yaml.dart'; + +import 'common/core.dart'; +import 'common/file_utils.dart'; +import 'common/git_version_finder.dart'; +import 'common/package_looping_command.dart'; +import 'common/plugin_command.dart'; +import 'common/process_runner.dart'; +import 'common/pub_version_finder.dart'; +import 'common/repository_package.dart'; + +@immutable +class _RemoteInfo { + const _RemoteInfo({required this.name, required this.url}); + + /// The git name for the remote. + final String name; + + /// The remote's URL. + final String url; +} + +/// Wraps pub publish with a few niceties used by the flutter/plugin team. +/// +/// 1. Checks for any modified files in git and refuses to publish if there's an +/// issue. +/// 2. Tags the release with the format -v. +/// 3. Pushes the release to a remote. +/// +/// Both 2 and 3 are optional, see `plugin_tools help publish-plugin` for full +/// usage information. +/// +/// [processRunner], [print], and [stdin] can be overriden for easier testing. +class PublishPluginCommand extends PackageLoopingCommand { + /// Creates an instance of the publish command. + PublishPluginCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + io.Stdin? stdinput, + GitDir? gitDir, + http.Client? httpClient, + }) : _pubVersionFinder = + PubVersionFinder(httpClient: httpClient ?? http.Client()), + _stdin = stdinput ?? io.stdin, + super(packagesDir, + platform: platform, processRunner: processRunner, gitDir: gitDir) { + argParser.addMultiOption(_pubFlagsOption, + help: + 'A list of options that will be forwarded on to pub. Separate multiple flags with commas.'); + argParser.addOption( + _remoteOption, + help: 'The name of the remote to push the tags to.', + // Flutter convention is to use "upstream" for the single source of truth, and "origin" for personal forks. + defaultsTo: 'upstream', + ); + argParser.addFlag( + _allChangedFlag, + help: + 'Release all packages that contains pubspec changes at the current commit compares to the base-sha.\n' + 'The --packages option is ignored if this is on.', + 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', + defaultsTo: false, + negatable: true); + } + + 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 packages and tag the release(s) on GitHub.\n' + 'If running this on CI, an environment variable named $_pubCredentialName must be set to a String that represents the pub credential JSON.\n' + 'WARNING: Do not check in the content of pub credential JSON, it should only come from secure sources.'; + + final io.Stdin _stdin; + StreamSubscription? _stdinSubscription; + final PubVersionFinder _pubVersionFinder; + + // Tags that already exist in the repository. + List _existingGitTags = []; + // The remote to push tags to. + late _RemoteInfo _remote; + + @override + String get successSummaryMessage => 'published'; + + @override + String get failureListHeader => + 'The following packages had failures during publishing:'; + + @override + Future initializeRun() async { + print('Checking local repo...'); + + // Ensure that the requested remote is present. + final String remoteName = getStringArg(_remoteOption); + final String? remoteUrl = await _verifyRemote(remoteName); + if (remoteUrl == null) { + printError('Unable to find URL for remote $remoteName; cannot push tags'); + throw ToolExit(1); + } + _remote = _RemoteInfo(name: remoteName, url: remoteUrl); + + // Pre-fetch all the repository's tags, to check against when publishing. + final GitDir repository = await gitDir; + final io.ProcessResult existingTagsResult = + await repository.runCommand(['tag', '--sort=-committerdate']); + _existingGitTags = (existingTagsResult.stdout as String).split('\n') + ..removeWhere((String element) => element.isEmpty); + + if (getBoolArg(_dryRunFlag)) { + print('=============== DRY RUN ==============='); + } + } + + @override + Stream getPackagesToProcess() async* { + if (getBoolArg(_allChangedFlag)) { + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + final String baseSha = await gitVersionFinder.getBaseSha(); + print( + 'Publishing all packages that have changed relative to "$baseSha"\n'); + final List changedPubspecs = + await gitVersionFinder.getChangedPubSpecs(); + + for (final String pubspecPath in changedPubspecs) { + // git outputs a relativa, Posix-style path. + final File pubspecFile = childFileWithSubcomponents( + packagesDir.fileSystem.directory((await gitDir).path), + p.posix.split(pubspecPath)); + yield PackageEnumerationEntry(RepositoryPackage(pubspecFile.parent), + excluded: false); + } + } else { + yield* getTargetPackages(filterExcluded: false); + } + } + + @override + Future runForPackage(RepositoryPackage package) async { + final PackageResult? checkResult = await _checkNeedsRelease(package); + if (checkResult != null) { + return checkResult; + } + + if (!await _checkGitStatus(package)) { + return PackageResult.fail(['uncommitted changes']); + } + + if (!await _publish(package)) { + return PackageResult.fail(['publish failed']); + } + + if (!await _tagRelease(package)) { + return PackageResult.fail(['tagging failed']); + } + + print('\nPublished ${package.directory.basename} successfully!'); + return PackageResult.success(); + } + + @override + Future completeRun() async { + _pubVersionFinder.httpClient.close(); + await _stdinSubscription?.cancel(); + _stdinSubscription = null; + } + + /// Checks whether [package] needs to be released, printing check status and + /// returning one of: + /// - PackageResult.fail if the check could not be completed + /// - PackageResult.skip if no release is necessary + /// - null if releasing should proceed + /// + /// In cases where a non-null result is returned, that should be returned + /// as the final result for the package, without further processing. + Future _checkNeedsRelease(RepositoryPackage package) async { + final File pubspecFile = package.pubspecFile; + if (!pubspecFile.existsSync()) { + logWarning(''' +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 PackageResult.skip('package deleted'); + } + + final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + + if (pubspec.name == 'flutter_plugin_tools') { + // Ignore flutter_plugin_tools package when running publishing through flutter_plugin_tools. + // TODO(cyanglaz): Make the tool also auto publish flutter_plugin_tools package. + // https://github.com/flutter/flutter/issues/85430 + return PackageResult.skip( + 'publishing flutter_plugin_tools via the tool is not supported'); + } + + if (pubspec.publishTo == 'none') { + return PackageResult.skip('publish_to: none'); + } + + if (pubspec.version == null) { + printError( + 'No version found. A package that intentionally has no version should be marked "publish_to: none"'); + return PackageResult.fail(['no version']); + } + + // Check if the package named `packageName` with `version` has already + // been published. + final Version version = pubspec.version!; + final PubVersionFinderResponse pubVersionFinderResponse = + await _pubVersionFinder.getPackageVersion(packageName: pubspec.name); + if (pubVersionFinderResponse.versions.contains(version)) { + final String tagsForPackageWithSameVersion = _existingGitTags.firstWhere( + (String tag) => + tag.split('-v').first == pubspec.name && + tag.split('-v').last == version.toString(), + orElse: () => ''); + if (tagsForPackageWithSameVersion.isEmpty) { + printError( + '${pubspec.name} $version has already been published, however ' + 'the git release tag (${pubspec.name}-v$version) was not found. ' + 'Please manually fix the tag then run the command again.'); + return PackageResult.fail(['published but untagged']); + } else { + print('${pubspec.name} $version has already been published.'); + return PackageResult.skip('already published'); + } + } + return null; + } + + // Tag the release with -v, and push it to the remote. + // + // Return `true` if successful, `false` otherwise. + Future _tagRelease(RepositoryPackage package) async { + final String tag = _getTag(package); + print('Tagging release $tag...'); + if (!getBoolArg(_dryRunFlag)) { + final io.ProcessResult result = await (await gitDir).runCommand( + ['tag', tag], + throwOnError: false, + ); + if (result.exitCode != 0) { + return false; + } + } + + print('Pushing tag to ${_remote.name}...'); + final bool success = await _pushTagToRemote( + tag: tag, + remote: _remote, + ); + if (success) { + print('Release tagged!'); + } + return success; + } + + Future _checkGitStatus(RepositoryPackage package) async { + final io.ProcessResult statusResult = await (await gitDir).runCommand( + [ + 'status', + '--porcelain', + '--ignored', + package.directory.absolute.path + ], + throwOnError: false, + ); + if (statusResult.exitCode != 0) { + return false; + } + + final String statusOutput = statusResult.stdout as String; + if (statusOutput.isNotEmpty) { + printError( + "There are files in the package directory that haven't been saved in git. Refusing to publish these files:\n\n" + '$statusOutput\n' + 'If the directory should be clean, you can run `git clean -xdf && git reset --hard HEAD` to wipe all local changes.'); + } + return statusOutput.isEmpty; + } + + Future _verifyRemote(String remote) async { + final io.ProcessResult getRemoteUrlResult = await (await gitDir).runCommand( + ['remote', 'get-url', remote], + throwOnError: false, + ); + if (getRemoteUrlResult.exitCode != 0) { + return null; + } + return getRemoteUrlResult.stdout as String?; + } + + Future _publish(RepositoryPackage package) async { + print('Publishing...'); + final List publishFlags = getStringListArg(_pubFlagsOption); + print('Running `pub publish ${publishFlags.join(' ')}` in ' + '${package.directory.absolute.path}...\n'); + if (getBoolArg(_dryRunFlag)) { + return true; + } + + if (getBoolArg(_skipConfirmationFlag)) { + publishFlags.add('--force'); + } + if (publishFlags.contains('--force')) { + _ensureValidPubCredential(); + } + + final io.Process publish = await processRunner.start( + flutterCommand, ['pub', 'publish'] + publishFlags, + workingDirectory: package.directory); + publish.stdout.transform(utf8.decoder).listen((String data) => print(data)); + publish.stderr.transform(utf8.decoder).listen((String data) => print(data)); + _stdinSubscription ??= _stdin + .transform(utf8.decoder) + .listen((String data) => publish.stdin.writeln(data)); + final int result = await publish.exitCode; + if (result != 0) { + printError('Publishing ${package.directory.basename} failed.'); + return false; + } + + print('Package published!'); + return true; + } + + String _getTag(RepositoryPackage package) { + final File pubspecFile = package.pubspecFile; + final YamlMap pubspecYaml = + loadYaml(pubspecFile.readAsStringSync()) as YamlMap; + final String name = pubspecYaml['name'] as String; + final String version = pubspecYaml['version'] as String; + // We should have failed to publish if these were unset. + assert(name.isNotEmpty && version.isNotEmpty); + return _tagFormat + .replaceAll('%PACKAGE%', name) + .replaceAll('%VERSION%', version); + } + + // Pushes the `tag` to `remote` + // + // Return `true` if successful, `false` otherwise. + Future _pushTagToRemote({ + required String tag, + required _RemoteInfo remote, + }) async { + assert(remote != null && tag != null); + if (!getBoolArg(_dryRunFlag)) { + final io.ProcessResult result = await (await gitDir).runCommand( + ['push', remote.name, tag], + throwOnError: false, + ); + if (result.exitCode != 0) { + return false; + } + } + return true; + } + + void _ensureValidPubCredential() { + final String credentialsPath = _credentialsPath; + final File credentialFile = packagesDir.fileSystem.file(credentialsPath); + if (credentialFile.existsSync() && + credentialFile.readAsStringSync().isNotEmpty) { + return; + } + final String? credential = io.Platform.environment[_pubCredentialName]; + if (credential == null) { + printError(''' +No pub credential available. Please check if `$credentialsPath` is valid. +If running this command on CI, you can set the pub credential content in the $_pubCredentialName environment variable. +'''); + throw ToolExit(1); + } + credentialFile.openSync(mode: FileMode.writeOnlyAppend) + ..writeStringSync(credential) + ..closeSync(); + } + + /// Returns the correct path where the pub credential is stored. + @visibleForTesting + static String getCredentialPath() { + return _credentialsPath; + } +} + +/// The path in which pub expects to find its credentials file. +final String _credentialsPath = () { + // This follows the same logic as pub: + // https://github.com/dart-lang/pub/blob/d99b0d58f4059d7bb4ac4616fd3d54ec00a2b5d4/lib/src/system_cache.dart#L34-L43 + String? cacheDir; + final String? pubCache = io.Platform.environment['PUB_CACHE']; + if (pubCache != null) { + cacheDir = pubCache; + } else if (io.Platform.isWindows) { + final String? appData = io.Platform.environment['APPDATA']; + if (appData == null) { + printError('"APPDATA" environment variable is not set.'); + } else { + cacheDir = p.join(appData, 'Pub', 'Cache'); + } + } else { + final String? home = io.Platform.environment['HOME']; + if (home == null) { + printError('"HOME" environment variable is not set.'); + } else { + cacheDir = p.join(home, '.pub-cache'); + } + } + + if (cacheDir == null) { + printError('Unable to determine pub cache location'); + throw ToolExit(1); + } + + return p.join(cacheDir, 'credentials.json'); +}(); diff --git a/script/tool/lib/src/pubspec_check_command.dart b/script/tool/lib/src/pubspec_check_command.dart new file mode 100644 index 000000000000..b99f5af68c45 --- /dev/null +++ b/script/tool/lib/src/pubspec_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 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:platform/platform.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; + +import 'common/core.dart'; +import 'common/package_looping_command.dart'; +import 'common/process_runner.dart'; +import 'common/repository_package.dart'; + +/// A command to enforce pubspec conventions across the repository. +/// +/// This both ensures that repo best practices for which optional fields are +/// used are followed, and that the structure is consistent to make edits +/// across multiple pubspec files easier. +class PubspecCheckCommand extends PackageLoopingCommand { + /// Creates an instance of the version check command. + PubspecCheckCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + GitDir? gitDir, + }) : super( + packagesDir, + processRunner: processRunner, + platform: platform, + gitDir: gitDir, + ); + + // Section order for plugins. Because the 'flutter' section is critical + // information for plugins, and usually small, it goes near the top unlike in + // a normal app or package. + static const List _majorPluginSections = [ + 'environment:', + 'flutter:', + 'dependencies:', + 'dev_dependencies:', + 'false_secrets:', + ]; + + static const List _majorPackageSections = [ + 'environment:', + 'dependencies:', + 'dev_dependencies:', + 'flutter:', + 'false_secrets:', + ]; + + static const String _expectedIssueLinkFormat = + 'https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A'; + + @override + final String name = 'pubspec-check'; + + @override + final String description = + 'Checks that pubspecs follow repository conventions.'; + + @override + bool get hasLongOutput => false; + + @override + bool get includeSubpackages => true; + + @override + Future runForPackage(RepositoryPackage package) async { + final File pubspec = package.pubspecFile; + final bool passesCheck = + !pubspec.existsSync() || await _checkPubspec(pubspec, package: package); + if (!passesCheck) { + return PackageResult.fail(); + } + return PackageResult.success(); + } + + Future _checkPubspec( + File pubspecFile, { + required RepositoryPackage package, + }) async { + final String contents = pubspecFile.readAsStringSync(); + final Pubspec? pubspec = _tryParsePubspec(contents); + if (pubspec == null) { + return false; + } + + final List pubspecLines = contents.split('\n'); + final bool isPlugin = pubspec.flutter?.containsKey('plugin') ?? false; + final List sectionOrder = + isPlugin ? _majorPluginSections : _majorPackageSections; + bool passing = _checkSectionOrder(pubspecLines, sectionOrder); + if (!passing) { + printError('${indentation}Major sections should follow standard ' + 'repository ordering:'); + final String listIndentation = indentation * 2; + printError('$listIndentation${sectionOrder.join('\n$listIndentation')}'); + } + + if (isPlugin) { + final String? error = _checkForImplementsError(pubspec, package: package); + if (error != null) { + printError('$indentation$error'); + passing = false; + } + } + + // Ignore metadata that's only relevant for published packages if the + // packages is not intended for publishing. + if (pubspec.publishTo != 'none') { + final List repositoryErrors = + _checkForRepositoryLinkErrors(pubspec, package: package); + if (repositoryErrors.isNotEmpty) { + for (final String error in repositoryErrors) { + printError('$indentation$error'); + } + passing = false; + } + + if (!_checkIssueLink(pubspec)) { + printError( + '${indentation}A package should have an "issue_tracker" link to a ' + 'search for open flutter/flutter bugs with the relevant label:\n' + '${indentation * 2}$_expectedIssueLinkFormat'); + passing = false; + } + + // Don't check descriptions for federated package components other than + // the app-facing package, since they are unlisted, and are expected to + // have short descriptions. + if (!package.isPlatformInterface && !package.isPlatformImplementation) { + final String? descriptionError = + _checkDescription(pubspec, package: package); + if (descriptionError != null) { + printError('$indentation$descriptionError'); + passing = false; + } + } + } + + return passing; + } + + Pubspec? _tryParsePubspec(String pubspecContents) { + try { + return Pubspec.parse(pubspecContents); + } on Exception catch (exception) { + print(' Cannot parse pubspec.yaml: $exception'); + } + return null; + } + + bool _checkSectionOrder( + List pubspecLines, List sectionOrder) { + int previousSectionIndex = 0; + for (final String line in pubspecLines) { + final int index = sectionOrder.indexOf(line); + if (index == -1) { + continue; + } + if (index < previousSectionIndex) { + return false; + } + previousSectionIndex = index; + } + return true; + } + + List _checkForRepositoryLinkErrors( + Pubspec pubspec, { + required RepositoryPackage package, + }) { + final List errorMessages = []; + if (pubspec.repository == null) { + errorMessages.add('Missing "repository"'); + } else { + final String relativePackagePath = + path.relative(package.path, from: packagesDir.parent.path); + if (!pubspec.repository!.path.endsWith(relativePackagePath)) { + errorMessages + .add('The "repository" link should end with the package path.'); + } + } + + if (pubspec.homepage != null) { + errorMessages + .add('Found a "homepage" entry; only "repository" should be used.'); + } + + return errorMessages; + } + + // Validates the "description" field for a package, returning an error + // string if there are any issues. + String? _checkDescription( + Pubspec pubspec, { + required RepositoryPackage package, + }) { + final String? description = pubspec.description; + if (description == null) { + return 'Missing "description"'; + } + + if (description.length < 60) { + return '"description" is too short. pub.dev recommends package ' + 'descriptions of 60-180 characters.'; + } + if (description.length > 180) { + return '"description" is too long. pub.dev recommends package ' + 'descriptions of 60-180 characters.'; + } + } + + bool _checkIssueLink(Pubspec pubspec) { + return pubspec.issueTracker + ?.toString() + .startsWith(_expectedIssueLinkFormat) == + true; + } + + // Validates the "implements" keyword for a plugin, returning an error + // string if there are any issues. + // + // Should only be called on plugin packages. + String? _checkForImplementsError( + Pubspec pubspec, { + required RepositoryPackage package, + }) { + if (_isImplementationPackage(package)) { + final String? implements = + pubspec.flutter!['plugin']!['implements'] as String?; + final String expectedImplements = package.directory.parent.basename; + if (implements == null) { + return 'Missing "implements: $expectedImplements" in "plugin" section.'; + } else if (implements != expectedImplements) { + return 'Expecetd "implements: $expectedImplements"; ' + 'found "implements: $implements".'; + } + } + return null; + } + + // Returns true if [packageName] appears to be an implementation package + // according to repository conventions. + bool _isImplementationPackage(RepositoryPackage package) { + if (!package.isFederated) { + return false; + } + final String packageName = package.directory.basename; + final String parentName = package.directory.parent.basename; + // A few known package names are not implementation packages; assume + // anything else is. (This is done instead of listing known implementation + // suffixes to allow for non-standard suffixes; e.g., to put several + // platforms in one package for code-sharing purposes.) + const Set nonImplementationSuffixes = { + '', // App-facing package. + '_platform_interface', // Platform interface package. + }; + final String suffix = packageName.substring(parentName.length); + return !nonImplementationSuffixes.contains(suffix); + } +} diff --git a/script/tool/lib/src/test_command.dart b/script/tool/lib/src/test_command.dart new file mode 100644 index 000000000000..5a0b43d3b223 --- /dev/null +++ b/script/tool/lib/src/test_command.dart @@ -0,0 +1,101 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:platform/platform.dart'; + +import 'common/core.dart'; +import 'common/package_looping_command.dart'; +import 'common/plugin_utils.dart'; +import 'common/process_runner.dart'; +import 'common/repository_package.dart'; + +/// A command to run Dart unit tests for packages. +class TestCommand extends PackageLoopingCommand { + /// Creates an instance of the test command. + TestCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + }) : super(packagesDir, processRunner: processRunner, platform: platform) { + argParser.addOption( + kEnableExperiment, + defaultsTo: '', + help: + 'Runs Dart unit tests in Dart VM with the given experiments enabled. ' + 'See https://github.com/dart-lang/sdk/blob/master/docs/process/experimental-flags.md ' + 'for details.', + ); + } + + @override + final String name = 'test'; + + @override + final String description = 'Runs the Dart tests for all packages.\n\n' + 'This command requires "flutter" to be in your path.'; + + @override + Future runForPackage(RepositoryPackage package) async { + if (!package.directory.childDirectory('test').existsSync()) { + return PackageResult.skip('No test/ directory.'); + } + + bool passed; + if (isFlutterPackage(package.directory)) { + passed = await _runFlutterTests(package); + } else { + passed = await _runDartTests(package); + } + return passed ? PackageResult.success() : PackageResult.fail(); + } + + /// Runs the Dart tests for a Flutter package, returning true on success. + Future _runFlutterTests(RepositoryPackage package) async { + final String experiment = getStringArg(kEnableExperiment); + + final int exitCode = await processRunner.runAndStream( + flutterCommand, + [ + 'test', + '--color', + if (experiment.isNotEmpty) '--enable-experiment=$experiment', + // TODO(ditman): Remove this once all plugins are migrated to 'drive'. + if (pluginSupportsPlatform(kPlatformWeb, package)) '--platform=chrome', + ], + workingDir: package.directory, + ); + return exitCode == 0; + } + + /// Runs the Dart tests for a non-Flutter package, returning true on success. + Future _runDartTests(RepositoryPackage package) async { + // Unlike `flutter test`, `pub run test` does not automatically get + // packages + int exitCode = await processRunner.runAndStream( + 'dart', + ['pub', 'get'], + workingDir: package.directory, + ); + if (exitCode != 0) { + printError('Unable to fetch dependencies.'); + return false; + } + + final String experiment = getStringArg(kEnableExperiment); + + exitCode = await processRunner.runAndStream( + 'dart', + [ + 'pub', + 'run', + if (experiment.isNotEmpty) '--enable-experiment=$experiment', + 'test', + ], + workingDir: package.directory, + ); + + return exitCode == 0; + } +} 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..90ba0668002b --- /dev/null +++ b/script/tool/lib/src/version_check_command.dart @@ -0,0 +1,477 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:git/git.dart'; +import 'package:http/http.dart' as http; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:pubspec_parse/pubspec_parse.dart'; + +import 'common/core.dart'; +import 'common/git_version_finder.dart'; +import 'common/package_looping_command.dart'; +import 'common/process_runner.dart'; +import 'common/pub_version_finder.dart'; +import 'common/repository_package.dart'; + +const int _exitMissingChangeDescriptionFile = 3; + +/// Categories of version change types. +enum NextVersionType { + /// A breaking change. + BREAKING_MAJOR, + + /// A minor change (e.g., added feature). + MINOR, + + /// A bugfix change. + PATCH, + + /// The release of an existing prerelease version. + RELEASE, +} + +/// The state of a package's version relative to the comparison base. +enum _CurrentVersionState { + /// The version is unchanged. + unchanged, + + /// The version has changed, and the transition is valid. + validChange, + + /// The version has changed, and the transition is invalid. + invalidChange, + + /// There was an error determining the version state. + unknown, +} + +/// Returns the set of allowed next versions, with their change type, for +/// [version]. +/// +/// [newVersion] is used to check whether this is a pre-1.0 version bump, as +/// those have different semver rules. +@visibleForTesting +Map getAllowedNextVersions( + Version version, { + required Version newVersion, +}) { + final Map allowedNextVersions = + { + version.nextMajor: NextVersionType.BREAKING_MAJOR, + version.nextMinor: NextVersionType.MINOR, + version.nextPatch: NextVersionType.PATCH, + }; + + if (version.major < 1 && newVersion.major < 1) { + int nextBuildNumber = -1; + if (version.build.isEmpty) { + nextBuildNumber = 1; + } else { + final int currentBuildNumber = version.build.first as int; + nextBuildNumber = currentBuildNumber + 1; + } + final Version preReleaseVersion = Version( + version.major, + version.minor, + version.patch, + build: nextBuildNumber.toString(), + ); + allowedNextVersions.clear(); + allowedNextVersions[version.nextMajor] = NextVersionType.RELEASE; + allowedNextVersions[version.nextMinor] = NextVersionType.BREAKING_MAJOR; + allowedNextVersions[version.nextPatch] = NextVersionType.MINOR; + allowedNextVersions[preReleaseVersion] = NextVersionType.PATCH; + } + return allowedNextVersions; +} + +/// A command to validate version changes to packages. +class VersionCheckCommand extends PackageLoopingCommand { + /// Creates an instance of the version check command. + VersionCheckCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + GitDir? gitDir, + http.Client? httpClient, + }) : _pubVersionFinder = + PubVersionFinder(httpClient: httpClient ?? http.Client()), + super( + packagesDir, + processRunner: processRunner, + platform: platform, + gitDir: gitDir, + ) { + argParser.addFlag( + _againstPubFlag, + help: 'Whether the version check should run against the version on pub.\n' + 'Defaults to false, which means the version check only run against ' + 'the previous version in code.', + defaultsTo: false, + negatable: true, + ); + argParser.addOption(_changeDescriptionFile, + help: 'The path to a file containing the description of the change ' + '(e.g., PR description or commit message).\n\n' + 'If supplied, this is used to allow overrides to some version ' + 'checks.'); + argParser.addFlag(_ignorePlatformInterfaceBreaks, + help: 'Bypasses the check that platform interfaces do not contain ' + 'breaking changes.\n\n' + 'This is only intended for use in post-submit CI checks, to ' + 'prevent the possibility of post-submit breakage if a change ' + 'description justification is not transferred into the commit ' + 'message. Pre-submit checks should always use ' + '--$_changeDescriptionFile instead.', + hide: true); + } + + static const String _againstPubFlag = 'against-pub'; + static const String _changeDescriptionFile = 'change-description-file'; + static const String _ignorePlatformInterfaceBreaks = + 'ignore-platform-interface-breaks'; + + /// The string that must be in [_changeDescriptionFile] to allow a breaking + /// change to a platform interface. + static const String _breakingChangeJustificationMarker = + '## Breaking change justification'; + + final PubVersionFinder _pubVersionFinder; + + @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.'; + + @override + bool get hasLongOutput => false; + + @override + Future initializeRun() async {} + + @override + Future runForPackage(RepositoryPackage package) async { + final Pubspec? pubspec = _tryParsePubspec(package); + if (pubspec == null) { + // No remaining checks make sense, so fail immediately. + return PackageResult.fail(['Invalid pubspec.yaml.']); + } + + if (pubspec.publishTo == 'none') { + return PackageResult.skip('Found "publish_to: none".'); + } + + final Version? currentPubspecVersion = pubspec.version; + if (currentPubspecVersion == null) { + printError('${indentation}No version found in pubspec.yaml. A package ' + 'that intentionally has no version should be marked ' + '"publish_to: none".'); + // No remaining checks make sense, so fail immediately. + return PackageResult.fail(['No pubspec.yaml version.']); + } + + final List errors = []; + + bool versionChanged; + final _CurrentVersionState versionState = + await _getVersionState(package, pubspec: pubspec); + switch (versionState) { + case _CurrentVersionState.unchanged: + versionChanged = false; + break; + case _CurrentVersionState.validChange: + versionChanged = true; + break; + case _CurrentVersionState.invalidChange: + versionChanged = true; + errors.add('Disallowed version change.'); + break; + case _CurrentVersionState.unknown: + versionChanged = false; + errors.add('Unable to determine previous version.'); + break; + } + + if (!(await _validateChangelogVersion(package, + pubspec: pubspec, pubspecVersionChanged: versionChanged))) { + errors.add('CHANGELOG.md failed validation.'); + } + + return errors.isEmpty + ? PackageResult.success() + : PackageResult.fail(errors); + } + + @override + Future completeRun() async { + _pubVersionFinder.httpClient.close(); + } + + /// Returns the previous published version of [package]. + /// + /// [packageName] must be the actual name of the package as published (i.e., + /// the name from pubspec.yaml, not the on disk name if different.) + Future _fetchPreviousVersionFromPub(String packageName) async { + final PubVersionFinderResponse pubVersionFinderResponse = + await _pubVersionFinder.getPackageVersion(packageName: packageName); + switch (pubVersionFinderResponse.result) { + case PubVersionFinderResult.success: + return pubVersionFinderResponse.versions.first; + case PubVersionFinderResult.fail: + printError(''' +${indentation}Error fetching version on pub for $packageName. +${indentation}HTTP Status ${pubVersionFinderResponse.httpResponse.statusCode} +${indentation}HTTP response: ${pubVersionFinderResponse.httpResponse.body} +'''); + return null; + case PubVersionFinderResult.noPackageFound: + return Version.none; + } + } + + /// Returns the version of [package] from git at the base comparison hash. + Future _getPreviousVersionFromGit( + RepositoryPackage package, { + required GitVersionFinder gitVersionFinder, + }) async { + final File pubspecFile = package.pubspecFile; + final String relativePath = + path.relative(pubspecFile.absolute.path, from: (await gitDir).path); + // Use Posix-style paths for git. + final String gitPath = path.style == p.Style.windows + ? p.posix.joinAll(path.split(relativePath)) + : relativePath; + return await gitVersionFinder.getPackageVersion(gitPath); + } + + /// Returns the state of the verison of [package] relative to the comparison + /// base (git or pub, depending on flags). + Future<_CurrentVersionState> _getVersionState( + RepositoryPackage package, { + required Pubspec pubspec, + }) async { + // This method isn't called unless `version` is non-null. + final Version currentVersion = pubspec.version!; + Version? previousVersion; + if (getBoolArg(_againstPubFlag)) { + previousVersion = await _fetchPreviousVersionFromPub(pubspec.name); + if (previousVersion == null) { + return _CurrentVersionState.unknown; + } + if (previousVersion != Version.none) { + print( + '$indentation${pubspec.name}: Current largest version on pub: $previousVersion'); + } + } else { + final GitVersionFinder gitVersionFinder = await retrieveVersionFinder(); + previousVersion = await _getPreviousVersionFromGit(package, + gitVersionFinder: gitVersionFinder) ?? + Version.none; + } + if (previousVersion == Version.none) { + print('${indentation}Unable to find previous version ' + '${getBoolArg(_againstPubFlag) ? 'on pub server' : 'at git base'}.'); + logWarning( + '${indentation}If this plugin is not new, something has gone wrong.'); + return _CurrentVersionState.validChange; // Assume new, thus valid. + } + + if (previousVersion == currentVersion) { + print('${indentation}No version change.'); + return _CurrentVersionState.unchanged; + } + + // Check for reverts when doing local validation. + if (!getBoolArg(_againstPubFlag) && currentVersion < previousVersion) { + final Map possibleVersionsFromNewVersion = + getAllowedNextVersions(currentVersion, newVersion: previousVersion); + // Since this skips validation, try to ensure that it really is likely + // to be a revert rather than a typo by checking that the transition + // from the lower version to the new version would have been valid. + if (possibleVersionsFromNewVersion.containsKey(previousVersion)) { + logWarning('${indentation}New version is lower than previous version. ' + 'This is assumed to be a revert.'); + return _CurrentVersionState.validChange; + } + } + + final Map allowedNextVersions = + getAllowedNextVersions(previousVersion, newVersion: currentVersion); + + if (allowedNextVersions.containsKey(currentVersion)) { + print('$indentation$previousVersion -> $currentVersion'); + } else { + final String source = (getBoolArg(_againstPubFlag)) ? 'pub' : 'master'; + printError('${indentation}Incorrectly updated version.\n' + '${indentation}HEAD: $currentVersion, $source: $previousVersion.\n' + '${indentation}Allowed versions: $allowedNextVersions'); + return _CurrentVersionState.invalidChange; + } + + if (allowedNextVersions[currentVersion] == NextVersionType.BREAKING_MAJOR && + !_validateBreakingChange(package)) { + printError('${indentation}Breaking change detected.\n' + '${indentation}Breaking changes to platform interfaces are not ' + 'allowed without explicit justification.\n' + '${indentation}See ' + 'https://github.com/flutter/flutter/wiki/Contributing-to-Plugins-and-Packages ' + 'for more information.'); + return _CurrentVersionState.invalidChange; + } + + return _CurrentVersionState.validChange; + } + + /// Checks whether or not [package]'s CHANGELOG's versioning is correct, + /// both that it matches [pubspec] and that NEXT is used correctly, printing + /// the results of its checks. + /// + /// Returns false if the CHANGELOG fails validation. + Future _validateChangelogVersion( + RepositoryPackage package, { + required Pubspec pubspec, + required bool pubspecVersionChanged, + }) async { + // This method isn't called unless `version` is non-null. + final Version fromPubspec = pubspec.version!; + + // get first version from CHANGELOG + final File changelog = package.directory.childFile('CHANGELOG.md'); + final List lines = changelog.readAsLinesSync(); + String? firstLineWithText; + final Iterator iterator = lines.iterator; + while (iterator.moveNext()) { + if (iterator.current.trim().isNotEmpty) { + firstLineWithText = iterator.current.trim(); + break; + } + } + // Remove all leading mark down syntax from the version line. + String? versionString = firstLineWithText?.split(' ').last; + + final String badNextErrorMessage = '${indentation}When bumping the version ' + 'for release, the NEXT section should be incorporated into the new ' + 'version\'s release notes.'; + + // Skip validation for the special NEXT version that's used to accumulate + // changes that don't warrant publishing on their own. + final bool hasNextSection = versionString == 'NEXT'; + if (hasNextSection) { + // NEXT should not be present in a commit that changes the version. + if (pubspecVersionChanged) { + printError(badNextErrorMessage); + return false; + } + print( + '${indentation}Found NEXT; validating next version in the CHANGELOG.'); + // Ensure that the version in pubspec hasn't changed without updating + // CHANGELOG. That means the next version entry in the CHANGELOG should + // pass the normal validation. + versionString = null; + while (iterator.moveNext()) { + if (iterator.current.trim().startsWith('## ')) { + versionString = iterator.current.trim().split(' ').last; + break; + } + } + } + + if (versionString == null) { + printError('${indentation}Unable to find a version in CHANGELOG.md'); + print('${indentation}The current version should be on a line starting ' + 'with "## ", either on the first non-empty line or after a "## NEXT" ' + 'section.'); + return false; + } + + final Version fromChangeLog; + try { + fromChangeLog = Version.parse(versionString); + } on FormatException { + printError('"$versionString" could not be parsed as a version.'); + return false; + } + + if (fromPubspec != fromChangeLog) { + printError(''' +${indentation}Versions in CHANGELOG.md and pubspec.yaml do not match. +${indentation}The version in pubspec.yaml is $fromPubspec. +${indentation}The first version listed in CHANGELOG.md is $fromChangeLog. +'''); + return false; + } + + // If NEXT wasn't the first section, it should not exist at all. + if (!hasNextSection) { + final RegExp nextRegex = RegExp(r'^#+\s*NEXT\s*$'); + if (lines.any((String line) => nextRegex.hasMatch(line))) { + printError(badNextErrorMessage); + return false; + } + } + + return true; + } + + Pubspec? _tryParsePubspec(RepositoryPackage package) { + final File pubspecFile = package.pubspecFile; + + try { + final Pubspec pubspec = Pubspec.parse(pubspecFile.readAsStringSync()); + return pubspec; + } on Exception catch (exception) { + printError('${indentation}Failed to parse `pubspec.yaml`: $exception}'); + return null; + } + } + + /// Checks whether the current breaking change to [package] should be allowed, + /// logging extra information for auditing when allowing unusual cases. + bool _validateBreakingChange(RepositoryPackage package) { + // Only platform interfaces have breaking change restrictions. + if (!package.isPlatformInterface) { + return true; + } + + if (getBoolArg(_ignorePlatformInterfaceBreaks)) { + logWarning( + '${indentation}Allowing breaking change to ${package.displayName} ' + 'due to --$_ignorePlatformInterfaceBreaks'); + return true; + } + + if (_getChangeDescription().contains(_breakingChangeJustificationMarker)) { + logWarning( + '${indentation}Allowing breaking change to ${package.displayName} ' + 'due to "$_breakingChangeJustificationMarker" in the change ' + 'description.'); + return true; + } + + return false; + } + + /// Returns the contents of the file pointed to by [_changeDescriptionFile], + /// or an empty string if that flag is not provided. + String _getChangeDescription() { + final String path = getStringArg(_changeDescriptionFile); + if (path.isEmpty) { + return ''; + } + final File file = packagesDir.fileSystem.file(path); + if (!file.existsSync()) { + printError('${indentation}No such file: $path'); + throw ToolExit(_exitMissingChangeDescriptionFile); + } + return file.readAsStringSync(); + } +} diff --git a/script/tool/lib/src/xcode_analyze_command.dart b/script/tool/lib/src/xcode_analyze_command.dart new file mode 100644 index 000000000000..3d34dab9f087 --- /dev/null +++ b/script/tool/lib/src/xcode_analyze_command.dart @@ -0,0 +1,112 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:platform/platform.dart'; + +import 'common/core.dart'; +import 'common/package_looping_command.dart'; +import 'common/plugin_utils.dart'; +import 'common/process_runner.dart'; +import 'common/repository_package.dart'; +import 'common/xcode.dart'; + +/// The command to run Xcode's static analyzer on plugins. +class XcodeAnalyzeCommand extends PackageLoopingCommand { + /// Creates an instance of the test command. + XcodeAnalyzeCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + }) : _xcode = Xcode(processRunner: processRunner, log: true), + super(packagesDir, processRunner: processRunner, platform: platform) { + argParser.addFlag(kPlatformIos, help: 'Analyze iOS'); + argParser.addFlag(kPlatformMacos, help: 'Analyze macOS'); + } + + final Xcode _xcode; + + @override + final String name = 'xcode-analyze'; + + @override + final String description = + 'Runs Xcode analysis on the iOS and/or macOS example apps.'; + + @override + Future initializeRun() async { + if (!(getBoolArg(kPlatformIos) || getBoolArg(kPlatformMacos))) { + printError('At least one platform flag must be provided.'); + throw ToolExit(exitInvalidArguments); + } + } + + @override + Future runForPackage(RepositoryPackage package) async { + final bool testIos = getBoolArg(kPlatformIos) && + pluginSupportsPlatform(kPlatformIos, package, + requiredMode: PlatformSupport.inline); + final bool testMacos = getBoolArg(kPlatformMacos) && + pluginSupportsPlatform(kPlatformMacos, package, + requiredMode: PlatformSupport.inline); + + final bool multiplePlatformsRequested = + getBoolArg(kPlatformIos) && getBoolArg(kPlatformMacos); + if (!(testIos || testMacos)) { + return PackageResult.skip('Not implemented for target platform(s).'); + } + + final List failures = []; + if (testIos && + !await _analyzePlugin(package, 'iOS', extraFlags: [ + '-destination', + 'generic/platform=iOS Simulator' + ])) { + failures.add('iOS'); + } + if (testMacos && !await _analyzePlugin(package, 'macOS')) { + failures.add('macOS'); + } + + // Only provide the failing platform in the failure details if testing + // multiple platforms, otherwise it's just noise. + return failures.isEmpty + ? PackageResult.success() + : PackageResult.fail( + multiplePlatformsRequested ? failures : []); + } + + /// Analyzes [plugin] for [platform], returning true if it passed analysis. + Future _analyzePlugin( + RepositoryPackage plugin, + String platform, { + List extraFlags = const [], + }) async { + bool passing = true; + for (final RepositoryPackage example in plugin.getExamples()) { + // Running tests and static analyzer. + final String examplePath = getRelativePosixPath(example.directory, + from: plugin.directory.parent); + print('Running $platform tests and analyzer for $examplePath...'); + final int exitCode = await _xcode.runXcodeBuild( + example.directory, + actions: ['analyze'], + workspace: '${platform.toLowerCase()}/Runner.xcworkspace', + scheme: 'Runner', + configuration: 'Debug', + extraFlags: [ + ...extraFlags, + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + ); + if (exitCode == 0) { + printSuccess('$examplePath ($platform) passed analysis.'); + } else { + printError('$examplePath ($platform) failed analysis.'); + passing = false; + } + } + return passing; + } +} diff --git a/script/tool/pubspec.yaml b/script/tool/pubspec.yaml new file mode 100644 index 000000000000..689618f06123 --- /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.7.1 + +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..502fa9a0634c --- /dev/null +++ b/script/tool/test/analyze_command_test.dart @@ -0,0 +1,293 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/analyze_command.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late RecordingProcessRunner processRunner; + late CommandRunner runner; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final AnalyzeCommand analyzeCommand = AnalyzeCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = CommandRunner('analyze_command', 'Test for analyze_command'); + runner.addCommand(analyzeCommand); + }); + + test('analyzes all packages', () async { + final Directory plugin1Dir = createFakePlugin('a', packagesDir); + final Directory plugin2Dir = createFakePlugin('b', packagesDir); + + await runCapturingPrint(runner, ['analyze']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', const ['packages', 'get'], plugin1Dir.path), + ProcessCall( + 'flutter', const ['packages', 'get'], plugin2Dir.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], + plugin1Dir.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], + plugin2Dir.path), + ])); + }); + + test('skips flutter pub get for examples', () async { + final Directory plugin1Dir = createFakePlugin('a', packagesDir); + + await runCapturingPrint(runner, ['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); + + await runCapturingPrint(runner, ['analyze']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', const ['packages', 'get'], plugin1Dir.path), + ProcessCall( + 'flutter', const ['packages', 'get'], plugin2Dir.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], + plugin1Dir.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], + plugin2Dir.path), + ])); + }); + + test('uses a separate analysis sdk', () async { + final Directory pluginDir = createFakePlugin('a', packagesDir); + + await runCapturingPrint( + runner, ['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, + extraFiles: ['analysis_options.yaml']); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['analyze'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Found an extra analysis_options.yaml at /packages/foo/analysis_options.yaml'), + contains(' foo:\n' + ' Unexpected local analysis options'), + ]), + ); + }); + + test('fails .analysis_options', () async { + createFakePlugin('foo', packagesDir, + extraFiles: ['.analysis_options']); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['analyze'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Found an extra analysis_options.yaml at /packages/foo/.analysis_options'), + contains(' foo:\n' + ' Unexpected local analysis options'), + ]), + ); + }); + + test('takes an allow list', () async { + final Directory pluginDir = createFakePlugin('foo', packagesDir, + extraFiles: ['analysis_options.yaml']); + + await runCapturingPrint( + runner, ['analyze', '--custom-analysis', 'foo']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', const ['packages', 'get'], pluginDir.path), + ProcessCall('dart', const ['analyze', '--fatal-infos'], + pluginDir.path), + ])); + }); + + test('takes an allow config file', () async { + final Directory pluginDir = createFakePlugin('foo', packagesDir, + extraFiles: ['analysis_options.yaml']); + final File allowFile = packagesDir.childFile('custom.yaml'); + allowFile.writeAsStringSync('- foo'); + + await runCapturingPrint( + runner, ['analyze', '--custom-analysis', allowFile.path]); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', const ['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, + extraFiles: ['analysis_options.yaml']); + + await expectLater( + () => runCapturingPrint( + runner, ['analyze', '--custom-analysis', '']), + throwsA(isA())); + }); + }); + + test('fails if "packages get" fails', () async { + createFakePlugin('foo', packagesDir); + + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess(exitCode: 1) // flutter packages get + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['analyze'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to get dependencies'), + ]), + ); + }); + + test('fails if "analyze" fails', () async { + createFakePlugin('foo', packagesDir); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess(exitCode: 1) // dart analyze + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['analyze'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains(' foo'), + ]), + ); + }); + + // Ensure that the command used to analyze flutter/plugins in the Dart repo: + // https://github.com/dart-lang/sdk/blob/master/tools/bots/flutter/analyze_flutter_plugins.sh + // continues to work. + // + // DO NOT remove or modify this test without a coordination plan in place to + // modify the script above, as it is run from source, but out-of-repo. + // Contact stuartmorgan or devoncarew for assistance. + test('Dart repo analyze command works', () async { + final Directory pluginDir = createFakePlugin('foo', packagesDir, + extraFiles: ['analysis_options.yaml']); + final File allowFile = packagesDir.childFile('custom.yaml'); + allowFile.writeAsStringSync('- foo'); + + await runCapturingPrint(runner, [ + // DO NOT change this call; see comment above. + 'analyze', + '--analysis-sdk', + 'foo/bar/baz', + '--custom-analysis', + allowFile.path + ]); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', + const ['packages', 'get'], + pluginDir.path, + ), + ProcessCall( + 'foo/bar/baz/bin/dart', + const ['analyze', '--fatal-infos'], + pluginDir.path, + ), + ]), + ); + }); +} 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..c3b0cb9d5cd1 --- /dev/null +++ b/script/tool/test/build_examples_command_test.dart @@ -0,0 +1,725 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/build_examples_command.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('build-example', () { + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final BuildExamplesCommand command = BuildExamplesCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = CommandRunner( + 'build_examples_command', 'Test for build_example_command'); + runner.addCommand(command); + }); + + test('fails if no plaform flags are passed', () async { + Error? commandError; + final List output = await runCapturingPrint( + runner, ['build-examples'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('At least one platform must be provided'), + ])); + }); + + test('fails if building fails', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + }); + + processRunner + .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = + [ + MockProcess(exitCode: 1) // flutter packages get + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['build-examples', '--ios'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains(' plugin:\n' + ' plugin/example (iOS)'), + ])); + }); + + test('fails if a plugin has no examples', () async { + createFakePlugin('plugin', packagesDir, + examples: [], + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) + }); + + processRunner + .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = + [ + MockProcess(exitCode: 1) // flutter packages get + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['build-examples', '--ios'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains(' plugin:\n' + ' No examples found'), + ])); + }); + + test('building for iOS when plugin is not set up for iOS results in no-op', + () async { + mockPlatform.isMacOS = true; + createFakePlugin('plugin', packagesDir); + + final List output = + await runCapturingPrint(runner, ['build-examples', '--ios']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('iOS is not supported by this plugin'), + ]), + ); + + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('building for iOS', () async { + mockPlatform.isMacOS = true; + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint(runner, + ['build-examples', '--ios', '--enable-experiment=exp1']); + + expect( + output, + containsAllInOrder([ + '\nBUILDING plugin/example for iOS', + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const [ + 'build', + 'ios', + '--no-codesign', + '--enable-experiment=exp1' + ], + pluginExampleDirectory.path), + ])); + }); + + test( + 'building for Linux when plugin is not set up for Linux results in no-op', + () async { + mockPlatform.isLinux = true; + createFakePlugin('plugin', packagesDir); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--linux']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('Linux is not supported by this plugin'), + ]), + ); + + // Output should be empty since running build-examples --linux with no + // Linux implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('building for Linux', () async { + mockPlatform.isLinux = true; + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--linux']); + + expect( + output, + containsAllInOrder([ + '\nBUILDING plugin/example for Linux', + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['build', 'linux'], pluginExampleDirectory.path), + ])); + }); + + test('building for macOS with no implementation results in no-op', + () async { + mockPlatform.isMacOS = true; + createFakePlugin('plugin', packagesDir); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--macos']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('macOS is not supported by this plugin'), + ]), + ); + + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('building for macOS', () async { + mockPlatform.isMacOS = true; + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--macos']); + + expect( + output, + containsAllInOrder([ + '\nBUILDING plugin/example for macOS', + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['build', 'macos'], pluginExampleDirectory.path), + ])); + }); + + test('building for web with no implementation results in no-op', () async { + createFakePlugin('plugin', packagesDir); + + final List output = + await runCapturingPrint(runner, ['build-examples', '--web']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('web is not supported by this plugin'), + ]), + ); + + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('building for web', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = + await runCapturingPrint(runner, ['build-examples', '--web']); + + expect( + output, + containsAllInOrder([ + '\nBUILDING plugin/example for web', + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['build', 'web'], pluginExampleDirectory.path), + ])); + }); + + test( + 'building for win32 when plugin is not set up for Windows results in no-op', + () async { + mockPlatform.isWindows = true; + createFakePlugin('plugin', packagesDir); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--windows']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('Win32 is not supported by this plugin'), + ]), + ); + + // Output should be empty since running build-examples --windows with no + // Windows implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('building for win32', () async { + mockPlatform.isWindows = true; + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--windows']); + + expect( + output, + containsAllInOrder([ + '\nBUILDING plugin/example for Win32 (windows)', + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const ['build', 'windows'], + pluginExampleDirectory.path), + ])); + }); + + test('building for UWP when plugin does not support UWP is a no-op', + () async { + createFakePlugin('plugin', packagesDir); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--winuwp']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('UWP is not supported by this plugin'), + ]), + ); + + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('building for UWP', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test', + ], platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.federated, + variants: [platformVariantWinUwp]), + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--winuwp']); + + expect( + output, + containsAllInOrder([ + contains('BUILDING plugin/example for UWP (winuwp)'), + ]), + ); + + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['build', 'winuwp'], pluginExampleDirectory.path), + ])); + }); + + test('building for UWP creates a folder if necessary', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test', + ], platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.federated, + variants: [platformVariantWinUwp]), + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--winuwp']); + + expect( + output, + contains('Creating temporary winuwp folder'), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const ['create', '--platforms=winuwp', '.'], + pluginExampleDirectory.path), + ProcessCall(getFlutterCommand(mockPlatform), + const ['build', 'winuwp'], pluginExampleDirectory.path), + ])); + }); + + test( + 'building for Android when plugin is not set up for Android results in no-op', + () async { + createFakePlugin('plugin', packagesDir); + + final List output = + await runCapturingPrint(runner, ['build-examples', '--apk']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('Android is not supported by this plugin'), + ]), + ); + + // Output should be empty since running build-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('building for Android', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'build-examples', + '--apk', + ]); + + expect( + output, + containsAllInOrder([ + '\nBUILDING plugin/example for Android (apk)', + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['build', 'apk'], pluginExampleDirectory.path), + ])); + }); + + test('enable-experiment flag for Android', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + await runCapturingPrint(runner, + ['build-examples', '--apk', '--enable-experiment=exp1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const ['build', 'apk', '--enable-experiment=exp1'], + pluginExampleDirectory.path), + ])); + }); + + test('enable-experiment flag for ios', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + await runCapturingPrint(runner, + ['build-examples', '--ios', '--enable-experiment=exp1']); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const [ + 'build', + 'ios', + '--no-codesign', + '--enable-experiment=exp1' + ], + pluginExampleDirectory.path), + ])); + }); + + test('logs skipped platforms', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + }); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--apk', '--ios', '--macos']); + + expect( + output, + containsAllInOrder([ + contains('Skipping unsupported platform(s): iOS, macOS'), + ]), + ); + }); + + group('packages', () { + test('builds when requested platform is supported by example', () async { + final Directory packageDirectory = createFakePackage( + 'package', packagesDir, isFlutter: true, extraFiles: [ + 'example/ios/Runner.xcodeproj/project.pbxproj' + ]); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--ios']); + + expect( + output, + containsAllInOrder([ + contains('Running for package'), + contains('BUILDING package/example for iOS'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const [ + 'build', + 'ios', + '--no-codesign', + ], + packageDirectory.childDirectory('example').path), + ])); + }); + + test('skips non-Flutter examples', () async { + createFakePackage('package', packagesDir, isFlutter: false); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--ios']); + + expect( + output, + containsAllInOrder([ + contains('Running for package'), + contains('No examples found supporting requested platform(s).'), + ]), + ); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skips when there is no example', () async { + createFakePackage('package', packagesDir, + isFlutter: true, examples: []); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--ios']); + + expect( + output, + containsAllInOrder([ + contains('Running for package'), + contains('No examples found supporting requested platform(s).'), + ]), + ); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skip when example does not support requested platform', () async { + createFakePackage('package', packagesDir, + isFlutter: true, + extraFiles: ['example/linux/CMakeLists.txt']); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--ios']); + + expect( + output, + containsAllInOrder([ + contains('Running for package'), + contains('Skipping iOS for package/example; not supported.'), + contains('No examples found supporting requested platform(s).'), + ]), + ); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('logs skipped platforms when only some are supported', () async { + final Directory packageDirectory = createFakePackage( + 'package', packagesDir, + isFlutter: true, + extraFiles: ['example/linux/CMakeLists.txt']); + + final List output = await runCapturingPrint( + runner, ['build-examples', '--apk', '--linux']); + + expect( + output, + containsAllInOrder([ + contains('Running for package'), + contains('Building for: Android, Linux'), + contains('Skipping Android for package/example; not supported.'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const ['build', 'linux'], + packageDirectory.childDirectory('example').path), + ])); + }); + }); + + test('The .pluginToolsConfig.yaml file', () async { + mockPlatform.isLinux = true; + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final File pluginExampleConfigFile = + pluginExampleDirectory.childFile('.pluginToolsConfig.yaml'); + pluginExampleConfigFile + .writeAsStringSync('buildFlags:\n global:\n - "test argument"'); + + final List output = [ + ...await runCapturingPrint( + runner, ['build-examples', '--linux']), + ...await runCapturingPrint( + runner, ['build-examples', '--macos']), + ]; + + expect( + output, + containsAllInOrder([ + '\nBUILDING plugin/example for Linux', + '\nBUILDING plugin/example for macOS', + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const ['build', 'linux', 'test argument'], + pluginExampleDirectory.path), + ProcessCall( + getFlutterCommand(mockPlatform), + const ['build', 'macos', 'test argument'], + pluginExampleDirectory.path), + ])); + }); + }); +} diff --git a/script/tool/test/common/file_utils_test.dart b/script/tool/test/common/file_utils_test.dart new file mode 100644 index 000000000000..e3986842a969 --- /dev/null +++ b/script/tool/test/common/file_utils_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:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/file_utils.dart'; +import 'package:test/test.dart'; + +void main() { + test('works on Posix', () async { + final FileSystem fileSystem = + MemoryFileSystem(style: FileSystemStyle.posix); + + final Directory base = fileSystem.directory('/').childDirectory('base'); + final File file = + childFileWithSubcomponents(base, ['foo', 'bar', 'baz.txt']); + + expect(file.absolute.path, '/base/foo/bar/baz.txt'); + }); + + test('works on Windows', () async { + final FileSystem fileSystem = + MemoryFileSystem(style: FileSystemStyle.windows); + + final Directory base = fileSystem.directory(r'C:\').childDirectory('base'); + final File file = + childFileWithSubcomponents(base, ['foo', 'bar', 'baz.txt']); + + expect(file.absolute.path, r'C:\base\foo\bar\baz.txt'); + }); +} diff --git a/script/tool/test/common/git_version_finder_test.dart b/script/tool/test/common/git_version_finder_test.dart new file mode 100644 index 000000000000..f1f40b5e0035 --- /dev/null +++ b/script/tool/test/common/git_version_finder_test.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 'dart:io'; + +import 'package:flutter_plugin_tools/src/common/git_version_finder.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'plugin_command_test.mocks.dart'; + +void main() { + late List?> gitDirCommands; + late String gitDiffResponse; + late MockGitDir gitDir; + String? mergeBaseResponse; + + setUp(() { + gitDirCommands = ?>[]; + gitDiffResponse = ''; + gitDir = MockGitDir(); + when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) + .thenAnswer((Invocation invocation) { + 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); + }); + }); + + 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'])); + }); +} + +class MockProcessResult extends Mock implements ProcessResult {} diff --git a/script/tool/test/common/gradle_test.dart b/script/tool/test/common/gradle_test.dart new file mode 100644 index 000000000000..3eac60baf3c3 --- /dev/null +++ b/script/tool/test/common/gradle_test.dart @@ -0,0 +1,179 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/gradle.dart'; +import 'package:test/test.dart'; + +import '../mocks.dart'; +import '../util.dart'; + +void main() { + late FileSystem fileSystem; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + processRunner = RecordingProcessRunner(); + }); + + group('isConfigured', () { + test('reports true when configured on Windows', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/gradlew.bat']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isWindows: true), + ); + + expect(project.isConfigured(), true); + }); + + test('reports true when configured on non-Windows', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/gradlew']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isMacOS: true), + ); + + expect(project.isConfigured(), true); + }); + + test('reports false when not configured on Windows', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/foo']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isWindows: true), + ); + + expect(project.isConfigured(), false); + }); + + test('reports true when configured on non-Windows', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/foo']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isMacOS: true), + ); + + expect(project.isConfigured(), false); + }); + }); + + group('runXcodeBuild', () { + test('runs without arguments', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/gradlew']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isMacOS: true), + ); + + final int exitCode = await project.runCommand('foo'); + + expect(exitCode, 0); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + plugin.childDirectory('android').childFile('gradlew').path, + const [ + 'foo', + ], + plugin.childDirectory('android').path), + ])); + }); + + test('runs with arguments', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/gradlew']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isMacOS: true), + ); + + final int exitCode = await project.runCommand( + 'foo', + arguments: ['--bar', '--baz'], + ); + + expect(exitCode, 0); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + plugin.childDirectory('android').childFile('gradlew').path, + const [ + 'foo', + '--bar', + '--baz', + ], + plugin.childDirectory('android').path), + ])); + }); + + test('runs with the correct wrapper on Windows', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/gradlew.bat']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isWindows: true), + ); + + final int exitCode = await project.runCommand('foo'); + + expect(exitCode, 0); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + plugin.childDirectory('android').childFile('gradlew.bat').path, + const [ + 'foo', + ], + plugin.childDirectory('android').path), + ])); + }); + + test('returns error codes', () async { + final Directory plugin = createFakePlugin( + 'plugin', fileSystem.directory('/'), + extraFiles: ['android/gradlew.bat']); + final GradleProject project = GradleProject( + plugin, + processRunner: processRunner, + platform: MockPlatform(isWindows: true), + ); + + processRunner.mockProcessesForExecutable[project.gradleWrapper.path] = + [ + MockProcess(exitCode: 1), + ]; + + final int exitCode = await project.runCommand('foo'); + + expect(exitCode, 1); + }); + }); +} diff --git a/script/tool/test/common/package_looping_command_test.dart b/script/tool/test/common/package_looping_command_test.dart new file mode 100644 index 000000000000..7cf03960a74d --- /dev/null +++ b/script/tool/test/common/package_looping_command_test.dart @@ -0,0 +1,732 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/package_looping_command.dart'; +import 'package:flutter_plugin_tools/src/common/process_runner.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; +import 'package:git/git.dart'; +import 'package:mockito/mockito.dart'; +import 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +import '../mocks.dart'; +import '../util.dart'; +import 'plugin_command_test.mocks.dart'; + +// Constants for colorized output start and end. +const String _startErrorColor = '\x1B[31m'; +const String _startHeadingColor = '\x1B[36m'; +const String _startSkipColor = '\x1B[90m'; +const String _startSkipWithWarningColor = '\x1B[93m'; +const String _startSuccessColor = '\x1B[32m'; +const String _startWarningColor = '\x1B[33m'; +const String _endColor = '\x1B[0m'; + +// The filename within a package containing errors to return from runForPackage. +const String _errorFile = 'errors'; +// The filename within a package indicating that it should be skipped. +const String _skipFile = 'skip'; +// The filename within a package containing warnings to log during runForPackage. +const String _warningFile = 'warnings'; +// The filename within a package indicating that it should throw. +const String _throwFile = 'throw'; + +void main() { + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late Directory thirdPartyPackagesDir; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + thirdPartyPackagesDir = packagesDir.parent + .childDirectory('third_party') + .childDirectory('packages'); + }); + + /// Creates a TestPackageLoopingCommand instance that uses [gitDiffResponse] + /// for git diffs, and logs output to [printOutput]. + TestPackageLoopingCommand createTestCommand({ + String gitDiffResponse = '', + bool hasLongOutput = true, + bool includeSubpackages = false, + bool failsDuringInit = false, + bool warnsDuringInit = false, + bool warnsDuringCleanup = false, + bool captureOutput = false, + String? customFailureListHeader, + String? customFailureListFooter, + }) { + // Set up the git diff response. + final MockGitDir gitDir = MockGitDir(); + when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) + .thenAnswer((Invocation invocation) { + final MockProcessResult mockProcessResult = MockProcessResult(); + if (invocation.positionalArguments[0][0] == 'diff') { + when(mockProcessResult.stdout as String?) + .thenReturn(gitDiffResponse); + } + return Future.value(mockProcessResult); + }); + + return TestPackageLoopingCommand( + packagesDir, + platform: mockPlatform, + hasLongOutput: hasLongOutput, + includeSubpackages: includeSubpackages, + failsDuringInit: failsDuringInit, + warnsDuringInit: warnsDuringInit, + warnsDuringCleanup: warnsDuringCleanup, + customFailureListHeader: customFailureListHeader, + customFailureListFooter: customFailureListFooter, + captureOutput: captureOutput, + gitDir: gitDir, + ); + } + + /// Runs [command] with the given [arguments], and returns its output. + Future> runCommand( + TestPackageLoopingCommand command, { + List arguments = const [], + void Function(Error error)? errorHandler, + }) async { + late CommandRunner runner; + runner = CommandRunner('test_package_looping_command', + 'Test for base package looping functionality'); + runner.addCommand(command); + return await runCapturingPrint( + runner, + [command.name, ...arguments], + errorHandler: errorHandler, + ); + } + + group('tool exit', () { + test('is handled during initializeRun', () async { + final TestPackageLoopingCommand command = + createTestCommand(failsDuringInit: true); + + expect(() => runCommand(command), throwsA(isA())); + }); + + test('does not stop looping on error', () async { + createFakePackage('package_a', packagesDir); + final Directory failingPackage = + createFakePlugin('package_b', packagesDir); + createFakePackage('package_c', packagesDir); + failingPackage.childFile(_errorFile).createSync(); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + Error? commandError; + final List output = + await runCommand(command, errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + '${_startHeadingColor}Running for package_a...$_endColor', + '${_startHeadingColor}Running for package_b...$_endColor', + '${_startHeadingColor}Running for package_c...$_endColor', + ])); + }); + + test('does not stop looping on exceptions', () async { + createFakePackage('package_a', packagesDir); + final Directory failingPackage = + createFakePlugin('package_b', packagesDir); + createFakePackage('package_c', packagesDir); + failingPackage.childFile(_throwFile).createSync(); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + Error? commandError; + final List output = + await runCommand(command, errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + '${_startHeadingColor}Running for package_a...$_endColor', + '${_startHeadingColor}Running for package_b...$_endColor', + '${_startHeadingColor}Running for package_c...$_endColor', + ])); + }); + }); + + group('package iteration', () { + test('includes plugins and packages', () async { + final Directory plugin = createFakePlugin('a_plugin', packagesDir); + final Directory package = createFakePackage('a_package', packagesDir); + + final TestPackageLoopingCommand command = createTestCommand(); + await runCommand(command); + + expect(command.checkedPackages, + unorderedEquals([plugin.path, package.path])); + }); + + test('includes third_party/packages', () async { + final Directory package1 = createFakePackage('a_package', packagesDir); + final Directory package2 = + createFakePackage('another_package', thirdPartyPackagesDir); + + final TestPackageLoopingCommand command = createTestCommand(); + await runCommand(command); + + expect(command.checkedPackages, + unorderedEquals([package1.path, package2.path])); + }); + + test('includes subpackages when requested', () async { + final Directory plugin = createFakePlugin('a_plugin', packagesDir, + examples: ['example1', 'example2']); + final Directory package = createFakePackage('a_package', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(includeSubpackages: true); + await runCommand(command); + + expect( + command.checkedPackages, + unorderedEquals([ + plugin.path, + plugin.childDirectory('example').childDirectory('example1').path, + plugin.childDirectory('example').childDirectory('example2').path, + package.path, + package.childDirectory('example').path, + ])); + }); + + test('excludes subpackages when main package is excluded', () async { + final Directory excluded = createFakePlugin('a_plugin', packagesDir, + examples: ['example1', 'example2']); + final Directory included = createFakePackage('a_package', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(includeSubpackages: true); + await runCommand(command, arguments: ['--exclude=a_plugin']); + + expect( + command.checkedPackages, + unorderedEquals([ + included.path, + included.childDirectory('example').path, + ])); + expect(command.checkedPackages, isNot(contains(excluded.path))); + expect(command.checkedPackages, + isNot(contains(excluded.childDirectory('example1').path))); + expect(command.checkedPackages, + isNot(contains(excluded.childDirectory('example2').path))); + }); + }); + + group('output', () { + test('has the expected package headers for long-form output', () async { + createFakePlugin('package_a', packagesDir); + createFakePackage('package_b', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: true); + final List output = await runCommand(command); + + const String separator = + '============================================================'; + expect( + output, + containsAllInOrder([ + '$_startHeadingColor\n$separator\n|| Running for package_a\n$separator\n$_endColor', + '$_startHeadingColor\n$separator\n|| Running for package_b\n$separator\n$_endColor', + ])); + }); + + test('has the expected package headers for short-form output', () async { + createFakePlugin('package_a', packagesDir); + createFakePackage('package_b', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + final List output = await runCommand(command); + + expect( + output, + containsAllInOrder([ + '${_startHeadingColor}Running for package_a...$_endColor', + '${_startHeadingColor}Running for package_b...$_endColor', + ])); + }); + + test('shows the success message when nothing fails', () async { + createFakePackage('package_a', packagesDir); + createFakePackage('package_b', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + final List output = await runCommand(command); + + expect( + output, + containsAllInOrder([ + '\n', + '${_startSuccessColor}No issues found!$_endColor', + ])); + }); + + test('shows failure summaries when something fails without extra details', + () async { + createFakePackage('package_a', packagesDir); + final Directory failingPackage1 = + createFakePlugin('package_b', packagesDir); + createFakePackage('package_c', packagesDir); + final Directory failingPackage2 = + createFakePlugin('package_d', packagesDir); + failingPackage1.childFile(_errorFile).createSync(); + failingPackage2.childFile(_errorFile).createSync(); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + Error? commandError; + final List output = + await runCommand(command, errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + '\n', + '${_startErrorColor}The following packages had errors:$_endColor', + '$_startErrorColor package_b$_endColor', + '$_startErrorColor package_d$_endColor', + '${_startErrorColor}See above for full details.$_endColor', + ])); + }); + + test('uses custom summary header and footer if provided', () async { + createFakePackage('package_a', packagesDir); + final Directory failingPackage1 = + createFakePlugin('package_b', packagesDir); + createFakePackage('package_c', packagesDir); + final Directory failingPackage2 = + createFakePlugin('package_d', packagesDir); + failingPackage1.childFile(_errorFile).createSync(); + failingPackage2.childFile(_errorFile).createSync(); + + final TestPackageLoopingCommand command = createTestCommand( + hasLongOutput: false, + customFailureListHeader: 'This is a custom header', + customFailureListFooter: 'And a custom footer!'); + Error? commandError; + final List output = + await runCommand(command, errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + '\n', + '${_startErrorColor}This is a custom header$_endColor', + '$_startErrorColor package_b$_endColor', + '$_startErrorColor package_d$_endColor', + '${_startErrorColor}And a custom footer!$_endColor', + ])); + }); + + test('shows failure summaries when something fails with extra details', + () async { + createFakePackage('package_a', packagesDir); + final Directory failingPackage1 = + createFakePlugin('package_b', packagesDir); + createFakePackage('package_c', packagesDir); + final Directory failingPackage2 = + createFakePlugin('package_d', packagesDir); + final File errorFile1 = failingPackage1.childFile(_errorFile); + errorFile1.createSync(); + errorFile1.writeAsStringSync('just one detail'); + final File errorFile2 = failingPackage2.childFile(_errorFile); + errorFile2.createSync(); + errorFile2.writeAsStringSync('first detail\nsecond detail'); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + Error? commandError; + final List output = + await runCommand(command, errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + '\n', + '${_startErrorColor}The following packages had errors:$_endColor', + '$_startErrorColor package_b:\n just one detail$_endColor', + '$_startErrorColor package_d:\n first detail\n second detail$_endColor', + '${_startErrorColor}See above for full details.$_endColor', + ])); + }); + + test('is captured, not printed, when requested', () async { + createFakePlugin('package_a', packagesDir); + createFakePackage('package_b', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: true, captureOutput: true); + final List output = await runCommand(command); + + expect(output, isEmpty); + + // None of the output should be colorized when captured. + const String separator = + '============================================================'; + expect( + command.capturedOutput, + containsAllInOrder([ + '\n$separator\n|| Running for package_a\n$separator\n', + '\n$separator\n|| Running for package_b\n$separator\n', + 'No issues found!', + ])); + }); + + test('logs skips', () async { + createFakePackage('package_a', packagesDir); + final Directory skipPackage = createFakePackage('package_b', packagesDir); + skipPackage.childFile(_skipFile).writeAsStringSync('For a reason'); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + final List output = await runCommand(command); + + expect( + output, + containsAllInOrder([ + '${_startHeadingColor}Running for package_a...$_endColor', + '${_startHeadingColor}Running for package_b...$_endColor', + '$_startSkipColor SKIPPING: For a reason$_endColor', + ])); + }); + + test('logs exclusions', () async { + createFakePackage('package_a', packagesDir); + createFakePackage('package_b', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + final List output = + await runCommand(command, arguments: ['--exclude=package_b']); + + expect( + output, + containsAllInOrder([ + '${_startHeadingColor}Running for package_a...$_endColor', + '${_startSkipColor}Not running for package_b; excluded$_endColor', + ])); + }); + + test('logs warnings', () async { + final Directory warnPackage = createFakePackage('package_a', packagesDir); + warnPackage + .childFile(_warningFile) + .writeAsStringSync('Warning 1\nWarning 2'); + createFakePackage('package_b', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + final List output = await runCommand(command); + + expect( + output, + containsAllInOrder([ + '${_startHeadingColor}Running for package_a...$_endColor', + '${_startWarningColor}Warning 1$_endColor', + '${_startWarningColor}Warning 2$_endColor', + '${_startHeadingColor}Running for package_b...$_endColor', + ])); + }); + + test('logs unhandled exceptions as errors', () async { + createFakePackage('package_a', packagesDir); + final Directory failingPackage = + createFakePlugin('package_b', packagesDir); + createFakePackage('package_c', packagesDir); + failingPackage.childFile(_throwFile).createSync(); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + Error? commandError; + final List output = + await runCommand(command, errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + '${_startErrorColor}Exception: Uh-oh$_endColor', + '${_startErrorColor}The following packages had errors:$_endColor', + '$_startErrorColor package_b:\n Unhandled exception$_endColor', + ])); + }); + + test('prints run summary on success', () async { + final Directory warnPackage1 = + createFakePackage('package_a', packagesDir); + warnPackage1 + .childFile(_warningFile) + .writeAsStringSync('Warning 1\nWarning 2'); + createFakePackage('package_b', packagesDir); + final Directory skipPackage = createFakePackage('package_c', packagesDir); + skipPackage.childFile(_skipFile).writeAsStringSync('For a reason'); + final Directory skipAndWarnPackage = + createFakePackage('package_d', packagesDir); + skipAndWarnPackage.childFile(_warningFile).writeAsStringSync('Warning'); + skipAndWarnPackage.childFile(_skipFile).writeAsStringSync('See warning'); + final Directory warnPackage2 = + createFakePackage('package_e', packagesDir); + warnPackage2 + .childFile(_warningFile) + .writeAsStringSync('Warning 1\nWarning 2'); + createFakePackage('package_f', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + final List output = await runCommand(command); + + expect( + output, + containsAllInOrder([ + '------------------------------------------------------------', + 'Ran for 4 package(s) (2 with warnings)', + 'Skipped 2 package(s) (1 with warnings)', + '\n', + '${_startSuccessColor}No issues found!$_endColor', + ])); + // The long-form summary should not be printed for short-form commands. + expect(output, isNot(contains('Run summary:'))); + expect(output, isNot(contains(contains('package a - ran')))); + }); + + test('counts exclusions as skips in run summary', () async { + createFakePackage('package_a', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: false); + final List output = + await runCommand(command, arguments: ['--exclude=package_a']); + + expect( + output, + containsAllInOrder([ + '------------------------------------------------------------', + 'Skipped 1 package(s)', + '\n', + '${_startSuccessColor}No issues found!$_endColor', + ])); + }); + + test('prints long-form run summary for long-output commands', () async { + final Directory warnPackage1 = + createFakePackage('package_a', packagesDir); + warnPackage1 + .childFile(_warningFile) + .writeAsStringSync('Warning 1\nWarning 2'); + createFakePackage('package_b', packagesDir); + final Directory skipPackage = createFakePackage('package_c', packagesDir); + skipPackage.childFile(_skipFile).writeAsStringSync('For a reason'); + final Directory skipAndWarnPackage = + createFakePackage('package_d', packagesDir); + skipAndWarnPackage.childFile(_warningFile).writeAsStringSync('Warning'); + skipAndWarnPackage.childFile(_skipFile).writeAsStringSync('See warning'); + final Directory warnPackage2 = + createFakePackage('package_e', packagesDir); + warnPackage2 + .childFile(_warningFile) + .writeAsStringSync('Warning 1\nWarning 2'); + createFakePackage('package_f', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: true); + final List output = await runCommand(command); + + expect( + output, + containsAllInOrder([ + '------------------------------------------------------------', + 'Run overview:', + ' package_a - ${_startWarningColor}ran (with warning)$_endColor', + ' package_b - ${_startSuccessColor}ran$_endColor', + ' package_c - ${_startSkipColor}skipped$_endColor', + ' package_d - ${_startSkipWithWarningColor}skipped (with warning)$_endColor', + ' package_e - ${_startWarningColor}ran (with warning)$_endColor', + ' package_f - ${_startSuccessColor}ran$_endColor', + '', + 'Ran for 4 package(s) (2 with warnings)', + 'Skipped 2 package(s) (1 with warnings)', + '\n', + '${_startSuccessColor}No issues found!$_endColor', + ])); + }); + + test('prints exclusions as skips in long-form run summary', () async { + createFakePackage('package_a', packagesDir); + + final TestPackageLoopingCommand command = + createTestCommand(hasLongOutput: true); + final List output = + await runCommand(command, arguments: ['--exclude=package_a']); + + expect( + output, + containsAllInOrder([ + ' package_a - ${_startSkipColor}excluded$_endColor', + '', + 'Skipped 1 package(s)', + '\n', + '${_startSuccessColor}No issues found!$_endColor', + ])); + }); + + test('handles warnings outside of runForPackage', () async { + createFakePackage('package_a', packagesDir); + + final TestPackageLoopingCommand command = createTestCommand( + hasLongOutput: false, + warnsDuringCleanup: true, + warnsDuringInit: true, + ); + final List output = await runCommand(command); + + expect( + output, + containsAllInOrder([ + '${_startWarningColor}Warning during initializeRun$_endColor', + '${_startHeadingColor}Running for package_a...$_endColor', + '${_startWarningColor}Warning during completeRun$_endColor', + '------------------------------------------------------------', + 'Ran for 1 package(s)', + '2 warnings not associated with a package', + '\n', + '${_startSuccessColor}No issues found!$_endColor', + ])); + }); + }); +} + +class TestPackageLoopingCommand extends PackageLoopingCommand { + TestPackageLoopingCommand( + Directory packagesDir, { + required Platform platform, + this.hasLongOutput = true, + this.includeSubpackages = false, + this.customFailureListHeader, + this.customFailureListFooter, + this.failsDuringInit = false, + this.warnsDuringInit = false, + this.warnsDuringCleanup = false, + this.captureOutput = false, + ProcessRunner processRunner = const ProcessRunner(), + GitDir? gitDir, + }) : super(packagesDir, + processRunner: processRunner, platform: platform, gitDir: gitDir); + + final List checkedPackages = []; + final List capturedOutput = []; + + final String? customFailureListHeader; + final String? customFailureListFooter; + + final bool failsDuringInit; + final bool warnsDuringInit; + final bool warnsDuringCleanup; + + @override + bool hasLongOutput; + + @override + bool includeSubpackages; + + @override + String get failureListHeader => + customFailureListHeader ?? super.failureListHeader; + + @override + String get failureListFooter => + customFailureListFooter ?? super.failureListFooter; + + @override + bool captureOutput; + + @override + final String name = 'loop-test'; + + @override + final String description = 'sample package looping command'; + + @override + Future initializeRun() async { + if (warnsDuringInit) { + logWarning('Warning during initializeRun'); + } + if (failsDuringInit) { + throw ToolExit(2); + } + } + + @override + Future runForPackage(RepositoryPackage package) async { + checkedPackages.add(package.path); + final File warningFile = package.directory.childFile(_warningFile); + if (warningFile.existsSync()) { + final List warnings = warningFile.readAsLinesSync(); + warnings.forEach(logWarning); + } + final File skipFile = package.directory.childFile(_skipFile); + if (skipFile.existsSync()) { + return PackageResult.skip(skipFile.readAsStringSync()); + } + final File errorFile = package.directory.childFile(_errorFile); + if (errorFile.existsSync()) { + return PackageResult.fail(errorFile.readAsLinesSync()); + } + final File throwFile = package.directory.childFile(_throwFile); + if (throwFile.existsSync()) { + throw Exception('Uh-oh'); + } + return PackageResult.success(); + } + + @override + Future completeRun() async { + if (warnsDuringInit) { + logWarning('Warning during completeRun'); + } + } + + @override + Future handleCapturedOutput(List output) async { + capturedOutput.addAll(output); + } +} + +class MockProcessResult extends Mock implements io.ProcessResult {} diff --git a/script/tool/test/common/plugin_command_test.dart b/script/tool/test/common/plugin_command_test.dart new file mode 100644 index 000000000000..13724e26e5f8 --- /dev/null +++ b/script/tool/test/common/plugin_command_test.dart @@ -0,0 +1,763 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_command.dart'; +import 'package:flutter_plugin_tools/src/common/process_runner.dart'; +import 'package:git/git.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +import '../mocks.dart'; +import '../util.dart'; +import 'plugin_command_test.mocks.dart'; + +@GenerateMocks([GitDir]) +void main() { + late RecordingProcessRunner processRunner; + late SamplePluginCommand command; + late CommandRunner runner; + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late Directory thirdPartyPackagesDir; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + thirdPartyPackagesDir = packagesDir.parent + .childDirectory('third_party') + .childDirectory('packages'); + + final MockGitDir gitDir = MockGitDir(); + when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) + .thenAnswer((Invocation invocation) { + final List arguments = + invocation.positionalArguments[0]! as List; + // Attach the first argument to the command to make targeting the mock + // results easier. + final String gitCommand = arguments.removeAt(0); + return processRunner.run('git-$gitCommand', arguments); + }); + processRunner = RecordingProcessRunner(); + command = SamplePluginCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + gitDir: gitDir, + ); + runner = + CommandRunner('common_command', 'Test for common functionality'); + runner.addCommand(command); + }); + + group('plugin iteration', () { + test('all plugins from file system', () async { + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runCapturingPrint(runner, ['sample']); + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('includes both plugins and packages', () async { + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + final Directory package3 = createFakePackage('package3', packagesDir); + final Directory package4 = createFakePackage('package4', packagesDir); + await runCapturingPrint(runner, ['sample']); + expect( + command.plugins, + unorderedEquals([ + plugin1.path, + plugin2.path, + package3.path, + package4.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 runCapturingPrint(runner, ['sample']); + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path, plugin3.path])); + }); + + test('--packages limits packages', () async { + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + createFakePlugin('plugin2', packagesDir); + createFakePackage('package3', packagesDir); + final Directory package4 = createFakePackage('package4', packagesDir); + await runCapturingPrint( + runner, ['sample', '--packages=plugin1,package4']); + expect( + command.plugins, + unorderedEquals([ + plugin1.path, + package4.path, + ])); + }); + + test('--plugins acts as an alias to --packages', () async { + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + createFakePlugin('plugin2', packagesDir); + createFakePackage('package3', packagesDir); + final Directory package4 = createFakePackage('package4', packagesDir); + await runCapturingPrint( + runner, ['sample', '--plugins=plugin1,package4']); + expect( + command.plugins, + unorderedEquals([ + plugin1.path, + package4.path, + ])); + }); + + test('exclude packages when packages flag is specified', () async { + createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runCapturingPrint(runner, [ + 'sample', + '--packages=plugin1,plugin2', + '--exclude=plugin1' + ]); + expect(command.plugins, unorderedEquals([plugin2.path])); + }); + + test('exclude packages when packages flag isn\'t specified', () async { + createFakePlugin('plugin1', packagesDir); + createFakePlugin('plugin2', packagesDir); + await runCapturingPrint( + runner, ['sample', '--exclude=plugin1,plugin2']); + expect(command.plugins, unorderedEquals([])); + }); + + test('exclude federated plugins when packages flag is specified', () async { + createFakePlugin('plugin1', packagesDir.childDirectory('federated')); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runCapturingPrint(runner, [ + 'sample', + '--packages=federated/plugin1,plugin2', + '--exclude=federated/plugin1' + ]); + expect(command.plugins, unorderedEquals([plugin2.path])); + }); + + test('exclude entire federated plugins when packages flag is specified', + () async { + createFakePlugin('plugin1', packagesDir.childDirectory('federated')); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runCapturingPrint(runner, [ + 'sample', + '--packages=federated/plugin1,plugin2', + '--exclude=federated' + ]); + expect(command.plugins, unorderedEquals([plugin2.path])); + }); + + test('exclude accepts config files', () async { + createFakePlugin('plugin1', packagesDir); + final File configFile = packagesDir.childFile('exclude.yaml'); + configFile.writeAsStringSync('- plugin1'); + + await runCapturingPrint(runner, [ + 'sample', + '--packages=plugin1', + '--exclude=${configFile.path}' + ]); + expect(command.plugins, unorderedEquals([])); + }); + + group('conflicting package selection', () { + test('does not allow --packages with --run-on-changed-packages', + () async { + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'sample', + '--run-on-changed-packages', + '--packages=plugin1', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Only one of --packages, --run-on-changed-packages, or ' + '--packages-for-branch can be provided.') + ])); + }); + + test('does not allow --packages with --packages-for-branch', () async { + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'sample', + '--packages-for-branch', + '--packages=plugin1', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Only one of --packages, --run-on-changed-packages, or ' + '--packages-for-branch can be provided.') + ])); + }); + + test( + 'does not allow --run-on-changed-packages with --packages-for-branch', + () async { + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'sample', + '--packages-for-branch', + '--packages=plugin1', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Only one of --packages, --run-on-changed-packages, or ' + '--packages-for-branch can be provided.') + ])); + }); + }); + + group('test run-on-changed-packages', () { + test('all plugins should be tested if there are no changes.', () async { + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runCapturingPrint(runner, [ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); + }); + + test( + 'all plugins should be tested if there are no plugin related changes.', + () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: 'AUTHORS'), + ]; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runCapturingPrint(runner, [ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('all plugins should be tested if .cirrus.yml changes.', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +.cirrus.yml +packages/plugin1/CHANGELOG +'''), + ]; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runCapturingPrint(runner, [ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('all plugins should be tested if .ci.yaml changes', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +.ci.yaml +packages/plugin1/CHANGELOG +'''), + ]; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runCapturingPrint(runner, [ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('all plugins should be tested if anything in .ci/ changes', + () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +.ci/Dockerfile +packages/plugin1/CHANGELOG +'''), + ]; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runCapturingPrint(runner, [ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('all plugins should be tested if anything in script changes.', + () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +script/tool_runner.sh +packages/plugin1/CHANGELOG +'''), + ]; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runCapturingPrint(runner, [ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('all plugins should be tested if the root analysis options change.', + () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +analysis_options.yaml +packages/plugin1/CHANGELOG +'''), + ]; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runCapturingPrint(runner, [ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('all plugins should be tested if formatting options change.', + () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +.clang-format +packages/plugin1/CHANGELOG +'''), + ]; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + await runCapturingPrint(runner, [ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); + }); + + test('Only changed plugin should be tested.', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: 'packages/plugin1/plugin1.dart'), + ]; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + createFakePlugin('plugin2', packagesDir); + final List output = await runCapturingPrint(runner, [ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect( + output, + containsAllInOrder([ + contains( + 'Running for all packages that have changed relative to "master"'), + ])); + + expect(command.plugins, unorderedEquals([plugin1.path])); + }); + + test('multiple files in one plugin should also test the plugin', + () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin1/plugin1.dart +packages/plugin1/ios/plugin1.m +'''), + ]; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + createFakePlugin('plugin2', packagesDir); + await runCapturingPrint(runner, [ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(command.plugins, unorderedEquals([plugin1.path])); + }); + + test('multiple plugins changed should test all the changed plugins', + () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin1/plugin1.dart +packages/plugin2/ios/plugin2.m +'''), + ]; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + createFakePlugin('plugin3', packagesDir); + await runCapturingPrint(runner, [ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); + }); + + test( + 'multiple plugins inside the same plugin group changed should output the plugin group name', + () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin1/plugin1/plugin1.dart +packages/plugin1/plugin1_platform_interface/plugin1_platform_interface.dart +packages/plugin1/plugin1_web/plugin1_web.dart +'''), + ]; + final Directory plugin1 = + createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); + createFakePlugin('plugin2', packagesDir); + createFakePlugin('plugin3', packagesDir); + await runCapturingPrint(runner, [ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(command.plugins, unorderedEquals([plugin1.path])); + }); + + test( + 'changing one plugin in a federated group should include all plugins in the group', + () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin1/plugin1/plugin1.dart +'''), + ]; + final Directory plugin1 = + createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); + final Directory plugin2 = createFakePlugin('plugin1_platform_interface', + packagesDir.childDirectory('plugin1')); + final Directory plugin3 = createFakePlugin( + 'plugin1_web', packagesDir.childDirectory('plugin1')); + await runCapturingPrint(runner, [ + 'sample', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect( + command.plugins, + unorderedEquals( + [plugin1.path, plugin2.path, plugin3.path])); + }); + + test('--exclude flag works with --run-on-changed-packages', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: ''' +packages/plugin1/plugin1.dart +packages/plugin2/ios/plugin2.m +packages/plugin3/plugin3.dart +'''), + ]; + final Directory plugin1 = + createFakePlugin('plugin1', packagesDir.childDirectory('plugin1')); + createFakePlugin('plugin2', packagesDir); + createFakePlugin('plugin3', packagesDir); + await runCapturingPrint(runner, [ + 'sample', + '--exclude=plugin2,plugin3', + '--base-sha=master', + '--run-on-changed-packages' + ]); + + expect(command.plugins, unorderedEquals([plugin1.path])); + }); + }); + }); + + group('--packages-for-branch', () { + test('only tests changed packages on a branch', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: 'packages/plugin1/plugin1.dart'), + ]; + processRunner.mockProcessesForExecutable['git-rev-parse'] = [ + MockProcess(stdout: 'a-branch'), + ]; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + createFakePlugin('plugin2', packagesDir); + + final List output = await runCapturingPrint( + runner, ['sample', '--packages-for-branch']); + + expect(command.plugins, unorderedEquals([plugin1.path])); + expect( + output, + containsAllInOrder([ + contains('--packages-for-branch: running on changed packages'), + ])); + }); + + test('tests all packages on master', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: 'packages/plugin1/plugin1.dart'), + ]; + processRunner.mockProcessesForExecutable['git-rev-parse'] = [ + MockProcess(stdout: 'master'), + ]; + final Directory plugin1 = createFakePlugin('plugin1', packagesDir); + final Directory plugin2 = createFakePlugin('plugin2', packagesDir); + + final List output = await runCapturingPrint( + runner, ['sample', '--packages-for-branch']); + + expect(command.plugins, + unorderedEquals([plugin1.path, plugin2.path])); + expect( + output, + containsAllInOrder([ + contains('--packages-for-branch: running on all packages'), + ])); + }); + + test('throws if getting the branch fails', () async { + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: 'packages/plugin1/plugin1.dart'), + ]; + processRunner.mockProcessesForExecutable['git-rev-parse'] = [ + MockProcess(exitCode: 1), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['sample', '--packages-for-branch'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unabled to determine branch'), + ])); + }); + }); + + group('sharding', () { + test('distributes evenly when evenly divisible', () async { + final List> expectedShards = >[ + [ + createFakePackage('package1', packagesDir), + createFakePackage('package2', packagesDir), + createFakePackage('package3', packagesDir), + ], + [ + createFakePackage('package4', packagesDir), + createFakePackage('package5', packagesDir), + createFakePackage('package6', packagesDir), + ], + [ + createFakePackage('package7', packagesDir), + createFakePackage('package8', packagesDir), + createFakePackage('package9', packagesDir), + ], + ]; + + for (int i = 0; i < expectedShards.length; ++i) { + final SamplePluginCommand localCommand = SamplePluginCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + gitDir: MockGitDir(), + ); + final CommandRunner localRunner = + CommandRunner('common_command', 'Shard testing'); + localRunner.addCommand(localCommand); + + await runCapturingPrint(localRunner, [ + 'sample', + '--shardIndex=$i', + '--shardCount=3', + ]); + expect( + localCommand.plugins, + unorderedEquals(expectedShards[i] + .map((Directory packageDir) => packageDir.path) + .toList())); + } + }); + + test('distributes as evenly as possible when not evenly divisible', + () async { + final List> expectedShards = >[ + [ + createFakePackage('package1', packagesDir), + createFakePackage('package2', packagesDir), + createFakePackage('package3', packagesDir), + ], + [ + createFakePackage('package4', packagesDir), + createFakePackage('package5', packagesDir), + createFakePackage('package6', packagesDir), + ], + [ + createFakePackage('package7', packagesDir), + createFakePackage('package8', packagesDir), + ], + ]; + + for (int i = 0; i < expectedShards.length; ++i) { + final SamplePluginCommand localCommand = SamplePluginCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + gitDir: MockGitDir(), + ); + final CommandRunner localRunner = + CommandRunner('common_command', 'Shard testing'); + localRunner.addCommand(localCommand); + + await runCapturingPrint(localRunner, [ + 'sample', + '--shardIndex=$i', + '--shardCount=3', + ]); + expect( + localCommand.plugins, + unorderedEquals(expectedShards[i] + .map((Directory packageDir) => packageDir.path) + .toList())); + } + }); + + // In CI (which is the use case for sharding) we often want to run muliple + // commands on the same set of packages, but the exclusion lists for those + // commands may be different. In those cases we still want all the commands + // to operate on a consistent set of plugins. + // + // E.g., some commands require running build-examples in a previous step; + // excluding some plugins from the later step shouldn't change what's tested + // in each shard, as it may no longer align with what was built. + test('counts excluded plugins when sharding', () async { + final List> expectedShards = >[ + [ + createFakePackage('package1', packagesDir), + createFakePackage('package2', packagesDir), + createFakePackage('package3', packagesDir), + ], + [ + createFakePackage('package4', packagesDir), + createFakePackage('package5', packagesDir), + createFakePackage('package6', packagesDir), + ], + [ + createFakePackage('package7', packagesDir), + ], + ]; + // These would be in the last shard, but are excluded. + createFakePackage('package8', packagesDir); + createFakePackage('package9', packagesDir); + + for (int i = 0; i < expectedShards.length; ++i) { + final SamplePluginCommand localCommand = SamplePluginCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + gitDir: MockGitDir(), + ); + final CommandRunner localRunner = + CommandRunner('common_command', 'Shard testing'); + localRunner.addCommand(localCommand); + + await runCapturingPrint(localRunner, [ + 'sample', + '--shardIndex=$i', + '--shardCount=3', + '--exclude=package8,package9', + ]); + expect( + localCommand.plugins, + unorderedEquals(expectedShards[i] + .map((Directory packageDir) => packageDir.path) + .toList())); + } + }); + }); +} + +class SamplePluginCommand extends PluginCommand { + SamplePluginCommand( + Directory packagesDir, { + ProcessRunner processRunner = const ProcessRunner(), + Platform platform = const LocalPlatform(), + GitDir? gitDir, + }) : super(packagesDir, + processRunner: processRunner, platform: platform, gitDir: gitDir); + + final List plugins = []; + + @override + final String name = 'sample'; + + @override + final String description = 'sample command'; + + @override + Future run() async { + await for (final PackageEnumerationEntry entry in getTargetPackages()) { + plugins.add(entry.package.path); + } + } +} diff --git a/script/tool/test/common/plugin_command_test.mocks.dart b/script/tool/test/common/plugin_command_test.mocks.dart new file mode 100644 index 000000000000..b7f7807b3b05 --- /dev/null +++ b/script/tool/test/common/plugin_command_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/common/plugin_utils_test.dart b/script/tool/test/common/plugin_utils_test.dart new file mode 100644 index 000000000000..ac619e2622e0 --- /dev/null +++ b/script/tool/test/common/plugin_utils_test.dart @@ -0,0 +1,344 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; +import 'package:test/test.dart'; + +import '../util.dart'; + +void main() { + late FileSystem fileSystem; + late Directory packagesDir; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + }); + + group('pluginSupportsPlatform', () { + test('no platforms', () async { + final RepositoryPackage plugin = + RepositoryPackage(createFakePlugin('plugin', packagesDir)); + + expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isFalse); + expect(pluginSupportsPlatform(kPlatformIos, plugin), isFalse); + expect(pluginSupportsPlatform(kPlatformLinux, plugin), isFalse); + expect(pluginSupportsPlatform(kPlatformMacos, plugin), isFalse); + expect(pluginSupportsPlatform(kPlatformWeb, plugin), isFalse); + expect(pluginSupportsPlatform(kPlatformWindows, plugin), isFalse); + }); + + test('all platforms', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + })); + + expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isTrue); + expect(pluginSupportsPlatform(kPlatformIos, plugin), isTrue); + expect(pluginSupportsPlatform(kPlatformLinux, plugin), isTrue); + expect(pluginSupportsPlatform(kPlatformMacos, plugin), isTrue); + expect(pluginSupportsPlatform(kPlatformWeb, plugin), isTrue); + expect(pluginSupportsPlatform(kPlatformWindows, plugin), isTrue); + }); + + test('some platforms', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + })); + + expect(pluginSupportsPlatform(kPlatformAndroid, plugin), isTrue); + expect(pluginSupportsPlatform(kPlatformIos, plugin), isFalse); + expect(pluginSupportsPlatform(kPlatformLinux, plugin), isTrue); + expect(pluginSupportsPlatform(kPlatformMacos, plugin), isFalse); + expect(pluginSupportsPlatform(kPlatformWeb, plugin), isTrue); + expect(pluginSupportsPlatform(kPlatformWindows, plugin), isFalse); + }); + + test('inline plugins are only detected as inline', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + })); + + expect( + pluginSupportsPlatform(kPlatformAndroid, plugin, + requiredMode: PlatformSupport.inline), + isTrue); + expect( + pluginSupportsPlatform(kPlatformAndroid, plugin, + requiredMode: PlatformSupport.federated), + isFalse); + expect( + pluginSupportsPlatform(kPlatformIos, plugin, + requiredMode: PlatformSupport.inline), + isTrue); + expect( + pluginSupportsPlatform(kPlatformIos, plugin, + requiredMode: PlatformSupport.federated), + isFalse); + expect( + pluginSupportsPlatform(kPlatformLinux, plugin, + requiredMode: PlatformSupport.inline), + isTrue); + expect( + pluginSupportsPlatform(kPlatformLinux, plugin, + requiredMode: PlatformSupport.federated), + isFalse); + expect( + pluginSupportsPlatform(kPlatformMacos, plugin, + requiredMode: PlatformSupport.inline), + isTrue); + expect( + pluginSupportsPlatform(kPlatformMacos, plugin, + requiredMode: PlatformSupport.federated), + isFalse); + expect( + pluginSupportsPlatform(kPlatformWeb, plugin, + requiredMode: PlatformSupport.inline), + isTrue); + expect( + pluginSupportsPlatform(kPlatformWeb, plugin, + requiredMode: PlatformSupport.federated), + isFalse); + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + requiredMode: PlatformSupport.inline), + isTrue); + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + requiredMode: PlatformSupport.federated), + isFalse); + }); + + test('federated plugins are only detected as federated', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.federated), + kPlatformIos: const PlatformDetails(PlatformSupport.federated), + kPlatformLinux: const PlatformDetails(PlatformSupport.federated), + kPlatformMacos: const PlatformDetails(PlatformSupport.federated), + kPlatformWeb: const PlatformDetails(PlatformSupport.federated), + kPlatformWindows: const PlatformDetails(PlatformSupport.federated), + })); + + expect( + pluginSupportsPlatform(kPlatformAndroid, plugin, + requiredMode: PlatformSupport.federated), + isTrue); + expect( + pluginSupportsPlatform(kPlatformAndroid, plugin, + requiredMode: PlatformSupport.inline), + isFalse); + expect( + pluginSupportsPlatform(kPlatformIos, plugin, + requiredMode: PlatformSupport.federated), + isTrue); + expect( + pluginSupportsPlatform(kPlatformIos, plugin, + requiredMode: PlatformSupport.inline), + isFalse); + expect( + pluginSupportsPlatform(kPlatformLinux, plugin, + requiredMode: PlatformSupport.federated), + isTrue); + expect( + pluginSupportsPlatform(kPlatformLinux, plugin, + requiredMode: PlatformSupport.inline), + isFalse); + expect( + pluginSupportsPlatform(kPlatformMacos, plugin, + requiredMode: PlatformSupport.federated), + isTrue); + expect( + pluginSupportsPlatform(kPlatformMacos, plugin, + requiredMode: PlatformSupport.inline), + isFalse); + expect( + pluginSupportsPlatform(kPlatformWeb, plugin, + requiredMode: PlatformSupport.federated), + isTrue); + expect( + pluginSupportsPlatform(kPlatformWeb, plugin, + requiredMode: PlatformSupport.inline), + isFalse); + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + requiredMode: PlatformSupport.federated), + isTrue); + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + requiredMode: PlatformSupport.inline), + isFalse); + }); + + test('windows without variants is only win32', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + }, + )); + + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWin32), + isTrue); + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWinUwp), + isFalse); + }); + + test('windows with both variants matches win32 and winuwp', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformWindows: const PlatformDetails( + PlatformSupport.federated, + variants: [platformVariantWin32, platformVariantWinUwp], + ), + })); + + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWin32), + isTrue); + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWinUwp), + isTrue); + }); + + test('win32 plugin is only win32', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformWindows: const PlatformDetails( + PlatformSupport.federated, + variants: [platformVariantWin32], + ), + })); + + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWin32), + isTrue); + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWinUwp), + isFalse); + }); + + test('winup plugin is only winuwp', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.federated, + variants: [platformVariantWinUwp]), + }, + )); + + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWin32), + isFalse); + expect( + pluginSupportsPlatform(kPlatformWindows, plugin, + variant: platformVariantWinUwp), + isTrue); + }); + }); + + group('pluginHasNativeCodeForPlatform', () { + test('returns false for web', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + }, + )); + + expect(pluginHasNativeCodeForPlatform(kPlatformWeb, plugin), isFalse); + }); + + test('returns false for a native-only plugin', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + }, + )); + + expect(pluginHasNativeCodeForPlatform(kPlatformLinux, plugin), isTrue); + expect(pluginHasNativeCodeForPlatform(kPlatformMacos, plugin), isTrue); + expect(pluginHasNativeCodeForPlatform(kPlatformWindows, plugin), isTrue); + }); + + test('returns true for a native+Dart plugin', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline, + hasNativeCode: true, hasDartCode: true), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline, + hasNativeCode: true, hasDartCode: true), + kPlatformWindows: const PlatformDetails(PlatformSupport.inline, + hasNativeCode: true, hasDartCode: true), + }, + )); + + expect(pluginHasNativeCodeForPlatform(kPlatformLinux, plugin), isTrue); + expect(pluginHasNativeCodeForPlatform(kPlatformMacos, plugin), isTrue); + expect(pluginHasNativeCodeForPlatform(kPlatformWindows, plugin), isTrue); + }); + + test('returns false for a Dart-only plugin', () async { + final RepositoryPackage plugin = RepositoryPackage(createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline, + hasNativeCode: false, hasDartCode: true), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline, + hasNativeCode: false, hasDartCode: true), + kPlatformWindows: const PlatformDetails(PlatformSupport.inline, + hasNativeCode: false, hasDartCode: true), + }, + )); + + expect(pluginHasNativeCodeForPlatform(kPlatformLinux, plugin), isFalse); + expect(pluginHasNativeCodeForPlatform(kPlatformMacos, plugin), isFalse); + expect(pluginHasNativeCodeForPlatform(kPlatformWindows, plugin), isFalse); + }); + }); +} diff --git a/script/tool/test/common/pub_version_finder_test.dart b/script/tool/test/common/pub_version_finder_test.dart new file mode 100644 index 000000000000..1692cf214abe --- /dev/null +++ b/script/tool/test/common/pub_version_finder_test.dart @@ -0,0 +1,89 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter_plugin_tools/src/common/pub_version_finder.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:mockito/mockito.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:test/test.dart'; + +void main() { + test('Package does not exist.', () async { + final MockClient mockClient = MockClient((http.Request request) async { + return http.Response('', 404); + }); + final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); + final PubVersionFinderResponse response = + await finder.getPackageVersion(packageName: 'some_package'); + + expect(response.versions, isEmpty); + expect(response.result, PubVersionFinderResult.noPackageFound); + expect(response.httpResponse.statusCode, 404); + expect(response.httpResponse.body, ''); + }); + + test('HTTP error when getting versions from pub', () async { + final MockClient mockClient = MockClient((http.Request request) async { + return http.Response('', 400); + }); + final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); + final PubVersionFinderResponse response = + await finder.getPackageVersion(packageName: 'some_package'); + + expect(response.versions, isEmpty); + expect(response.result, PubVersionFinderResult.fail); + expect(response.httpResponse.statusCode, 400); + expect(response.httpResponse.body, ''); + }); + + test('Get a correct list of versions when http response is OK.', () async { + const Map httpResponse = { + 'name': 'some_package', + 'versions': [ + '0.0.1', + '0.0.2', + '0.0.2+2', + '0.1.1', + '0.0.1+1', + '0.1.0', + '0.2.0', + '0.1.0+1', + '0.0.2+1', + '2.0.0', + '1.2.0', + '1.0.0', + ], + }; + final MockClient mockClient = MockClient((http.Request request) async { + return http.Response(json.encode(httpResponse), 200); + }); + final PubVersionFinder finder = PubVersionFinder(httpClient: mockClient); + final PubVersionFinderResponse response = + await finder.getPackageVersion(packageName: 'some_package'); + + expect(response.versions, [ + Version.parse('2.0.0'), + Version.parse('1.2.0'), + Version.parse('1.0.0'), + Version.parse('0.2.0'), + Version.parse('0.1.1'), + Version.parse('0.1.0+1'), + Version.parse('0.1.0'), + Version.parse('0.0.2+2'), + Version.parse('0.0.2+1'), + Version.parse('0.0.2'), + Version.parse('0.0.1+1'), + Version.parse('0.0.1'), + ]); + expect(response.result, PubVersionFinderResult.success); + expect(response.httpResponse.statusCode, 200); + expect(response.httpResponse.body, json.encode(httpResponse)); + }); +} + +class MockProcessResult extends Mock implements ProcessResult {} diff --git a/script/tool/test/common/repository_package_test.dart b/script/tool/test/common/repository_package_test.dart new file mode 100644 index 000000000000..4c20389ae4be --- /dev/null +++ b/script/tool/test/common/repository_package_test.dart @@ -0,0 +1,158 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; +import 'package:test/test.dart'; + +import '../util.dart'; + +void main() { + late FileSystem fileSystem; + late Directory packagesDir; + + setUp(() { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + }); + + group('displayName', () { + test('prints packageDir-relative paths by default', () async { + expect( + RepositoryPackage(packagesDir.childDirectory('foo')).displayName, + 'foo', + ); + expect( + RepositoryPackage(packagesDir + .childDirectory('foo') + .childDirectory('bar') + .childDirectory('baz')) + .displayName, + 'foo/bar/baz', + ); + }); + + test('handles third_party/packages/', () async { + expect( + RepositoryPackage(packagesDir.parent + .childDirectory('third_party') + .childDirectory('packages') + .childDirectory('foo') + .childDirectory('bar') + .childDirectory('baz')) + .displayName, + 'foo/bar/baz', + ); + }); + + test('always uses Posix-style paths', () async { + final Directory windowsPackagesDir = createPackagesDirectory( + fileSystem: MemoryFileSystem(style: FileSystemStyle.windows)); + + expect( + RepositoryPackage(windowsPackagesDir.childDirectory('foo')).displayName, + 'foo', + ); + expect( + RepositoryPackage(windowsPackagesDir + .childDirectory('foo') + .childDirectory('bar') + .childDirectory('baz')) + .displayName, + 'foo/bar/baz', + ); + }); + + test('elides group name in grouped federated plugin structure', () async { + expect( + RepositoryPackage(packagesDir + .childDirectory('a_plugin') + .childDirectory('a_plugin_platform_interface')) + .displayName, + 'a_plugin_platform_interface', + ); + expect( + RepositoryPackage(packagesDir + .childDirectory('a_plugin') + .childDirectory('a_plugin_platform_web')) + .displayName, + 'a_plugin_platform_web', + ); + }); + + // The app-facing package doesn't get elided to avoid potential confusion + // with the group folder itself. + test('does not elide group name for app-facing packages', () async { + expect( + RepositoryPackage(packagesDir + .childDirectory('a_plugin') + .childDirectory('a_plugin')) + .displayName, + 'a_plugin/a_plugin', + ); + }); + }); + + group('getExamples', () { + test('handles a single example', () async { + final Directory plugin = createFakePlugin('a_plugin', packagesDir); + + final List examples = + RepositoryPackage(plugin).getExamples().toList(); + + expect(examples.length, 1); + expect(examples[0].path, plugin.childDirectory('example').path); + }); + + test('handles multiple examples', () async { + final Directory plugin = createFakePlugin('a_plugin', packagesDir, + examples: ['example1', 'example2']); + + final List examples = + RepositoryPackage(plugin).getExamples().toList(); + + expect(examples.length, 2); + expect(examples[0].path, + plugin.childDirectory('example').childDirectory('example1').path); + expect(examples[1].path, + plugin.childDirectory('example').childDirectory('example2').path); + }); + }); + + group('federated plugin queries', () { + test('all return false for a simple plugin', () { + final Directory plugin = createFakePlugin('a_plugin', packagesDir); + expect(RepositoryPackage(plugin).isFederated, false); + expect(RepositoryPackage(plugin).isPlatformInterface, false); + expect(RepositoryPackage(plugin).isFederated, false); + }); + + test('handle app-facing packages', () { + final Directory plugin = + createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); + expect(RepositoryPackage(plugin).isFederated, true); + expect(RepositoryPackage(plugin).isPlatformInterface, false); + expect(RepositoryPackage(plugin).isPlatformImplementation, false); + }); + + test('handle platform interface packages', () { + final Directory plugin = createFakePlugin('a_plugin_platform_interface', + packagesDir.childDirectory('a_plugin')); + expect(RepositoryPackage(plugin).isFederated, true); + expect(RepositoryPackage(plugin).isPlatformInterface, true); + expect(RepositoryPackage(plugin).isPlatformImplementation, false); + }); + + test('handle platform implementation packages', () { + // A platform interface can end with anything, not just one of the known + // platform names, because of cases like webview_flutter_wkwebview. + final Directory plugin = createFakePlugin( + 'a_plugin_foo', packagesDir.childDirectory('a_plugin')); + expect(RepositoryPackage(plugin).isFederated, true); + expect(RepositoryPackage(plugin).isPlatformInterface, false); + expect(RepositoryPackage(plugin).isPlatformImplementation, true); + }); + }); +} diff --git a/script/tool/test/common/xcode_test.dart b/script/tool/test/common/xcode_test.dart new file mode 100644 index 000000000000..259d8ea36cd2 --- /dev/null +++ b/script/tool/test/common/xcode_test.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:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:file/local.dart'; +import 'package:flutter_plugin_tools/src/common/xcode.dart'; +import 'package:test/test.dart'; + +import '../mocks.dart'; +import '../util.dart'; + +void main() { + late RecordingProcessRunner processRunner; + late Xcode xcode; + + setUp(() { + processRunner = RecordingProcessRunner(); + xcode = Xcode(processRunner: processRunner); + }); + + group('findBestAvailableIphoneSimulator', () { + test('finds the newest device', () async { + const String expectedDeviceId = '1E76A0FD-38AC-4537-A989-EA639D7D012A'; + // Note: This uses `dynamic` deliberately, and should not be updated to + // Object, in order to ensure that the code correctly handles this return + // type from JSON decoding. + final Map devices = { + 'runtimes': >[ + { + 'bundlePath': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime', + 'buildversion': '17A577', + 'runtimeRoot': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.0.simruntime/Contents/Resources/RuntimeRoot', + 'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-0', + 'version': '13.0', + 'isAvailable': true, + 'name': 'iOS 13.0' + }, + { + 'bundlePath': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime', + 'buildversion': '17L255', + 'runtimeRoot': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot', + 'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-4', + 'version': '13.4', + 'isAvailable': true, + 'name': 'iOS 13.4' + }, + { + 'bundlePath': + '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime', + 'buildversion': '17T531', + 'runtimeRoot': + '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot', + 'identifier': 'com.apple.CoreSimulator.SimRuntime.watchOS-6-2', + 'version': '6.2.1', + 'isAvailable': true, + 'name': 'watchOS 6.2' + } + ], + 'devices': { + 'com.apple.CoreSimulator.SimRuntime.iOS-13-4': >[ + { + 'dataPath': + '/Users/xxx/Library/Developer/CoreSimulator/Devices/2706BBEB-1E01-403E-A8E9-70E8E5A24774/data', + 'logPath': + '/Users/xxx/Library/Logs/CoreSimulator/2706BBEB-1E01-403E-A8E9-70E8E5A24774', + 'udid': '2706BBEB-1E01-403E-A8E9-70E8E5A24774', + 'isAvailable': true, + 'deviceTypeIdentifier': + 'com.apple.CoreSimulator.SimDeviceType.iPhone-8', + 'state': 'Shutdown', + 'name': 'iPhone 8' + }, + { + 'dataPath': + '/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data', + 'logPath': + '/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'udid': expectedDeviceId, + 'isAvailable': true, + 'deviceTypeIdentifier': + 'com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus', + 'state': 'Shutdown', + 'name': 'iPhone 8 Plus' + } + ] + } + }; + + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: jsonEncode(devices)), + ]; + + expect(await xcode.findBestAvailableIphoneSimulator(), expectedDeviceId); + }); + + test('ignores non-iOS runtimes', () async { + // Note: This uses `dynamic` deliberately, and should not be updated to + // Object, in order to ensure that the code correctly handles this return + // type from JSON decoding. + final Map devices = { + 'runtimes': >[ + { + 'bundlePath': + '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime', + 'buildversion': '17T531', + 'runtimeRoot': + '/Applications/Xcode_11_7.app/Contents/Developer/Platforms/WatchOS.platform/Library/Developer/CoreSimulator/Profiles/Runtimes/watchOS.simruntime/Contents/Resources/RuntimeRoot', + 'identifier': 'com.apple.CoreSimulator.SimRuntime.watchOS-6-2', + 'version': '6.2.1', + 'isAvailable': true, + 'name': 'watchOS 6.2' + } + ], + 'devices': { + 'com.apple.CoreSimulator.SimRuntime.watchOS-6-2': + >[ + { + 'dataPath': + '/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data', + 'logPath': + '/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'udid': '1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'isAvailable': true, + 'deviceTypeIdentifier': + 'com.apple.CoreSimulator.SimDeviceType.Apple-Watch-38mm', + 'state': 'Shutdown', + 'name': 'Apple Watch' + } + ] + } + }; + + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: jsonEncode(devices)), + ]; + + expect(await xcode.findBestAvailableIphoneSimulator(), null); + }); + + test('returns null if simctl fails', () async { + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(exitCode: 1), + ]; + + expect(await xcode.findBestAvailableIphoneSimulator(), null); + }); + }); + + group('runXcodeBuild', () { + test('handles minimal arguments', () async { + final Directory directory = const LocalFileSystem().currentDirectory; + + final int exitCode = await xcode.runXcodeBuild( + directory, + workspace: 'A.xcworkspace', + scheme: 'AScheme', + ); + + expect(exitCode, 0); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'build', + '-workspace', + 'A.xcworkspace', + '-scheme', + 'AScheme', + ], + directory.path), + ])); + }); + + test('handles all arguments', () async { + final Directory directory = const LocalFileSystem().currentDirectory; + + final int exitCode = await xcode.runXcodeBuild(directory, + actions: ['action1', 'action2'], + workspace: 'A.xcworkspace', + scheme: 'AScheme', + configuration: 'Debug', + extraFlags: ['-a', '-b', 'c=d']); + + expect(exitCode, 0); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'action1', + 'action2', + '-workspace', + 'A.xcworkspace', + '-scheme', + 'AScheme', + '-configuration', + 'Debug', + '-a', + '-b', + 'c=d', + ], + directory.path), + ])); + }); + + test('returns error codes', () async { + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(exitCode: 1), + ]; + final Directory directory = const LocalFileSystem().currentDirectory; + + final int exitCode = await xcode.runXcodeBuild( + directory, + workspace: 'A.xcworkspace', + scheme: 'AScheme', + ); + + expect(exitCode, 1); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'build', + '-workspace', + 'A.xcworkspace', + '-scheme', + 'AScheme', + ], + directory.path), + ])); + }); + }); + + group('projectHasTarget', () { + test('returns true when present', () async { + const String stdout = ''' +{ + "project" : { + "configurations" : [ + "Debug", + "Release" + ], + "name" : "Runner", + "schemes" : [ + "Runner" + ], + "targets" : [ + "Runner", + "RunnerTests", + "RunnerUITests" + ] + } +}'''; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: stdout), + ]; + + final Directory project = + const LocalFileSystem().directory('/foo.xcodeproj'); + expect(await xcode.projectHasTarget(project, 'RunnerTests'), true); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + project.path, + ], + null), + ])); + }); + + test('returns false when not present', () async { + const String stdout = ''' +{ + "project" : { + "configurations" : [ + "Debug", + "Release" + ], + "name" : "Runner", + "schemes" : [ + "Runner" + ], + "targets" : [ + "Runner", + "RunnerUITests" + ] + } +}'''; + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: stdout), + ]; + + final Directory project = + const LocalFileSystem().directory('/foo.xcodeproj'); + expect(await xcode.projectHasTarget(project, 'RunnerTests'), false); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + project.path, + ], + null), + ])); + }); + + test('returns null for unexpected output', () async { + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: '{}'), + ]; + + final Directory project = + const LocalFileSystem().directory('/foo.xcodeproj'); + expect(await xcode.projectHasTarget(project, 'RunnerTests'), null); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + project.path, + ], + null), + ])); + }); + + test('returns null for invalid output', () async { + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: ':)'), + ]; + + final Directory project = + const LocalFileSystem().directory('/foo.xcodeproj'); + expect(await xcode.projectHasTarget(project, 'RunnerTests'), null); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + project.path, + ], + null), + ])); + }); + + test('returns null for failure', () async { + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(exitCode: 1), // xcodebuild -list + ]; + + final Directory project = + const LocalFileSystem().directory('/foo.xcodeproj'); + expect(await xcode.projectHasTarget(project, 'RunnerTests'), null); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + project.path, + ], + null), + ])); + }); + }); +} diff --git a/script/tool/test/create_all_plugins_app_command_test.dart b/script/tool/test/create_all_plugins_app_command_test.dart new file mode 100644 index 000000000000..0066cc53f61a --- /dev/null +++ b/script/tool/test/create_all_plugins_app_command_test.dart @@ -0,0 +1,118 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package: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; + late CreateAllPluginsAppCommand command; + late FileSystem fileSystem; + late Directory testRoot; + late Directory packagesDir; + + setUp(() { + // Since the core of this command is a call to 'flutter create', the test + // has to use the real filesystem. Put everything possible in a unique + // temporary to minimize effect on the host system. + fileSystem = const LocalFileSystem(); + testRoot = fileSystem.systemTempDirectory.createTempSync(); + packagesDir = testRoot.childDirectory('packages'); + + command = CreateAllPluginsAppCommand( + packagesDir, + pluginsRoot: testRoot, + ); + 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 runCapturingPrint(runner, ['all-plugins-app']); + final List pubspec = + command.appDirectory.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 runCapturingPrint(runner, ['all-plugins-app']); + final List pubspec = + command.appDirectory.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 runCapturingPrint(runner, ['all-plugins-app']); + final String pubspec = + command.appDirectory.childFile('pubspec.yaml').readAsStringSync(); + + expect(pubspec, contains(RegExp('sdk:\\s*(?:["\']>=|[^])2\\.12\\.'))); + }); + + test('handles --output-dir', () async { + createFakePlugin('plugina', packagesDir); + + final Directory customOutputDir = + fileSystem.systemTempDirectory.createTempSync(); + await runCapturingPrint(runner, + ['all-plugins-app', '--output-dir=${customOutputDir.path}']); + + expect(command.appDirectory.path, + customOutputDir.childDirectory('all_plugins').path); + }); + + test('logs exclusions', () async { + createFakePlugin('plugina', packagesDir); + createFakePlugin('pluginb', packagesDir); + createFakePlugin('pluginc', packagesDir); + + final List output = await runCapturingPrint( + runner, ['all-plugins-app', '--exclude=pluginb,pluginc']); + + expect( + output, + containsAllInOrder([ + 'Exluding the following plugins from the combined build:', + ' pluginb', + ' pluginc', + ])); + }); + }); +} 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..a7a1652c2fc2 --- /dev/null +++ b/script/tool/test/drive_examples_command_test.dart @@ -0,0 +1,1046 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:flutter_plugin_tools/src/drive_examples_command.dart'; +import 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +const String _fakeIosDevice = '67d5c3d1-8bdf-46ad-8f6b-b00e2a972dda'; +const String _fakeAndroidDevice = 'emulator-1234'; + +void main() { + group('test drive_example_command', () { + late FileSystem fileSystem; + late Platform mockPlatform; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final DriveExamplesCommand command = DriveExamplesCommand(packagesDir, + processRunner: processRunner, platform: mockPlatform); + + runner = CommandRunner( + 'drive_examples_command', 'Test for drive_example_command'); + runner.addCommand(command); + }); + + void setMockFlutterDevicesOutput({ + bool hasIosDevice = true, + bool hasAndroidDevice = true, + bool includeBanner = false, + }) { + const String updateBanner = ''' +╔════════════════════════════════════════════════════════════════════════════╗ +║ A new version of Flutter is available! ║ +║ ║ +║ To update to the latest version, run "flutter upgrade". ║ +╚════════════════════════════════════════════════════════════════════════════╝ +'''; + final List devices = [ + if (hasIosDevice) '{"id": "$_fakeIosDevice", "targetPlatform": "ios"}', + if (hasAndroidDevice) + '{"id": "$_fakeAndroidDevice", "targetPlatform": "android-x86"}', + ]; + final String output = + '''${includeBanner ? updateBanner : ''}[${devices.join(',')}]'''; + + final MockProcess mockDevicesProcess = + MockProcess(stdout: output, stdoutEncoding: utf8); + processRunner + .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = + [mockDevicesProcess]; + } + + test('fails if no platforms are provided', () async { + setMockFlutterDevicesOutput(); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Exactly one of'), + ]), + ); + }); + + test('fails if multiple platforms are provided', () async { + setMockFlutterDevicesOutput(); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--ios', '--macos'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Exactly one of'), + ]), + ); + }); + + test('fails for iOS if no iOS devices are present', () async { + setMockFlutterDevicesOutput(hasIosDevice: false); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--ios'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No iOS devices'), + ]), + ); + }); + + test('handles flutter tool banners when checking devices', () async { + createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/integration_test.dart', + 'example/integration_test/foo_test.dart', + ], + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + }, + ); + + setMockFlutterDevicesOutput(includeBanner: true); + final List output = + await runCapturingPrint(runner, ['drive-examples', '--ios']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), + ]), + ); + }); + + test('fails for iOS if getting devices fails', () async { + // Simulate failure from `flutter devices`. + processRunner + .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = + [MockProcess(exitCode: 1)]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--ios'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No iOS devices'), + ]), + ); + }); + + test('fails for Android if no Android devices are present', () async { + setMockFlutterDevicesOutput(hasAndroidDevice: false); + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--android'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No Android devices'), + ]), + ); + }); + + test('driving under folder "test_driver"', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ], + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + }, + ); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + setMockFlutterDevicesOutput(); + final List output = + await runCapturingPrint(runner, ['drive-examples', '--ios']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['devices', '--machine'], null), + ProcessCall( + getFlutterCommand(mockPlatform), + const [ + 'drive', + '-d', + _fakeIosDevice, + '--driver', + 'test_driver/plugin_test.dart', + '--target', + 'test_driver/plugin.dart' + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving under folder "test_driver" when test files are missing"', + () async { + setMockFlutterDevicesOutput(); + createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + ], + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + }, + ); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--android'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No driver tests were run (1 example(s) found).'), + contains('No test files for example/test_driver/plugin_test.dart'), + ]), + ); + }); + + test('a plugin without any integration test files is reported as an error', + () async { + setMockFlutterDevicesOutput(); + createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/lib/main.dart', + ], + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + }, + ); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--android'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No driver tests were run (1 example(s) found).'), + contains('No tests ran'), + ]), + ); + }); + + test( + 'driving under folder "test_driver" when targets are under "integration_test"', + () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/integration_test.dart', + 'example/integration_test/bar_test.dart', + 'example/integration_test/foo_test.dart', + 'example/integration_test/ignore_me.dart', + ], + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + }, + ); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + setMockFlutterDevicesOutput(); + final List output = + await runCapturingPrint(runner, ['drive-examples', '--ios']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['devices', '--machine'], null), + ProcessCall( + getFlutterCommand(mockPlatform), + const [ + 'drive', + '-d', + _fakeIosDevice, + '--driver', + 'test_driver/integration_test.dart', + '--target', + 'integration_test/bar_test.dart', + ], + pluginExampleDirectory.path), + ProcessCall( + getFlutterCommand(mockPlatform), + const [ + 'drive', + '-d', + _fakeIosDevice, + '--driver', + 'test_driver/integration_test.dart', + '--target', + 'integration_test/foo_test.dart', + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not support Linux is a no-op', () async { + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ]); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--linux', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('Skipping unsupported platform linux...'), + contains('No issues found!'), + ]), + ); + + // Output should be empty since running drive-examples --linux on a non-Linux + // plugin is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('driving on a Linux plugin', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ], + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + }, + ); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--linux', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const [ + 'drive', + '-d', + 'linux', + '--driver', + 'test_driver/plugin_test.dart', + '--target', + 'test_driver/plugin.dart' + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not suppport macOS is a no-op', () async { + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ]); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--macos', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('Skipping unsupported platform macos...'), + contains('No issues found!'), + ]), + ); + + // Output should be empty since running drive-examples --macos with no macos + // implementation is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('driving on a macOS plugin', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + 'example/macos/macos.swift', + ], + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }, + ); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--macos', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const [ + 'drive', + '-d', + 'macos', + '--driver', + 'test_driver/plugin_test.dart', + '--target', + 'test_driver/plugin.dart' + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not suppport web is a no-op', () async { + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ]); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--web', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), + ]), + ); + + // Output should be empty since running drive-examples --web on a non-web + // plugin is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('driving a web plugin', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ], + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + }, + ); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--web', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const [ + 'drive', + '-d', + 'web-server', + '--web-port=7357', + '--browser-name=chrome', + '--driver', + 'test_driver/plugin_test.dart', + '--target', + 'test_driver/plugin.dart' + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not suppport Windows is a no-op', () async { + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ]); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--windows', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('Skipping unsupported platform windows...'), + contains('No issues found!'), + ]), + ); + + // Output should be empty since running drive-examples --windows on a + // non-Windows plugin is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('driving on a Windows plugin', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ], + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + }, + ); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--windows', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const [ + 'drive', + '-d', + 'windows', + '--driver', + 'test_driver/plugin_test.dart', + '--target', + 'test_driver/plugin.dart' + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving UWP is a no-op', () async { + createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ], + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline, + variants: [platformVariantWinUwp]), + }, + ); + + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--winuwp', + ]); + + expect( + output, + containsAllInOrder([ + contains('Driving UWP applications is not yet supported'), + contains('Running for plugin'), + contains('SKIPPING: Drive does not yet support UWP'), + contains('No issues found!'), + ]), + ); + + // Output should be empty since running drive-examples --windows on a + // non-Windows plugin is a no-op. + expect(processRunner.recordedCalls, []); + }); + + test('driving on an Android plugin', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ], + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + }, + ); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + setMockFlutterDevicesOutput(); + final List output = await runCapturingPrint(runner, [ + 'drive-examples', + '--android', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['devices', '--machine'], null), + ProcessCall( + getFlutterCommand(mockPlatform), + const [ + 'drive', + '-d', + _fakeAndroidDevice, + '--driver', + 'test_driver/plugin_test.dart', + '--target', + 'test_driver/plugin.dart' + ], + pluginExampleDirectory.path), + ])); + }); + + test('driving when plugin does not support Android is no-op', () async { + createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ], + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }, + ); + + setMockFlutterDevicesOutput(); + final List output = await runCapturingPrint( + runner, ['drive-examples', '--android']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('Skipping unsupported platform android...'), + contains('No issues found!'), + ]), + ); + + // Output should be empty other than the device query. + expect(processRunner.recordedCalls, [ + ProcessCall(getFlutterCommand(mockPlatform), + const ['devices', '--machine'], null), + ]); + }); + + test('driving when plugin does not support iOS is no-op', () async { + createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ], + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }, + ); + + setMockFlutterDevicesOutput(); + final List output = + await runCapturingPrint(runner, ['drive-examples', '--ios']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('Skipping unsupported platform ios...'), + contains('No issues found!'), + ]), + ); + + // Output should be empty other than the device query. + expect(processRunner.recordedCalls, [ + ProcessCall(getFlutterCommand(mockPlatform), + const ['devices', '--machine'], null), + ]); + }); + + test('platform interface plugins are silently skipped', () async { + createFakePlugin('aplugin_platform_interface', packagesDir, + examples: []); + + setMockFlutterDevicesOutput(); + final List output = await runCapturingPrint( + runner, ['drive-examples', '--macos']); + + expect( + output, + containsAllInOrder([ + contains('Running for aplugin_platform_interface'), + contains( + 'SKIPPING: Platform interfaces are not expected to have integration tests.'), + contains('No issues found!'), + ]), + ); + + // Output should be empty since it's skipped. + expect(processRunner.recordedCalls, []); + }); + + test('enable-experiment flag', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/plugin_test.dart', + 'example/test_driver/plugin.dart', + ], + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + }, + ); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + setMockFlutterDevicesOutput(); + await runCapturingPrint(runner, [ + 'drive-examples', + '--ios', + '--enable-experiment=exp1', + ]); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['devices', '--machine'], null), + ProcessCall( + getFlutterCommand(mockPlatform), + const [ + 'drive', + '-d', + _fakeIosDevice, + '--enable-experiment=exp1', + '--driver', + 'test_driver/plugin_test.dart', + '--target', + 'test_driver/plugin.dart' + ], + pluginExampleDirectory.path), + ])); + }); + + test('fails when no example is present', () async { + createFakePlugin( + 'plugin', + packagesDir, + examples: [], + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + }, + ); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--web'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No driver tests were run (0 example(s) found).'), + contains('The following packages had errors:'), + contains(' plugin:\n' + ' No tests ran (use --exclude if this is intentional)'), + ]), + ); + }); + + test('fails when no driver is present', () async { + createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/integration_test/bar_test.dart', + 'example/integration_test/foo_test.dart', + ], + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + }, + ); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--web'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No driver tests found for plugin/example'), + contains('No driver tests were run (1 example(s) found).'), + contains('The following packages had errors:'), + contains(' plugin:\n' + ' No tests ran (use --exclude if this is intentional)'), + ]), + ); + }); + + test('fails when no integration tests are present', () async { + createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/integration_test.dart', + ], + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + }, + ); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['drive-examples', '--web'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('Found example/test_driver/integration_test.dart, but no ' + 'integration_test/*_test.dart files.'), + contains('No driver tests were run (1 example(s) found).'), + contains('The following packages had errors:'), + contains(' plugin:\n' + ' No test files for example/test_driver/integration_test.dart\n' + ' No tests ran (use --exclude if this is intentional)'), + ]), + ); + }); + + test('reports test failures', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/test_driver/integration_test.dart', + 'example/integration_test/bar_test.dart', + 'example/integration_test/foo_test.dart', + ], + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }, + ); + + // Simulate failure from `flutter drive`. + processRunner + .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = + [ + // No mock for 'devices', since it's running for macOS. + MockProcess(exitCode: 1), // 'drive' #1 + MockProcess(exitCode: 1), // 'drive' #2 + ]; + + Error? commandError; + final List output = + await runCapturingPrint(runner, ['drive-examples', '--macos'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('The following packages had errors:'), + contains(' plugin:\n' + ' example/integration_test/bar_test.dart\n' + ' example/integration_test/foo_test.dart'), + ]), + ); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const [ + 'drive', + '-d', + 'macos', + '--driver', + 'test_driver/integration_test.dart', + '--target', + 'integration_test/bar_test.dart', + ], + pluginExampleDirectory.path), + ProcessCall( + getFlutterCommand(mockPlatform), + const [ + 'drive', + '-d', + 'macos', + '--driver', + 'test_driver/integration_test.dart', + '--target', + 'integration_test/foo_test.dart', + ], + pluginExampleDirectory.path), + ])); + }); + }); +} diff --git a/script/tool/test/federation_safety_check_command_test.dart b/script/tool/test/federation_safety_check_command_test.dart new file mode 100644 index 000000000000..e23485fbc8b7 --- /dev/null +++ b/script/tool/test/federation_safety_check_command_test.dart @@ -0,0 +1,355 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/repository_package.dart'; +import 'package:flutter_plugin_tools/src/federation_safety_check_command.dart'; +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'common/plugin_command_test.mocks.dart'; +import 'mocks.dart'; +import 'util.dart'; + +void main() { + FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + + final MockGitDir gitDir = MockGitDir(); + when(gitDir.path).thenReturn(packagesDir.parent.path); + when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) + .thenAnswer((Invocation invocation) { + final List arguments = + invocation.positionalArguments[0]! as List; + // Route git calls through the process runner, to make mock output + // consistent with other processes. Attach the first argument to the + // command to make targeting the mock results easier. + final String gitCommand = arguments.removeAt(0); + return processRunner.run('git-$gitCommand', arguments); + }); + + processRunner = RecordingProcessRunner(); + final FederationSafetyCheckCommand command = FederationSafetyCheckCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + gitDir: gitDir); + + runner = CommandRunner('federation_safety_check_command', + 'Test for $FederationSafetyCheckCommand'); + runner.addCommand(command); + }); + + test('skips non-plugin packages', () async { + final Directory package = createFakePackage('foo', packagesDir); + + final String changedFileOutput = [ + package.childDirectory('lib').childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo...'), + contains('Not a plugin'), + contains('Skipped 1 package(s)'), + ]), + ); + }); + + test('skips unfederated plugins', () async { + final Directory package = createFakePlugin('foo', packagesDir); + + final String changedFileOutput = [ + package.childDirectory('lib').childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo...'), + contains('Not a federated plugin'), + contains('Skipped 1 package(s)'), + ]), + ); + }); + + test('skips interface packages', () async { + final Directory pluginGroupDir = packagesDir.childDirectory('foo'); + final Directory platformInterface = + createFakePlugin('foo_platform_interface', pluginGroupDir); + + final String changedFileOutput = [ + platformInterface.childDirectory('lib').childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo_platform_interface...'), + contains('Platform interface changes are not validated.'), + contains('Skipped 1 package(s)'), + ]), + ); + }); + + test('allows changes to just an interface package', () async { + final Directory pluginGroupDir = packagesDir.childDirectory('foo'); + final Directory platformInterface = + createFakePlugin('foo_platform_interface', pluginGroupDir); + createFakePlugin('foo', pluginGroupDir); + createFakePlugin('foo_ios', pluginGroupDir); + createFakePlugin('foo_android', pluginGroupDir); + + final String changedFileOutput = [ + platformInterface.childDirectory('lib').childFile('foo.dart'), + platformInterface.childFile('pubspec.yaml'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo/foo...'), + contains('No Dart changes.'), + contains('Running for foo_android...'), + contains('No Dart changes.'), + contains('Running for foo_ios...'), + contains('No Dart changes.'), + contains('Running for foo_platform_interface...'), + contains('Ran for 3 package(s)'), + contains('Skipped 1 package(s)'), + ]), + ); + expect( + output, + isNot(contains([ + contains('No published changes for foo_platform_interface'), + ])), + ); + }); + + test('allows changes to multiple non-interface packages', () async { + final Directory pluginGroupDir = packagesDir.childDirectory('foo'); + final Directory appFacing = createFakePlugin('foo', pluginGroupDir); + final Directory implementation = + createFakePlugin('foo_bar', pluginGroupDir); + createFakePlugin('foo_platform_interface', pluginGroupDir); + + final String changedFileOutput = [ + appFacing.childFile('foo.dart'), + implementation.childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo/foo...'), + contains('No published changes for foo_platform_interface.'), + contains('Running for foo_bar...'), + contains('No published changes for foo_platform_interface.'), + ]), + ); + }); + + test( + 'fails on changes to interface and non-interface packages in the same plugin', + () async { + final Directory pluginGroupDir = packagesDir.childDirectory('foo'); + final Directory appFacing = createFakePlugin('foo', pluginGroupDir); + final Directory implementation = + createFakePlugin('foo_bar', pluginGroupDir); + final Directory platformInterface = + createFakePlugin('foo_platform_interface', pluginGroupDir); + + final String changedFileOutput = [ + appFacing.childFile('foo.dart'), + implementation.childFile('foo.dart'), + platformInterface.childFile('pubspec.yaml'), + platformInterface.childDirectory('lib').childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['federation-safety-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running for foo/foo...'), + contains('Dart changes are not allowed to other packages in foo in the ' + 'same PR as changes to public Dart code in foo_platform_interface, ' + 'as this can cause accidental breaking changes to be missed by ' + 'automated checks. Please split the changes to these two packages ' + 'into separate PRs.'), + contains('Running for foo_bar...'), + contains('Dart changes are not allowed to other packages in foo'), + contains('The following packages had errors:'), + contains('foo/foo:\n' + ' foo_platform_interface changed.'), + contains('foo_bar:\n' + ' foo_platform_interface changed.'), + ]), + ); + }); + + test('ignores test-only changes to interface packages', () async { + final Directory pluginGroupDir = packagesDir.childDirectory('foo'); + final Directory appFacing = createFakePlugin('foo', pluginGroupDir); + final Directory implementation = + createFakePlugin('foo_bar', pluginGroupDir); + final Directory platformInterface = + createFakePlugin('foo_platform_interface', pluginGroupDir); + + final String changedFileOutput = [ + appFacing.childFile('foo.dart'), + implementation.childFile('foo.dart'), + platformInterface.childFile('pubspec.yaml'), + platformInterface.childDirectory('test').childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo/foo...'), + contains('No public code changes for foo_platform_interface.'), + contains('Running for foo_bar...'), + contains('No public code changes for foo_platform_interface.'), + ]), + ); + }); + + test('ignores unpublished changes to interface packages', () async { + final Directory pluginGroupDir = packagesDir.childDirectory('foo'); + final Directory appFacing = createFakePlugin('foo', pluginGroupDir); + final Directory implementation = + createFakePlugin('foo_bar', pluginGroupDir); + final Directory platformInterface = + createFakePlugin('foo_platform_interface', pluginGroupDir); + + final String changedFileOutput = [ + appFacing.childFile('foo.dart'), + implementation.childFile('foo.dart'), + platformInterface.childFile('pubspec.yaml'), + platformInterface.childDirectory('lib').childFile('foo.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + // Simulate no change to the version in the interface's pubspec.yaml. + processRunner.mockProcessesForExecutable['git-show'] = [ + MockProcess( + stdout: RepositoryPackage(platformInterface) + .pubspecFile + .readAsStringSync()), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo/foo...'), + contains('No published changes for foo_platform_interface.'), + contains('Running for foo_bar...'), + contains('No published changes for foo_platform_interface.'), + ]), + ); + }); + + test('allows things that look like mass changes, with warning', () async { + final Directory pluginGroupDir = packagesDir.childDirectory('foo'); + final Directory appFacing = createFakePlugin('foo', pluginGroupDir); + final Directory implementation = + createFakePlugin('foo_bar', pluginGroupDir); + final Directory platformInterface = + createFakePlugin('foo_platform_interface', pluginGroupDir); + + final Directory otherPlugin1 = createFakePlugin('bar', packagesDir); + final Directory otherPlugin2 = createFakePlugin('baz', packagesDir); + + final String changedFileOutput = [ + appFacing.childFile('foo.dart'), + implementation.childFile('foo.dart'), + platformInterface.childFile('pubspec.yaml'), + platformInterface.childDirectory('lib').childFile('foo.dart'), + otherPlugin1.childFile('bar.dart'), + otherPlugin2.childFile('baz.dart'), + ].map((File file) => file.path).join('\n'); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: changedFileOutput), + ]; + + final List output = + await runCapturingPrint(runner, ['federation-safety-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for foo/foo...'), + contains( + 'Ignoring potentially dangerous change, as this appears to be a mass change.'), + contains('Running for foo_bar...'), + contains( + 'Ignoring potentially dangerous change, as this appears to be a mass change.'), + contains('Ran for 2 package(s) (2 with warnings)'), + ]), + ); + }); +} diff --git a/script/tool/test/firebase_test_lab_command_test.dart b/script/tool/test/firebase_test_lab_command_test.dart new file mode 100644 index 000000000000..e39ccf30b136 --- /dev/null +++ b/script/tool/test/firebase_test_lab_command_test.dart @@ -0,0 +1,611 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/firebase_test_lab_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('$FirebaseTestLabCommand', () { + FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final FirebaseTestLabCommand command = FirebaseTestLabCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = CommandRunner( + 'firebase_test_lab_command', 'Test for $FirebaseTestLabCommand'); + runner.addCommand(command); + }); + + test('fails if gcloud auth fails', () async { + processRunner.mockProcessesForExecutable['gcloud'] = [ + MockProcess(exitCode: 1) + ]; + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/integration_test/foo_test.dart', + 'example/android/gradlew', + 'example/android/app/src/androidTest/MainActivityTest.java', + ]); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['firebase-test-lab'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to activate gcloud account.'), + ])); + }); + + test('retries gcloud set', () async { + processRunner.mockProcessesForExecutable['gcloud'] = [ + MockProcess(), // auth + MockProcess(exitCode: 1), // config + ]; + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/integration_test/foo_test.dart', + 'example/android/gradlew', + 'example/android/app/src/androidTest/MainActivityTest.java', + ]); + + final List output = + await runCapturingPrint(runner, ['firebase-test-lab']); + + expect( + output, + containsAllInOrder([ + contains( + 'Warning: gcloud config set returned a non-zero exit code. Continuing anyway.'), + ])); + }); + + test('only runs gcloud configuration once', () async { + createFakePlugin('plugin1', packagesDir, extraFiles: [ + 'test/plugin_test.dart', + 'example/integration_test/foo_test.dart', + 'example/android/gradlew', + 'example/android/app/src/androidTest/MainActivityTest.java', + ]); + createFakePlugin('plugin2', packagesDir, extraFiles: [ + 'test/plugin_test.dart', + 'example/integration_test/bar_test.dart', + 'example/android/gradlew', + 'example/android/app/src/androidTest/MainActivityTest.java', + ]); + + final List output = await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--device', + 'model=seoul,version=26', + '--test-run-id', + 'testRunId', + '--build-id', + 'buildId', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin1'), + contains('Firebase project configured.'), + contains('Testing example/integration_test/foo_test.dart...'), + contains('Running for plugin2'), + contains('Testing example/integration_test/bar_test.dart...'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'gcloud', + 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' + .split(' '), + null), + ProcessCall( + 'gcloud', 'config set project flutter-cirrus'.split(' '), null), + ProcessCall( + '/packages/plugin1/example/android/gradlew', + 'app:assembleAndroidTest -Pverbose=true'.split(' '), + '/packages/plugin1/example/android'), + ProcessCall( + '/packages/plugin1/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin1/example/integration_test/foo_test.dart' + .split(' '), + '/packages/plugin1/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin1/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin1/example'), + ProcessCall( + '/packages/plugin2/example/android/gradlew', + 'app:assembleAndroidTest -Pverbose=true'.split(' '), + '/packages/plugin2/example/android'), + ProcessCall( + '/packages/plugin2/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin2/example/integration_test/bar_test.dart' + .split(' '), + '/packages/plugin2/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin2/buildId/testRunId/0/ --device model=flame,version=29 --device model=seoul,version=26' + .split(' '), + '/packages/plugin2/example'), + ]), + ); + }); + + test('runs integration tests', () async { + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'test/plugin_test.dart', + 'example/integration_test/bar_test.dart', + 'example/integration_test/foo_test.dart', + 'example/integration_test/should_not_run.dart', + 'example/android/gradlew', + 'example/android/app/src/androidTest/MainActivityTest.java', + ]); + + final List output = await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--device', + 'model=seoul,version=26', + '--test-run-id', + 'testRunId', + '--build-id', + 'buildId', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('Firebase project configured.'), + contains('Testing example/integration_test/bar_test.dart...'), + contains('Testing example/integration_test/foo_test.dart...'), + ]), + ); + expect(output, isNot(contains('test/plugin_test.dart'))); + expect(output, + isNot(contains('example/integration_test/should_not_run.dart'))); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'gcloud', + 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' + .split(' '), + null), + ProcessCall( + 'gcloud', 'config set project flutter-cirrus'.split(' '), null), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleAndroidTest -Pverbose=true'.split(' '), + '/packages/plugin/example/android'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/bar_test.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_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/integration_test/foo_test.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_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'), + ]), + ); + }); + + test('fails if a test fails', () async { + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/integration_test/bar_test.dart', + 'example/integration_test/foo_test.dart', + 'example/android/gradlew', + 'example/android/app/src/androidTest/MainActivityTest.java', + ]); + + processRunner.mockProcessesForExecutable['gcloud'] = [ + MockProcess(), // auth + MockProcess(), // config + MockProcess(exitCode: 1), // integration test #1 + MockProcess(), // integration test #2 + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, + [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--device', + 'model=seoul,version=26', + '--test-run-id', + 'testRunId', + '--build-id', + 'buildId', + ], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Testing example/integration_test/bar_test.dart...'), + contains('Testing example/integration_test/foo_test.dart...'), + contains('plugin:\n' + ' example/integration_test/bar_test.dart failed tests'), + ]), + ); + }); + + test('fails for packages with no androidTest directory', () async { + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/integration_test/foo_test.dart', + 'example/android/gradlew', + ]); + + Error? commandError; + final List output = await runCapturingPrint( + runner, + [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--device', + 'model=seoul,version=26', + '--test-run-id', + 'testRunId', + '--build-id', + 'buildId', + ], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No androidTest directory found.'), + contains('The following packages had errors:'), + contains('plugin:\n' + ' No tests ran (use --exclude if this is intentional).'), + ]), + ); + }); + + test('fails for packages with no integration test files', () async { + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/androidTest/MainActivityTest.java', + ]); + + Error? commandError; + final List output = await runCapturingPrint( + runner, + [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--device', + 'model=seoul,version=26', + '--test-run-id', + 'testRunId', + '--build-id', + 'buildId', + ], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('No integration tests were run'), + contains('The following packages had errors:'), + contains('plugin:\n' + ' No tests ran (use --exclude if this is intentional).'), + ]), + ); + }); + + test('skips packages with no android directory', () async { + createFakePackage('package', packagesDir, extraFiles: [ + 'example/integration_test/foo_test.dart', + ]); + + final List output = await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--device', + 'model=seoul,version=26', + '--test-run-id', + 'testRunId', + '--build-id', + 'buildId', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for package'), + contains('package/example does not support Android'), + ]), + ); + expect(output, + isNot(contains('Testing example/integration_test/foo_test.dart...'))); + + expect( + processRunner.recordedCalls, + orderedEquals([]), + ); + }); + + test('builds if gradlew is missing', () async { + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/integration_test/foo_test.dart', + 'example/android/app/src/androidTest/MainActivityTest.java', + ]); + + final List output = await runCapturingPrint(runner, [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + '--device', + 'model=seoul,version=26', + '--test-run-id', + 'testRunId', + '--build-id', + 'buildId', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('Running flutter build apk...'), + contains('Firebase project configured.'), + contains('Testing example/integration_test/foo_test.dart...'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'flutter', + 'build apk'.split(' '), + '/packages/plugin/example/android', + ), + ProcessCall( + 'gcloud', + 'auth activate-service-account --key-file=${Platform.environment['HOME']}/gcloud-service-key.json' + .split(' '), + null), + ProcessCall( + 'gcloud', 'config set project flutter-cirrus'.split(' '), null), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleAndroidTest -Pverbose=true'.split(' '), + '/packages/plugin/example/android'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_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'), + ]), + ); + }); + + test('fails if building to generate gradlew fails', () async { + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/integration_test/foo_test.dart', + 'example/android/app/src/androidTest/MainActivityTest.java', + ]); + + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess(exitCode: 1) // flutter build + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, + [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + ], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to build example apk'), + ])); + }); + + test('fails if assembleAndroidTest fails', () async { + final Directory pluginDir = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/integration_test/foo_test.dart', + 'example/android/app/src/androidTest/MainActivityTest.java', + ]); + + final String gradlewPath = pluginDir + .childDirectory('example') + .childDirectory('android') + .childFile('gradlew') + .path; + processRunner.mockProcessesForExecutable[gradlewPath] = [ + MockProcess(exitCode: 1) + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, + [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + ], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to assemble androidTest'), + ])); + }); + + test('fails if assembleDebug fails', () async { + final Directory pluginDir = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/integration_test/foo_test.dart', + 'example/android/app/src/androidTest/MainActivityTest.java', + ]); + + final String gradlewPath = pluginDir + .childDirectory('example') + .childDirectory('android') + .childFile('gradlew') + .path; + processRunner.mockProcessesForExecutable[gradlewPath] = [ + MockProcess(), // assembleAndroidTest + MockProcess(exitCode: 1), // assembleDebug + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, + [ + 'firebase-test-lab', + '--device', + 'model=flame,version=29', + ], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Could not build example/integration_test/foo_test.dart'), + contains('The following packages had errors:'), + contains(' plugin:\n' + ' example/integration_test/foo_test.dart failed to build'), + ])); + }); + + test('experimental flag', () async { + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/integration_test/foo_test.dart', + 'example/android/gradlew', + '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-cirrus'.split(' '), null), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleAndroidTest -Pverbose=true -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + '/packages/plugin/example/android/gradlew', + 'app:assembleDebug -Pverbose=true -Ptarget=/packages/plugin/example/integration_test/foo_test.dart -Pextra-front-end-options=--enable-experiment%3Dexp1 -Pextra-gen-snapshot-options=--enable-experiment%3Dexp1' + .split(' '), + '/packages/plugin/example/android'), + ProcessCall( + 'gcloud', + 'firebase test android run --type instrumentation --app build/app/outputs/apk/debug/app-debug.apk --test build/app/outputs/apk/androidTest/debug/app-debug-androidTest.apk --timeout 7m --results-bucket=gs://flutter_firebase_testlab --results-dir=plugins_android_test/plugin/buildId/testRunId/0/ --device model=flame,version=29' + .split(' '), + '/packages/plugin/example'), + ]), + ); + }); + }); +} diff --git a/script/tool/test/format_command_test.dart b/script/tool/test/format_command_test.dart new file mode 100644 index 000000000000..d278bb2940b8 --- /dev/null +++ b/script/tool/test/format_command_test.dart @@ -0,0 +1,627 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/file_utils.dart'; +import 'package:flutter_plugin_tools/src/format_command.dart'; +import 'package:path/path.dart' as p; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late RecordingProcessRunner processRunner; + late FormatCommand analyzeCommand; + late CommandRunner runner; + late String javaFormatPath; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + analyzeCommand = FormatCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + // Create the java formatter file that the command checks for, to avoid + // a download. + final p.Context path = analyzeCommand.path; + javaFormatPath = path.join(path.dirname(path.fromUri(mockPlatform.script)), + 'google-java-format-1.3-all-deps.jar'); + fileSystem.file(javaFormatPath).createSync(recursive: true); + + runner = CommandRunner('format_command', 'Test for format_command'); + runner.addCommand(analyzeCommand); + }); + + /// Returns a modified version of a list of [relativePaths] that are relative + /// to [package] to instead be relative to [packagesDir]. + List _getPackagesDirRelativePaths( + Directory packageDir, List relativePaths) { + final p.Context path = analyzeCommand.path; + final String relativeBase = + path.relative(packageDir.path, from: packagesDir.path); + return relativePaths + .map((String relativePath) => path.join(relativeBase, relativePath)) + .toList(); + } + + /// Returns a list of [count] relative paths to pass to [createFakePlugin] + /// with name [pluginName] such that each path will be 99 characters long + /// relative to [packagesDir]. + /// + /// This is for each of testing batching, since it means each file will + /// consume 100 characters of the batch length. + List _get99CharacterPathExtraFiles(String pluginName, int count) { + final int padding = 99 - + pluginName.length - + 1 - // the path separator after the plugin name + 1 - // the path separator after the padding + 10; // the file name + const int filenameBase = 10000; + + final p.Context path = analyzeCommand.path; + return [ + for (int i = filenameBase; i < filenameBase + count; ++i) + path.join('a' * padding, '$i.dart'), + ]; + } + + test('formats .dart files', () async { + const List files = [ + 'lib/a.dart', + 'lib/src/b.dart', + 'lib/src/c.dart', + ]; + final Directory pluginDir = createFakePlugin( + 'a_plugin', + packagesDir, + extraFiles: files, + ); + + await runCapturingPrint(runner, ['format']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + [ + 'format', + ..._getPackagesDirRelativePaths(pluginDir, files) + ], + packagesDir.path), + ])); + }); + + test('does not format .dart files with pragma', () async { + const List formattedFiles = [ + 'lib/a.dart', + 'lib/src/b.dart', + 'lib/src/c.dart', + ]; + const String unformattedFile = 'lib/src/d.dart'; + final Directory pluginDir = createFakePlugin( + 'a_plugin', + packagesDir, + extraFiles: [ + ...formattedFiles, + unformattedFile, + ], + ); + + final p.Context posixContext = p.posix; + childFileWithSubcomponents(pluginDir, posixContext.split(unformattedFile)) + .writeAsStringSync( + '// copyright bla bla\n// This file is hand-formatted.\ncode...'); + + await runCapturingPrint(runner, ['format']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + [ + 'format', + ..._getPackagesDirRelativePaths(pluginDir, formattedFiles) + ], + packagesDir.path), + ])); + }); + + test('fails if flutter format fails', () async { + const List files = [ + 'lib/a.dart', + 'lib/src/b.dart', + 'lib/src/c.dart', + ]; + createFakePlugin('a_plugin', packagesDir, extraFiles: files); + + processRunner.mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = + [MockProcess(exitCode: 1)]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['format'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Failed to format Dart files: exit code 1.'), + ])); + }); + + test('formats .java files', () async { + const List files = [ + 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', + 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', + ]; + final Directory pluginDir = createFakePlugin( + 'a_plugin', + packagesDir, + extraFiles: files, + ); + + await runCapturingPrint(runner, ['format']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + const ProcessCall('java', ['-version'], null), + ProcessCall( + 'java', + [ + '-jar', + javaFormatPath, + '--replace', + ..._getPackagesDirRelativePaths(pluginDir, files) + ], + packagesDir.path), + ])); + }); + + test('fails with a clear message if Java is not in the path', () async { + const List files = [ + 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', + 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', + ]; + createFakePlugin('a_plugin', packagesDir, extraFiles: files); + + processRunner.mockProcessesForExecutable['java'] = [ + MockProcess(exitCode: 1) + ]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['format'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Unable to run \'java\'. Make sure that it is in your path, or ' + 'provide a full path with --java.'), + ])); + }); + + test('fails if Java formatter fails', () async { + const List files = [ + 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', + 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', + ]; + createFakePlugin('a_plugin', packagesDir, extraFiles: files); + + processRunner.mockProcessesForExecutable['java'] = [ + MockProcess(), // check for working java + MockProcess(exitCode: 1), // format + ]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['format'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Failed to format Java files: exit code 1.'), + ])); + }); + + test('honors --java flag', () async { + const List files = [ + 'android/src/main/java/io/flutter/plugins/a_plugin/a.java', + 'android/src/main/java/io/flutter/plugins/a_plugin/b.java', + ]; + final Directory pluginDir = createFakePlugin( + 'a_plugin', + packagesDir, + extraFiles: files, + ); + + await runCapturingPrint(runner, ['format', '--java=/path/to/java']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + const ProcessCall('/path/to/java', ['--version'], null), + ProcessCall( + '/path/to/java', + [ + '-jar', + javaFormatPath, + '--replace', + ..._getPackagesDirRelativePaths(pluginDir, files) + ], + packagesDir.path), + ])); + }); + + test('formats c-ish files', () async { + const List files = [ + 'ios/Classes/Foo.h', + 'ios/Classes/Foo.m', + 'linux/foo_plugin.cc', + 'macos/Classes/Foo.h', + 'macos/Classes/Foo.mm', + 'windows/foo_plugin.cpp', + ]; + final Directory pluginDir = createFakePlugin( + 'a_plugin', + packagesDir, + extraFiles: files, + ); + + await runCapturingPrint(runner, ['format']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + const ProcessCall('clang-format', ['--version'], null), + ProcessCall( + 'clang-format', + [ + '-i', + '--style=Google', + ..._getPackagesDirRelativePaths(pluginDir, files) + ], + packagesDir.path), + ])); + }); + + test('fails with a clear message if clang-format is not in the path', + () async { + const List files = [ + 'linux/foo_plugin.cc', + 'macos/Classes/Foo.h', + ]; + createFakePlugin('a_plugin', packagesDir, extraFiles: files); + + processRunner.mockProcessesForExecutable['clang-format'] = [ + MockProcess(exitCode: 1) + ]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['format'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Unable to run \'clang-format\'. Make sure that it is in your ' + 'path, or provide a full path with --clang-format.'), + ])); + }); + + test('honors --clang-format flag', () async { + const List files = [ + 'windows/foo_plugin.cpp', + ]; + final Directory pluginDir = createFakePlugin( + 'a_plugin', + packagesDir, + extraFiles: files, + ); + + await runCapturingPrint( + runner, ['format', '--clang-format=/path/to/clang-format']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + const ProcessCall( + '/path/to/clang-format', ['--version'], null), + ProcessCall( + '/path/to/clang-format', + [ + '-i', + '--style=Google', + ..._getPackagesDirRelativePaths(pluginDir, files) + ], + packagesDir.path), + ])); + }); + + test('fails if clang-format fails', () async { + const List files = [ + 'linux/foo_plugin.cc', + 'macos/Classes/Foo.h', + ]; + createFakePlugin('a_plugin', packagesDir, extraFiles: files); + + processRunner.mockProcessesForExecutable['clang-format'] = [ + MockProcess(), // check for working clang-format + MockProcess(exitCode: 1), // format + ]; + Error? commandError; + final List output = await runCapturingPrint( + runner, ['format'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Failed to format C, C++, and Objective-C files: exit code 1.'), + ])); + }); + + test('skips known non-repo files', () async { + const List skipFiles = [ + '/example/build/SomeFramework.framework/Headers/SomeFramework.h', + '/example/Pods/APod.framework/Headers/APod.h', + '.dart_tool/internals/foo.cc', + '.dart_tool/internals/Bar.java', + '.dart_tool/internals/baz.dart', + ]; + const List clangFiles = ['ios/Classes/Foo.h']; + const List dartFiles = ['lib/a.dart']; + const List javaFiles = [ + 'android/src/main/java/io/flutter/plugins/a_plugin/a.java' + ]; + final Directory pluginDir = createFakePlugin( + 'a_plugin', + packagesDir, + extraFiles: [ + ...skipFiles, + // Include some files that should be formatted to validate that it's + // correctly filtering even when running the commands. + ...clangFiles, + ...dartFiles, + ...javaFiles, + ], + ); + + await runCapturingPrint(runner, ['format']); + + expect( + processRunner.recordedCalls, + containsAll([ + ProcessCall( + 'clang-format', + [ + '-i', + '--style=Google', + ..._getPackagesDirRelativePaths(pluginDir, clangFiles) + ], + packagesDir.path), + ProcessCall( + getFlutterCommand(mockPlatform), + [ + 'format', + ..._getPackagesDirRelativePaths(pluginDir, dartFiles) + ], + packagesDir.path), + ProcessCall( + 'java', + [ + '-jar', + javaFormatPath, + '--replace', + ..._getPackagesDirRelativePaths(pluginDir, javaFiles) + ], + packagesDir.path), + ])); + }); + + test('fails if files are changed with --file-on-change', () async { + const List files = [ + 'linux/foo_plugin.cc', + 'macos/Classes/Foo.h', + ]; + createFakePlugin('a_plugin', packagesDir, extraFiles: files); + + const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; + processRunner.mockProcessesForExecutable['git'] = [ + MockProcess(stdout: changedFilePath), + ]; + + Error? commandError; + final List output = + await runCapturingPrint(runner, ['format', '--fail-on-change'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('These files are not formatted correctly'), + contains(changedFilePath), + contains('patch -p1 < files = [ + 'linux/foo_plugin.cc', + 'macos/Classes/Foo.h', + ]; + createFakePlugin('a_plugin', packagesDir, extraFiles: files); + + processRunner.mockProcessesForExecutable['git'] = [ + MockProcess(exitCode: 1) + ]; + Error? commandError; + final List output = + await runCapturingPrint(runner, ['format', '--fail-on-change'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to determine changed files.'), + ])); + }); + + test('reports git diff failures', () async { + const List files = [ + 'linux/foo_plugin.cc', + 'macos/Classes/Foo.h', + ]; + createFakePlugin('a_plugin', packagesDir, extraFiles: files); + + const String changedFilePath = 'packages/a_plugin/linux/foo_plugin.cc'; + processRunner.mockProcessesForExecutable['git'] = [ + MockProcess(stdout: changedFilePath), // ls-files + MockProcess(exitCode: 1), // diff + ]; + + Error? commandError; + final List output = + await runCapturingPrint(runner, ['format', '--fail-on-change'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('These files are not formatted correctly'), + contains(changedFilePath), + contains('Unable to determine diff.'), + ])); + }); + + test('Batches moderately long file lists on Windows', () async { + mockPlatform.isWindows = true; + + const String pluginName = 'a_plugin'; + // -1 since the command itself takes some length. + const int batchSize = (windowsCommandLineMax ~/ 100) - 1; + + // Make the file list one file longer than would fit in the batch. + final List batch1 = + _get99CharacterPathExtraFiles(pluginName, batchSize + 1); + final String extraFile = batch1.removeLast(); + + createFakePlugin( + pluginName, + packagesDir, + extraFiles: [...batch1, extraFile], + ); + + await runCapturingPrint(runner, ['format']); + + // Ensure that it was batched... + expect(processRunner.recordedCalls.length, 2); + // ... and that the spillover into the second batch was only one file. + expect( + processRunner.recordedCalls, + contains( + ProcessCall( + getFlutterCommand(mockPlatform), + [ + 'format', + '$pluginName\\$extraFile', + ], + packagesDir.path), + )); + }); + + // Validates that the Windows limit--which is much lower than the limit on + // other platforms--isn't being used on all platforms, as that would make + // formatting slower on Linux and macOS. + test('Does not batch moderately long file lists on non-Windows', () async { + const String pluginName = 'a_plugin'; + // -1 since the command itself takes some length. + const int batchSize = (windowsCommandLineMax ~/ 100) - 1; + + // Make the file list one file longer than would fit in a Windows batch. + final List batch = + _get99CharacterPathExtraFiles(pluginName, batchSize + 1); + + createFakePlugin( + pluginName, + packagesDir, + extraFiles: batch, + ); + + await runCapturingPrint(runner, ['format']); + + expect(processRunner.recordedCalls.length, 1); + }); + + test('Batches extremely long file lists on non-Windows', () async { + const String pluginName = 'a_plugin'; + // -1 since the command itself takes some length. + const int batchSize = (nonWindowsCommandLineMax ~/ 100) - 1; + + // Make the file list one file longer than would fit in the batch. + final List batch1 = + _get99CharacterPathExtraFiles(pluginName, batchSize + 1); + final String extraFile = batch1.removeLast(); + + createFakePlugin( + pluginName, + packagesDir, + extraFiles: [...batch1, extraFile], + ); + + await runCapturingPrint(runner, ['format']); + + // Ensure that it was batched... + expect(processRunner.recordedCalls.length, 2); + // ... and that the spillover into the second batch was only one file. + expect( + processRunner.recordedCalls, + contains( + ProcessCall( + getFlutterCommand(mockPlatform), + [ + 'format', + '$pluginName/$extraFile', + ], + packagesDir.path), + )); + }); +} diff --git a/script/tool/test/license_check_command_test.dart b/script/tool/test/license_check_command_test.dart new file mode 100644 index 000000000000..5a8a90e9a674 --- /dev/null +++ b/script/tool/test/license_check_command_test.dart @@ -0,0 +1,533 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/license_check_command.dart'; +import 'package:test/test.dart'; + +import 'util.dart'; + +void main() { + group('$LicenseCheckCommand', () { + late CommandRunner runner; + late FileSystem fileSystem; + late Directory root; + + setUp(() { + fileSystem = MemoryFileSystem(); + final Directory packagesDir = + fileSystem.currentDirectory.childDirectory('packages'); + root = packagesDir.parent; + + final LicenseCheckCommand command = LicenseCheckCommand( + packagesDir, + ); + 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, + 'kt': true, + 'm': true, + 'md': false, + 'mm': true, + 'png': false, + 'swift': true, + 'sh': true, + 'yaml': false, + }; + + const String filenameBase = 'a_file'; + for (final String fileExtension in extensions.keys) { + root.childFile('$filenameBase.$fileExtension').createSync(); + } + + final List output = await runCapturingPrint( + runner, ['license-check'], errorHandler: (Error e) { + // Ignore failure; the files are empty so the check is expected to fail, + // but this test isn't for that behavior. + }); + + extensions.forEach((String fileExtension, bool shouldCheck) { + final Matcher logLineMatcher = + contains('Checking $filenameBase.$fileExtension'); + expect(output, shouldCheck ? logLineMatcher : isNot(logLineMatcher)); + }); + }); + + test('ignore list overrides extension matches', () async { + final List ignoredFiles = [ + // Ignored base names. + 'flutter_export_environment.sh', + 'GeneratedPluginRegistrant.java', + 'GeneratedPluginRegistrant.m', + 'generated_plugin_registrant.cc', + 'generated_plugin_registrant.cpp', + // Ignored path suffixes. + 'foo.g.dart', + 'foo.mocks.dart', + // Ignored files. + 'resource.h', + ]; + + for (final String name in ignoredFiles) { + root.childFile(name).createSync(); + } + + final List output = + await runCapturingPrint(runner, ['license-check']); + + for (final String name in ignoredFiles) { + expect(output, isNot(contains('Checking $name'))); + } + }); + + test('passes if all checked files have license blocks', () async { + final File checked = root.childFile('checked.cc'); + checked.createSync(); + _writeLicense(checked); + final File notChecked = root.childFile('not_checked.md'); + notChecked.createSync(); + + final List output = + await runCapturingPrint(runner, ['license-check']); + + // Sanity check that the test did actually check a file. + expect( + output, + containsAllInOrder([ + contains('Checking checked.cc'), + contains('All files passed validation!'), + ])); + }); + + test('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: ''); + + final List output = + await runCapturingPrint(runner, ['license-check']); + + // Sanity check that the test did actually check the files. + expect( + output, + containsAllInOrder([ + contains('Checking file_a.cc'), + contains('Checking file_b.sh'), + contains('Checking file_c.html'), + contains('All files passed validation!'), + ])); + }); + + test('fails if any checked files are missing license blocks', () async { + final File goodA = root.childFile('good.cc'); + goodA.createSync(); + _writeLicense(goodA); + final File goodB = root.childFile('good.h'); + goodB.createSync(); + _writeLicense(goodB); + root.childFile('bad.cc').createSync(); + root.childFile('bad.h').createSync(); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['license-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + // Failure should give information about the problematic files. + expect( + output, + containsAllInOrder([ + contains( + 'The license block for these files is missing or incorrect:'), + contains(' bad.cc'), + contains(' bad.h'), + ])); + // Failure shouldn't print the success message. + expect(output, isNot(contains(contains('All files passed validation!')))); + }); + + test('fails if any checked files are missing just the copyright', () async { + final File good = root.childFile('good.cc'); + good.createSync(); + _writeLicense(good); + final File bad = root.childFile('bad.cc'); + bad.createSync(); + _writeLicense(bad, copyright: ''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['license-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + // Failure should give information about the problematic files. + expect( + output, + containsAllInOrder([ + contains( + 'The license block for these files is missing or incorrect:'), + contains(' bad.cc'), + ])); + // Failure shouldn't print the success message. + expect(output, isNot(contains(contains('All files passed validation!')))); + }); + + test('fails if any checked files are missing just the license', () async { + final File good = root.childFile('good.cc'); + good.createSync(); + _writeLicense(good); + final File bad = root.childFile('bad.cc'); + bad.createSync(); + _writeLicense(bad, license: []); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['license-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + // Failure should give information about the problematic files. + expect( + output, + containsAllInOrder([ + contains( + 'The license block for these files is missing or incorrect:'), + contains(' bad.cc'), + ])); + // Failure shouldn't print the success message. + expect(output, isNot(contains(contains('All files passed validation!')))); + }); + + test('fails if any third-party code is not in a third_party directory', + () async { + final File thirdPartyFile = root.childFile('third_party.cc'); + thirdPartyFile.createSync(); + _writeLicense(thirdPartyFile, copyright: 'Copyright 2017 Someone Else'); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['license-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + // Failure should give information about the problematic files. + expect( + output, + containsAllInOrder([ + contains( + 'The license block for these files is missing or incorrect:'), + contains(' third_party.cc'), + ])); + // Failure shouldn't print the success message. + expect(output, isNot(contains(contains('All files passed validation!')))); + }); + + test('succeeds for third-party code in a third_party directory', () async { + final File thirdPartyFile = root + .childDirectory('a_plugin') + .childDirectory('lib') + .childDirectory('src') + .childDirectory('third_party') + .childFile('file.cc'); + thirdPartyFile.createSync(recursive: true); + _writeLicense(thirdPartyFile, + copyright: 'Copyright 2017 Workiva Inc.', + license: [ + 'Licensed under the Apache License, Version 2.0 (the "License");', + 'you may not use this file except in compliance with the License.' + ]); + + final List output = + await runCapturingPrint(runner, ['license-check']); + + // Sanity check that the test did actually check the file. + expect( + output, + containsAllInOrder([ + contains('Checking a_plugin/lib/src/third_party/file.cc'), + contains('All files passed validation!'), + ])); + }); + + test('allows first-party code in a third_party directory', () async { + final File firstPartyFileInThirdParty = root + .childDirectory('a_plugin') + .childDirectory('lib') + .childDirectory('src') + .childDirectory('third_party') + .childFile('first_party.cc'); + firstPartyFileInThirdParty.createSync(recursive: true); + _writeLicense(firstPartyFileInThirdParty); + + final List output = + await runCapturingPrint(runner, ['license-check']); + + // Sanity check that the test did actually check the file. + expect( + output, + containsAllInOrder([ + contains('Checking a_plugin/lib/src/third_party/first_party.cc'), + contains('All files passed validation!'), + ])); + }); + + test('fails for licenses that the tool does not expect', () async { + final File good = root.childFile('good.cc'); + good.createSync(); + _writeLicense(good); + final File bad = root.childDirectory('third_party').childFile('bad.cc'); + bad.createSync(recursive: true); + _writeLicense(bad, license: [ + 'This program is free software: you can redistribute it and/or modify', + 'it under the terms of the GNU General Public License', + ]); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['license-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + // Failure should give information about the problematic files. + expect( + output, + containsAllInOrder([ + contains( + 'No recognized license was found for the following third-party files:'), + contains(' third_party/bad.cc'), + ])); + // Failure shouldn't print the success message. + expect(output, isNot(contains(contains('All files passed validation!')))); + }); + + test('Apache is not recognized for new authors without validation changes', + () async { + final File good = root.childFile('good.cc'); + good.createSync(); + _writeLicense(good); + final File bad = root.childDirectory('third_party').childFile('bad.cc'); + bad.createSync(recursive: true); + _writeLicense( + bad, + copyright: 'Copyright 2017 Some New Authors.', + license: [ + 'Licensed under the Apache License, Version 2.0 (the "License");', + 'you may not use this file except in compliance with the License.' + ], + ); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['license-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + // Failure should give information about the problematic files. + expect( + output, + containsAllInOrder([ + contains( + 'No recognized license was found for the following third-party files:'), + contains(' third_party/bad.cc'), + ])); + // Failure shouldn't print the success message. + expect(output, isNot(contains(contains('All files passed validation!')))); + }); + + test('passes if all first-party LICENSE files are correctly formatted', + () async { + final File license = root.childFile('LICENSE'); + license.createSync(); + license.writeAsStringSync(_correctLicenseFileText); + + final List output = + await runCapturingPrint(runner, ['license-check']); + + // Sanity check that the test did actually check the file. + expect( + output, + containsAllInOrder([ + contains('Checking LICENSE'), + contains('All files passed validation!'), + ])); + }); + + test('fails if any first-party LICENSE files are incorrectly formatted', + () async { + final File license = root.childFile('LICENSE'); + license.createSync(); + license.writeAsStringSync(_incorrectLicenseFileText); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['license-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect(output, isNot(contains(contains('All files passed validation!')))); + }); + + test('ignores third-party LICENSE format', () async { + final File license = + root.childDirectory('third_party').childFile('LICENSE'); + license.createSync(recursive: true); + license.writeAsStringSync(_incorrectLicenseFileText); + + final List output = + await runCapturingPrint(runner, ['license-check']); + + // The file shouldn't be checked. + expect(output, isNot(contains(contains('Checking third_party/LICENSE')))); + }); + + test('outputs all errors at the end', () async { + root.childFile('bad.cc').createSync(); + root + .childDirectory('third_party') + .childFile('bad.cc') + .createSync(recursive: true); + final File license = root.childFile('LICENSE'); + license.createSync(); + license.writeAsStringSync(_incorrectLicenseFileText); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['license-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Checking LICENSE'), + contains('Checking bad.cc'), + contains('Checking third_party/bad.cc'), + contains( + 'The following LICENSE files do not follow the expected format:'), + contains(' LICENSE'), + contains( + 'The license block for these files is missing or incorrect:'), + contains(' bad.cc'), + contains( + 'No recognized license was found for the following third-party files:'), + contains(' third_party/bad.cc'), + ])); + }); + }); +} + +const String _correctLicenseFileText = ''' +Copyright 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of Google Inc. nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +'''; + +// A common incorrect version created by copying text intended for a code file, +// with comment markers. +const String _incorrectLicenseFileText = ''' +// Copyright 2013 The Flutter Authors. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google Inc. nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +'''; diff --git a/script/tool/test/lint_android_command_test.dart b/script/tool/test/lint_android_command_test.dart new file mode 100644 index 000000000000..5670a64f30d8 --- /dev/null +++ b/script/tool/test/lint_android_command_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:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:flutter_plugin_tools/src/lint_android_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('$LintAndroidCommand', () { + FileSystem fileSystem; + late Directory packagesDir; + late CommandRunner runner; + late MockPlatform mockPlatform; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(style: FileSystemStyle.posix); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + mockPlatform = MockPlatform(); + processRunner = RecordingProcessRunner(); + final LintAndroidCommand command = LintAndroidCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = CommandRunner( + 'lint_android_test', 'Test for $LintAndroidCommand'); + runner.addCommand(command); + }); + + test('runs gradle lint', () async { + final Directory pluginDir = + createFakePlugin('plugin1', packagesDir, extraFiles: [ + 'example/android/gradlew', + ], platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + }); + + final Directory androidDir = + pluginDir.childDirectory('example').childDirectory('android'); + + final List output = + await runCapturingPrint(runner, ['lint-android']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidDir.childFile('gradlew').path, + const ['plugin1:lintDebug'], + androidDir.path, + ), + ]), + ); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin1'), + contains('No issues found!'), + ])); + }); + + test('fails if gradlew is missing', () async { + createFakePlugin('plugin1', packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + }); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['lint-android'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder( + [ + contains('Build example before linting'), + ], + )); + }); + + test('fails if linting finds issues', () async { + createFakePlugin('plugin1', packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + }); + + processRunner.mockProcessesForExecutable['gradlew'] = [ + MockProcess(exitCode: 1), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['lint-android'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder( + [ + contains('Build example before linting'), + ], + )); + }); + + test('skips non-Android plugins', () async { + createFakePlugin('plugin1', packagesDir); + + final List output = + await runCapturingPrint(runner, ['lint-android']); + + expect( + output, + containsAllInOrder( + [ + contains( + 'SKIPPING: Plugin does not have an Android implemenatation.') + ], + )); + }); + + test('skips non-inline plugins', () async { + createFakePlugin('plugin1', packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.federated) + }); + + final List output = + await runCapturingPrint(runner, ['lint-android']); + + expect( + output, + containsAllInOrder( + [ + contains( + 'SKIPPING: Plugin does not have an Android implemenatation.') + ], + )); + }); + }); +} 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..44247274028f --- /dev/null +++ b/script/tool/test/lint_podspecs_command_test.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:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/lint_podspecs_command.dart'; +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; + + setUp(() { + fileSystem = MemoryFileSystem(style: FileSystemStyle.posix); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + + mockPlatform = MockPlatform(isMacOS: true); + processRunner = RecordingProcessRunner(); + final LintPodspecsCommand command = LintPodspecsCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = + CommandRunner('podspec_test', 'Test for $LintPodspecsCommand'); + runner.addCommand(command); + }); + + test('only runs on macOS', () async { + createFakePlugin('plugin1', packagesDir, + extraFiles: ['plugin1.podspec']); + mockPlatform.isMacOS = false; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['podspecs'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + processRunner.recordedCalls, + equals([]), + ); + + expect( + output, + containsAllInOrder( + [contains('only supported on macOS')], + )); + }); + + test('runs pod lib lint on a podspec', () async { + final Directory plugin1Dir = createFakePlugin( + 'plugin1', + packagesDir, + extraFiles: [ + 'ios/plugin1.podspec', + 'bogus.dart', // Ignore non-podspecs. + ], + ); + + processRunner.mockProcessesForExecutable['pod'] = [ + MockProcess(stdout: 'Foo', stderr: 'Bar'), + MockProcess(), + ]; + + final List output = + await runCapturingPrint(runner, ['podspecs']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', const ['pod'], packagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + plugin1Dir + .childDirectory('ios') + .childFile('plugin1.podspec') + .path, + '--configuration=Debug', + '--skip-tests', + '--use-modular-headers', + '--use-libraries' + ], + packagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + plugin1Dir + .childDirectory('ios') + .childFile('plugin1.podspec') + .path, + '--configuration=Debug', + '--skip-tests', + '--use-modular-headers', + ], + packagesDir.path), + ]), + ); + + expect(output, contains('Linting plugin1.podspec')); + expect(output, contains('Foo')); + expect(output, contains('Bar')); + }); + + test('allow warnings for podspecs with known warnings', () async { + final Directory plugin1Dir = createFakePlugin('plugin1', packagesDir, + extraFiles: ['plugin1.podspec']); + + final List output = await runCapturingPrint( + runner, ['podspecs', '--ignore-warnings=plugin1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall('which', const ['pod'], packagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + plugin1Dir.childFile('plugin1.podspec').path, + '--configuration=Debug', + '--skip-tests', + '--use-modular-headers', + '--allow-warnings', + '--use-libraries' + ], + packagesDir.path), + ProcessCall( + 'pod', + [ + 'lib', + 'lint', + plugin1Dir.childFile('plugin1.podspec').path, + '--configuration=Debug', + '--skip-tests', + '--use-modular-headers', + '--allow-warnings', + ], + packagesDir.path), + ]), + ); + + expect(output, contains('Linting plugin1.podspec')); + }); + + test('fails if pod is missing', () async { + createFakePlugin('plugin1', packagesDir, + extraFiles: ['plugin1.podspec']); + + // Simulate failure from `which pod`. + processRunner.mockProcessesForExecutable['which'] = [ + MockProcess(exitCode: 1), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['podspecs'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder( + [ + contains('Unable to find "pod". Make sure it is in your path.'), + ], + )); + }); + + test('fails if linting as a framework fails', () async { + createFakePlugin('plugin1', packagesDir, + extraFiles: ['plugin1.podspec']); + + // Simulate failure from `pod`. + processRunner.mockProcessesForExecutable['pod'] = [ + MockProcess(exitCode: 1), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['podspecs'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder( + [ + contains('The following packages had errors:'), + contains('plugin1:\n' + ' plugin1.podspec') + ], + )); + }); + + test('fails if linting as a static library fails', () async { + createFakePlugin('plugin1', packagesDir, + extraFiles: ['plugin1.podspec']); + + // Simulate failure from the second call to `pod`. + processRunner.mockProcessesForExecutable['pod'] = [ + MockProcess(), + MockProcess(exitCode: 1), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['podspecs'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder( + [ + contains('The following packages had errors:'), + contains('plugin1:\n' + ' plugin1.podspec') + ], + )); + }); + + test('skips when there are no podspecs', () async { + createFakePlugin('plugin1', packagesDir); + + final List output = + await runCapturingPrint(runner, ['podspecs']); + + expect( + output, + containsAllInOrder( + [contains('SKIPPING: No podspecs.')], + )); + }); + }); +} diff --git a/script/tool/test/list_command_test.dart b/script/tool/test/list_command_test.dart new file mode 100644 index 000000000000..fcdf9fafdb63 --- /dev/null +++ b/script/tool/test/list_command_test.dart @@ -0,0 +1,206 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/list_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('$ListCommand', () { + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late CommandRunner runner; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + final ListCommand command = + ListCommand(packagesDir, platform: mockPlatform); + + runner = CommandRunner('list_test', 'Test for $ListCommand'); + runner.addCommand(command); + }); + + test('lists 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); + createFakePlugin('plugin2', packagesDir, + examples: ['example1', 'example2']); + createFakePlugin('plugin3', packagesDir, examples: []); + + final List examples = + await runCapturingPrint(runner, ['list', '--type=example']); + + expect( + examples, + orderedEquals([ + '/packages/plugin1/example', + '/packages/plugin2/example/example1', + '/packages/plugin2/example/example2', + ]), + ); + }); + + test('lists packages', () async { + createFakePlugin('plugin1', packagesDir); + createFakePlugin('plugin2', packagesDir, + examples: ['example1', 'example2']); + createFakePlugin('plugin3', packagesDir, examples: []); + + 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); + createFakePlugin('plugin2', packagesDir, + examples: ['example1', 'example2']); + createFakePlugin('plugin3', packagesDir, examples: []); + + final List examples = + await runCapturingPrint(runner, ['list', '--type=file']); + + expect( + examples, + unorderedEquals([ + '/packages/plugin1/pubspec.yaml', + '/packages/plugin1/AUTHORS', + '/packages/plugin1/CHANGELOG.md', + '/packages/plugin1/example/pubspec.yaml', + '/packages/plugin2/pubspec.yaml', + '/packages/plugin2/AUTHORS', + '/packages/plugin2/CHANGELOG.md', + '/packages/plugin2/example/example1/pubspec.yaml', + '/packages/plugin2/example/example2/pubspec.yaml', + '/packages/plugin3/pubspec.yaml', + '/packages/plugin3/AUTHORS', + '/packages/plugin3/CHANGELOG.md', + ]), + ); + }); + + test('lists plugins using federated plugin layout', () async { + createFakePlugin('plugin1', packagesDir); + + // Create a federated plugin by creating a directory under the packages + // directory with several packages underneath. + final Directory 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 --packages 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', '--packages=plugin1']); + expect( + plugins, + unorderedEquals([ + '/packages/plugin1', + ]), + ); + + plugins = await runCapturingPrint( + runner, ['list', '--packages=my_plugin']); + expect( + plugins, + unorderedEquals([ + '/packages/my_plugin/my_plugin', + '/packages/my_plugin/my_plugin_web', + '/packages/my_plugin/my_plugin_macos', + ]), + ); + + plugins = await runCapturingPrint( + runner, ['list', '--packages=my_plugin/my_plugin_web']); + expect( + plugins, + unorderedEquals([ + '/packages/my_plugin/my_plugin_web', + ]), + ); + + plugins = await runCapturingPrint(runner, + ['list', '--packages=my_plugin/my_plugin_web,plugin1']); + expect( + plugins, + unorderedEquals([ + '/packages/plugin1', + '/packages/my_plugin/my_plugin_web', + ]), + ); + }); + }); +} diff --git a/script/tool/test/mocks.dart b/script/tool/test/mocks.dart new file mode 100644 index 000000000000..3d0aef1b3971 --- /dev/null +++ b/script/tool/test/mocks.dart @@ -0,0 +1,86 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:file/file.dart'; +import 'package:mockito/mockito.dart'; +import 'package:platform/platform.dart'; + +class MockPlatform extends Mock implements Platform { + MockPlatform({ + this.isLinux = false, + this.isMacOS = false, + this.isWindows = false, + }); + + @override + bool isLinux; + + @override + bool isMacOS; + + @override + bool isWindows; + + @override + Uri get script => isWindows + ? Uri.file(r'C:\foo\bar', windows: true) + : Uri.file('/foo/bar', windows: false); +} + +class MockProcess extends Mock implements io.Process { + /// Creates a mock process with the given results. + /// + /// The default encodings match the ProcessRunner defaults; mocks for + /// processes run with a different encoding will need to be created with + /// the matching encoding. + MockProcess({ + int exitCode = 0, + String? stdout, + String? stderr, + Encoding stdoutEncoding = io.systemEncoding, + Encoding stderrEncoding = io.systemEncoding, + }) : _exitCode = exitCode { + if (stdout != null) { + _stdoutController.add(stdoutEncoding.encoder.convert(stdout)); + } + if (stderr != null) { + _stderrController.add(stderrEncoding.encoder.convert(stderr)); + } + _stdoutController.close(); + _stderrController.close(); + } + + final int _exitCode; + final StreamController> _stdoutController = + StreamController>(); + final StreamController> _stderrController = + StreamController>(); + final MockIOSink stdinMock = MockIOSink(); + + @override + int get pid => 99; + + @override + Future get exitCode async => _exitCode; + + @override + Stream> get stdout => _stdoutController.stream; + + @override + Stream> get stderr => _stderrController.stream; + + @override + IOSink get stdin => stdinMock; +} + +class MockIOSink extends Mock implements IOSink { + List lines = []; + + @override + void writeln([Object? obj = '']) => lines.add(obj.toString()); +} diff --git a/script/tool/test/native_test_command_test.dart b/script/tool/test/native_test_command_test.dart new file mode 100644 index 000000000000..ba93efcb3ace --- /dev/null +++ b/script/tool/test/native_test_command_test.dart @@ -0,0 +1,1668 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/file_utils.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:flutter_plugin_tools/src/native_test_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +const String _androidIntegrationTestFilter = + '-Pandroid.testInstrumentationRunnerArguments.' + 'notAnnotation=io.flutter.plugins.DartIntegrationTest'; + +final Map _kDeviceListMap = { + 'runtimes': >[ + { + 'bundlePath': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime', + 'buildversion': '17L255', + 'runtimeRoot': + '/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 13.4.simruntime/Contents/Resources/RuntimeRoot', + 'identifier': 'com.apple.CoreSimulator.SimRuntime.iOS-13-4', + 'version': '13.4', + 'isAvailable': true, + 'name': 'iOS 13.4' + }, + ], + 'devices': { + 'com.apple.CoreSimulator.SimRuntime.iOS-13-4': >[ + { + 'dataPath': + '/Users/xxx/Library/Developer/CoreSimulator/Devices/1E76A0FD-38AC-4537-A989-EA639D7D012A/data', + 'logPath': + '/Users/xxx/Library/Logs/CoreSimulator/1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'udid': '1E76A0FD-38AC-4537-A989-EA639D7D012A', + 'isAvailable': true, + 'deviceTypeIdentifier': + 'com.apple.CoreSimulator.SimDeviceType.iPhone-8-Plus', + 'state': 'Shutdown', + 'name': 'iPhone 8 Plus' + } + ] + } +}; + +// TODO(stuartmorgan): Rework these tests to use a mock Xcode instead of +// doing all the process mocking and validation. +void main() { + const String _kDestination = '--ios-destination'; + + group('test native_test_command on Posix', () { + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(isMacOS: true); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final NativeTestCommand command = NativeTestCommand(packagesDir, + processRunner: processRunner, platform: mockPlatform); + + runner = CommandRunner( + 'native_test_command', 'Test for native_test_command'); + runner.addCommand(command); + }); + + // Returns a MockProcess to provide for "xcrun xcodebuild -list" for a + // project that contains [targets]. + MockProcess _getMockXcodebuildListProcess(List targets) { + final Map projects = { + 'project': { + 'targets': targets, + } + }; + return MockProcess(stdout: jsonEncode(projects)); + } + + // Returns the ProcessCall to expect for checking the targets present in + // the [package]'s [platform]/Runner.xcodeproj. + ProcessCall _getTargetCheckCall(Directory package, String platform) { + return ProcessCall( + 'xcrun', + [ + 'xcodebuild', + '-list', + '-json', + '-project', + package + .childDirectory(platform) + .childDirectory('Runner.xcodeproj') + .path, + ], + null); + } + + // Returns the ProcessCall to expect for running the tests in the + // workspace [platform]/Runner.xcworkspace, with the given extra flags. + ProcessCall _getRunTestCall( + Directory package, + String platform, { + String? destination, + List extraFlags = const [], + }) { + return ProcessCall( + 'xcrun', + [ + 'xcodebuild', + 'test', + '-workspace', + '$platform/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + if (destination != null) ...['-destination', destination], + ...extraFlags, + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + package.path); + } + + test('fails if no platforms are provided', () async { + Error? commandError; + final List output = await runCapturingPrint( + runner, ['native-test'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('At least one platform flag must be provided.'), + ]), + ); + }); + + test('fails if all test types are disabled', () async { + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--macos', + '--no-unit', + '--no-integration', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('At least one test type must be enabled.'), + ]), + ); + }); + + test('reports skips with no tests', () async { + final Directory pluginDirectory1 = createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess(['RunnerTests', 'RunnerUITests']), + // Exit code 66 from testing indicates no tests. + MockProcess(exitCode: 66), + ]; + final List output = await runCapturingPrint( + runner, ['native-test', '--macos', '--no-unit']); + + expect( + output, + containsAllInOrder([ + contains('No tests found.'), + contains('Skipped 1 package(s)'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + _getTargetCheckCall(pluginExampleDirectory, 'macos'), + _getRunTestCall(pluginExampleDirectory, 'macos', + extraFlags: ['-only-testing:RunnerUITests']), + ])); + }); + + group('iOS', () { + test('skip if iOS is not supported', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }); + + final List output = await runCapturingPrint(runner, + ['native-test', '--ios', _kDestination, 'foo_destination']); + expect( + output, + containsAllInOrder([ + contains('No implementation for iOS.'), + contains('SKIPPING: Nothing to test for target platform(s).'), + ])); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skip if iOS is implemented in a federated package', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.federated) + }); + + final List output = await runCapturingPrint(runner, + ['native-test', '--ios', _kDestination, 'foo_destination']); + expect( + output, + containsAllInOrder([ + contains('No implementation for iOS.'), + contains('SKIPPING: Nothing to test for target platform(s).'), + ])); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('running with correct destination', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), + ]; + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--ios', + _kDestination, + 'foo_destination', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('Successfully ran iOS xctest for plugin/example') + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + _getTargetCheckCall(pluginExampleDirectory, 'ios'), + _getRunTestCall(pluginExampleDirectory, 'ios', + destination: 'foo_destination'), + ])); + }); + + test('Not specifying --ios-destination assigns an available simulator', + () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) + }); + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(stdout: jsonEncode(_kDeviceListMap)), // simctl + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), + ]; + + await runCapturingPrint(runner, ['native-test', '--ios']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + const ProcessCall( + 'xcrun', + [ + 'simctl', + 'list', + 'devices', + 'runtimes', + 'available', + '--json', + ], + null), + _getTargetCheckCall(pluginExampleDirectory, 'ios'), + _getRunTestCall(pluginExampleDirectory, 'ios', + destination: 'id=1E76A0FD-38AC-4537-A989-EA639D7D012A'), + ])); + }); + }); + + group('macOS', () { + test('skip if macOS is not supported', () async { + createFakePlugin('plugin', packagesDir); + + final List output = + await runCapturingPrint(runner, ['native-test', '--macos']); + + expect( + output, + containsAllInOrder([ + contains('No implementation for macOS.'), + contains('SKIPPING: Nothing to test for target platform(s).'), + ])); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skip if macOS is implemented in a federated package', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.federated), + }); + + final List output = + await runCapturingPrint(runner, ['native-test', '--macos']); + + expect( + output, + containsAllInOrder([ + contains('No implementation for macOS.'), + contains('SKIPPING: Nothing to test for target platform(s).'), + ])); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('runs for macOS plugin', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), + ]; + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--macos', + ]); + + expect( + output, + contains( + contains('Successfully ran macOS xctest for plugin/example'))); + + expect( + processRunner.recordedCalls, + orderedEquals([ + _getTargetCheckCall(pluginExampleDirectory, 'macos'), + _getRunTestCall(pluginExampleDirectory, 'macos'), + ])); + }); + }); + + group('Android', () { + test('runs Java unit tests in Android implementation folder', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + }, + extraFiles: [ + 'example/android/gradlew', + 'android/src/test/example_test.java', + ], + ); + + await runCapturingPrint(runner, ['native-test', '--android']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const ['testDebugUnitTest'], + androidFolder.path, + ), + ]), + ); + }); + + test('runs Java unit tests in example folder', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/test/example_test.java', + ], + ); + + await runCapturingPrint(runner, ['native-test', '--android']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const ['testDebugUnitTest'], + androidFolder.path, + ), + ]), + ); + }); + + test('runs Java integration tests', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + await runCapturingPrint( + runner, ['native-test', '--android', '--no-unit']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const [ + 'app:connectedAndroidTest', + _androidIntegrationTestFilter, + ], + androidFolder.path, + ), + ]), + ); + }); + + test( + 'ignores Java integration test files associated with integration_test', + () async { + createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/androidTest/java/io/flutter/plugins/DartIntegrationTest.java', + 'example/android/app/src/androidTest/java/io/flutter/plugins/plugin/FlutterActivityTest.java', + 'example/android/app/src/androidTest/java/io/flutter/plugins/plugin/MainActivityTest.java', + ], + ); + + await runCapturingPrint( + runner, ['native-test', '--android', '--no-unit']); + + // Nothing should run since those files are all + // integration_test-specific. + expect( + processRunner.recordedCalls, + orderedEquals([]), + ); + }); + + test('runs all tests when present', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + }, + extraFiles: [ + 'android/src/test/example_test.java', + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + await runCapturingPrint(runner, ['native-test', '--android']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const ['testDebugUnitTest'], + androidFolder.path, + ), + ProcessCall( + androidFolder.childFile('gradlew').path, + const [ + 'app:connectedAndroidTest', + _androidIntegrationTestFilter, + ], + androidFolder.path, + ), + ]), + ); + }); + + test('honors --no-unit', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + }, + extraFiles: [ + 'android/src/test/example_test.java', + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + await runCapturingPrint( + runner, ['native-test', '--android', '--no-unit']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const [ + 'app:connectedAndroidTest', + _androidIntegrationTestFilter, + ], + androidFolder.path, + ), + ]), + ); + }); + + test('honors --no-integration', () async { + final Directory plugin = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + }, + extraFiles: [ + 'android/src/test/example_test.java', + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + await runCapturingPrint( + runner, ['native-test', '--android', '--no-integration']); + + final Directory androidFolder = + plugin.childDirectory('example').childDirectory('android'); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + androidFolder.childFile('gradlew').path, + const ['testDebugUnitTest'], + androidFolder.path, + ), + ]), + ); + }); + + test('fails when the app needs to be built', () async { + createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + }, + extraFiles: [ + 'example/android/app/src/test/example_test.java', + ], + ); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['native-test', '--android'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder([ + contains('ERROR: Run "flutter build apk" on plugin/example'), + contains('plugin:\n' + ' Examples must be built before testing.') + ]), + ); + }); + + test('logs missing test types', () async { + // No unit tests. + createFakePlugin( + 'plugin1', + packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + // No integration tests. + createFakePlugin( + 'plugin2', + packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + }, + extraFiles: [ + 'android/src/test/example_test.java', + 'example/android/gradlew', + ], + ); + + final List output = await runCapturingPrint( + runner, ['native-test', '--android'], + errorHandler: (Error e) { + // Having no unit tests is fatal, but that's not the point of this + // test so just ignore the failure. + }); + + expect( + output, + containsAllInOrder([ + contains('No Android unit tests found for plugin1/example'), + contains('Running integration tests...'), + contains( + 'No Android integration tests found for plugin2/example'), + contains('Running unit tests...'), + ])); + }); + + test('fails when a unit test fails', () async { + final Directory pluginDir = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/test/example_test.java', + ], + ); + + final String gradlewPath = pluginDir + .childDirectory('example') + .childDirectory('android') + .childFile('gradlew') + .path; + processRunner.mockProcessesForExecutable[gradlewPath] = [ + MockProcess(exitCode: 1) + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['native-test', '--android'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder([ + contains('plugin/example unit tests failed.'), + contains('The following packages had errors:'), + contains('plugin') + ]), + ); + }); + + test('fails when an integration test fails', () async { + final Directory pluginDir = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/test/example_test.java', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + final String gradlewPath = pluginDir + .childDirectory('example') + .childDirectory('android') + .childFile('gradlew') + .path; + processRunner.mockProcessesForExecutable[gradlewPath] = [ + MockProcess(), // unit passes + MockProcess(exitCode: 1), // integration fails + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['native-test', '--android'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder([ + contains('plugin/example integration tests failed.'), + contains('The following packages had errors:'), + contains('plugin') + ]), + ); + }); + + test('fails if there are no unit tests', () async { + createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/androidTest/IntegrationTest.java', + ], + ); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['native-test', '--android'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder([ + contains('No Android unit tests found for plugin/example'), + contains( + 'No unit tests ran. Plugins are required to have unit tests.'), + contains('The following packages had errors:'), + contains('plugin:\n' + ' No unit tests ran (use --exclude if this is intentional).') + ]), + ); + }); + + test('skips if Android is not supported', () async { + createFakePlugin( + 'plugin', + packagesDir, + ); + + final List output = await runCapturingPrint( + runner, ['native-test', '--android']); + + expect( + output, + containsAllInOrder([ + contains('No implementation for Android.'), + contains('SKIPPING: Nothing to test for target platform(s).'), + ]), + ); + }); + + test('skips when running no tests in integration-only mode', () async { + createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline) + }, + ); + + final List output = await runCapturingPrint( + runner, ['native-test', '--android', '--no-unit']); + + expect( + output, + containsAllInOrder([ + contains('No Android integration tests found for plugin/example'), + contains('SKIPPING: No tests found.'), + ]), + ); + }); + }); + + group('Linux', () { + test('runs unit tests', () async { + const String testBinaryRelativePath = + 'build/linux/foo/release/bar/plugin_test'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/$testBinaryRelativePath' + ], platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + }); + + final File testBinary = childFileWithSubcomponents(pluginDirectory, + ['example', ...testBinaryRelativePath.split('/')]); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--linux', + '--no-integration', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running plugin_test...'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(testBinary.path, const [], null), + ])); + }); + + test('only runs release unit tests', () async { + const String debugTestBinaryRelativePath = + 'build/linux/foo/debug/bar/plugin_test'; + const String releaseTestBinaryRelativePath = + 'build/linux/foo/release/bar/plugin_test'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/$debugTestBinaryRelativePath', + 'example/$releaseTestBinaryRelativePath' + ], platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + }); + + final File releaseTestBinary = childFileWithSubcomponents( + pluginDirectory, + ['example', ...releaseTestBinaryRelativePath.split('/')]); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--linux', + '--no-integration', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running plugin_test...'), + contains('No issues found!'), + ]), + ); + + // Only the release version should be run. + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(releaseTestBinary.path, const [], null), + ])); + }); + + test('fails if there are no unit tests', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + }); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--linux', + '--no-integration', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No test binaries found.'), + ]), + ); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('fails if a unit test fails', () async { + const String testBinaryRelativePath = + 'build/linux/foo/release/bar/plugin_test'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/$testBinaryRelativePath' + ], platformSupport: { + kPlatformLinux: const PlatformDetails(PlatformSupport.inline), + }); + + final File testBinary = childFileWithSubcomponents(pluginDirectory, + ['example', ...testBinaryRelativePath.split('/')]); + + processRunner.mockProcessesForExecutable[testBinary.path] = + [MockProcess(exitCode: 1)]; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--linux', + '--no-integration', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running plugin_test...'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(testBinary.path, const [], null), + ])); + }); + }); + + // Tests behaviors of implementation that is shared between iOS and macOS. + group('iOS/macOS', () { + test('fails if xcrun fails', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(exitCode: 1) + ]; + + Error? commandError; + final List output = + await runCapturingPrint(runner, ['native-test', '--macos'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains(' plugin'), + ]), + ); + }); + + test('honors unit-only', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), + ]; + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--macos', + '--no-integration', + ]); + + expect( + output, + contains( + contains('Successfully ran macOS xctest for plugin/example'))); + + // --no-integration should translate to '-only-testing:RunnerTests'. + expect( + processRunner.recordedCalls, + orderedEquals([ + _getTargetCheckCall(pluginExampleDirectory, 'macos'), + _getRunTestCall(pluginExampleDirectory, 'macos', + extraFlags: ['-only-testing:RunnerTests']), + ])); + }); + + test('honors integration-only', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), + ]; + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--macos', + '--no-unit', + ]); + + expect( + output, + contains( + contains('Successfully ran macOS xctest for plugin/example'))); + + // --no-unit should translate to '-only-testing:RunnerUITests'. + expect( + processRunner.recordedCalls, + orderedEquals([ + _getTargetCheckCall(pluginExampleDirectory, 'macos'), + _getRunTestCall(pluginExampleDirectory, 'macos', + extraFlags: ['-only-testing:RunnerUITests']), + ])); + }); + + test('skips when the requested target is not present', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + // Simulate a project with unit tests but no integration tests... + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess(['RunnerTests']), + ]; + + // ... then try to run only integration tests. + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--macos', + '--no-unit', + ]); + + expect( + output, + containsAllInOrder([ + contains( + 'No "RunnerUITests" target in plugin/example; skipping.'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + _getTargetCheckCall(pluginExampleDirectory, 'macos'), + ])); + }); + + test('fails if there are no unit tests', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess(['RunnerUITests']), + ]; + + Error? commandError; + final List output = + await runCapturingPrint(runner, ['native-test', '--macos'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No "RunnerTests" target in plugin/example; skipping.'), + contains( + 'No unit tests ran. Plugins are required to have unit tests.'), + contains('The following packages had errors:'), + contains('plugin:\n' + ' No unit tests ran (use --exclude if this is intentional).'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + _getTargetCheckCall(pluginExampleDirectory, 'macos'), + ])); + }); + + test('fails if unable to check for requested target', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(exitCode: 1), // xcodebuild -list + ]; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--macos', + '--no-integration', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to check targets for plugin/example.'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + _getTargetCheckCall(pluginExampleDirectory, 'macos'), + ])); + }); + }); + + group('multiplatform', () { + test('runs all platfroms when supported', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: [ + 'example/android/gradlew', + 'android/src/test/example_test.java', + ], + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }, + ); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + final Directory androidFolder = + pluginExampleDirectory.childDirectory('android'); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), // iOS list + MockProcess(), // iOS run + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), // macOS list + MockProcess(), // macOS run + ]; + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--android', + '--ios', + '--macos', + _kDestination, + 'foo_destination', + ]); + + expect( + output, + containsAll([ + contains('Running Android tests for plugin/example'), + contains('Successfully ran iOS xctest for plugin/example'), + contains('Successfully ran macOS xctest for plugin/example'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(androidFolder.childFile('gradlew').path, + const ['testDebugUnitTest'], androidFolder.path), + _getTargetCheckCall(pluginExampleDirectory, 'ios'), + _getRunTestCall(pluginExampleDirectory, 'ios', + destination: 'foo_destination'), + _getTargetCheckCall(pluginExampleDirectory, 'macos'), + _getRunTestCall(pluginExampleDirectory, 'macos'), + ])); + }); + + test('runs only macOS for a macOS plugin', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), + ]; + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--ios', + '--macos', + _kDestination, + 'foo_destination', + ]); + + expect( + output, + containsAllInOrder([ + contains('No implementation for iOS.'), + contains('Successfully ran macOS xctest for plugin/example'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + _getTargetCheckCall(pluginExampleDirectory, 'macos'), + _getRunTestCall(pluginExampleDirectory, 'macos'), + ])); + }); + + test('runs only iOS for a iOS plugin', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), + ]; + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--ios', + '--macos', + _kDestination, + 'foo_destination', + ]); + + expect( + output, + containsAllInOrder([ + contains('No implementation for macOS.'), + contains('Successfully ran iOS xctest for plugin/example') + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + _getTargetCheckCall(pluginExampleDirectory, 'ios'), + _getRunTestCall(pluginExampleDirectory, 'ios', + destination: 'foo_destination'), + ])); + }); + + test('skips when nothing is supported', () async { + createFakePlugin('plugin', packagesDir); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--android', + '--ios', + '--macos', + '--windows', + _kDestination, + 'foo_destination', + ]); + + expect( + output, + containsAllInOrder([ + contains('No implementation for Android.'), + contains('No implementation for iOS.'), + contains('No implementation for macOS.'), + contains('SKIPPING: Nothing to test for target platform(s).'), + ])); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skips Dart-only plugins', () async { + createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline, + hasDartCode: true, hasNativeCode: false), + kPlatformWindows: const PlatformDetails(PlatformSupport.inline, + hasDartCode: true, hasNativeCode: false), + }, + ); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--macos', + '--windows', + _kDestination, + 'foo_destination', + ]); + + expect( + output, + containsAllInOrder([ + contains('No native code for macOS.'), + contains('No native code for Windows.'), + contains('SKIPPING: Nothing to test for target platform(s).'), + ])); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('failing one platform does not stop the tests', () async { + final Directory pluginDir = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/test/example_test.java', + ], + ); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + _getMockXcodebuildListProcess( + ['RunnerTests', 'RunnerUITests']), + ]; + + // Simulate failing Android, but not iOS. + final String gradlewPath = pluginDir + .childDirectory('example') + .childDirectory('android') + .childFile('gradlew') + .path; + processRunner.mockProcessesForExecutable[gradlewPath] = [ + MockProcess(exitCode: 1) + ]; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--android', + '--ios', + '--ios-destination', + 'foo_destination', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder([ + contains('Running tests for Android...'), + contains('plugin/example unit tests failed.'), + contains('Running tests for iOS...'), + contains('Successfully ran iOS xctest for plugin/example'), + contains('The following packages had errors:'), + contains('plugin:\n' + ' Android') + ]), + ); + }); + + test('failing multiple platforms reports multiple failures', () async { + final Directory pluginDir = createFakePlugin( + 'plugin', + packagesDir, + platformSupport: { + kPlatformAndroid: const PlatformDetails(PlatformSupport.inline), + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + }, + extraFiles: [ + 'example/android/gradlew', + 'example/android/app/src/test/example_test.java', + ], + ); + + // Simulate failing Android. + final String gradlewPath = pluginDir + .childDirectory('example') + .childDirectory('android') + .childFile('gradlew') + .path; + processRunner.mockProcessesForExecutable[gradlewPath] = [ + MockProcess(exitCode: 1) + ]; + // Simulate failing Android. + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(exitCode: 1) + ]; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--android', + '--ios', + '--ios-destination', + 'foo_destination', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + + expect( + output, + containsAllInOrder([ + contains('Running tests for Android...'), + contains('Running tests for iOS...'), + contains('The following packages had errors:'), + contains('plugin:\n' + ' Android\n' + ' iOS') + ]), + ); + }); + }); + }); + + group('test native_test_command on Windows', () { + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(style: FileSystemStyle.windows); + mockPlatform = MockPlatform(isWindows: true); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final NativeTestCommand command = NativeTestCommand(packagesDir, + processRunner: processRunner, platform: mockPlatform); + + runner = CommandRunner( + 'native_test_command', 'Test for native_test_command'); + runner.addCommand(command); + }); + + group('Windows', () { + test('runs unit tests', () async { + const String testBinaryRelativePath = + 'build/windows/foo/Release/bar/plugin_test.exe'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/$testBinaryRelativePath' + ], platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + }); + + final File testBinary = childFileWithSubcomponents(pluginDirectory, + ['example', ...testBinaryRelativePath.split('/')]); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--windows', + '--no-integration', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running plugin_test.exe...'), + contains('No issues found!'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(testBinary.path, const [], null), + ])); + }); + + test('only runs release unit tests', () async { + const String debugTestBinaryRelativePath = + 'build/windows/foo/Debug/bar/plugin_test.exe'; + const String releaseTestBinaryRelativePath = + 'build/windows/foo/Release/bar/plugin_test.exe'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/$debugTestBinaryRelativePath', + 'example/$releaseTestBinaryRelativePath' + ], platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + }); + + final File releaseTestBinary = childFileWithSubcomponents( + pluginDirectory, + ['example', ...releaseTestBinaryRelativePath.split('/')]); + + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--windows', + '--no-integration', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running plugin_test.exe...'), + contains('No issues found!'), + ]), + ); + + // Only the release version should be run. + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(releaseTestBinary.path, const [], null), + ])); + }); + + test('fails if there are no unit tests', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + }); + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--windows', + '--no-integration', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No test binaries found.'), + ]), + ); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('fails if a unit test fails', () async { + const String testBinaryRelativePath = + 'build/windows/foo/Release/bar/plugin_test.exe'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, extraFiles: [ + 'example/$testBinaryRelativePath' + ], platformSupport: { + kPlatformWindows: const PlatformDetails(PlatformSupport.inline), + }); + + final File testBinary = childFileWithSubcomponents(pluginDirectory, + ['example', ...testBinaryRelativePath.split('/')]); + + processRunner.mockProcessesForExecutable[testBinary.path] = + [MockProcess(exitCode: 1)]; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'native-test', + '--windows', + '--no-integration', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Running plugin_test.exe...'), + ]), + ); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(testBinary.path, const [], null), + ])); + }); + }); + }); +} diff --git a/script/tool/test/publish_check_command_test.dart b/script/tool/test/publish_check_command_test.dart new file mode 100644 index 000000000000..c5527af21736 --- /dev/null +++ b/script/tool/test/publish_check_command_test.dart @@ -0,0 +1,414 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/publish_check_command.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('$PublishCheckCommand tests', () { + FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late RecordingProcessRunner processRunner; + late CommandRunner runner; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final PublishCheckCommand publishCheckCommand = PublishCheckCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = CommandRunner( + 'publish_check_command', + 'Test for publish-check command.', + ); + runner.addCommand(publishCheckCommand); + }); + + test('publish check all packages', () async { + final Directory plugin1Dir = + createFakePlugin('plugin_tools_test_package_a', packagesDir); + final Directory plugin2Dir = + createFakePlugin('plugin_tools_test_package_b', packagesDir); + + await runCapturingPrint(runner, ['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('plugin_tools_test_package_a', packagesDir); + + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess(exitCode: 1, stdout: 'Some error from pub') + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['publish-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Some error from pub'), + contains('Unable to publish plugin_tools_test_package_a'), + ]), + ); + }); + + test('fail on bad pubspec', () async { + final Directory dir = createFakePlugin('c', packagesDir); + await dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['publish-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No valid pubspec found.'), + ]), + ); + }); + + test('fails if AUTHORS is missing', () async { + final Directory package = createFakePackage('a_package', packagesDir); + package.childFile('AUTHORS').delete(); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['publish-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'No AUTHORS file found. Packages must include an AUTHORS file.'), + ]), + ); + }); + + test('does not require AUTHORS for third-party', () async { + final Directory package = createFakePackage( + 'a_package', + packagesDir.parent + .childDirectory('third_party') + .childDirectory('packages')); + package.childFile('AUTHORS').delete(); + + final List output = + await runCapturingPrint(runner, ['publish-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for a_package'), + ]), + ); + }); + + test('pass on prerelease if --allow-pre-release flag is on', () async { + createFakePlugin('d', packagesDir); + + final MockProcess process = MockProcess( + exitCode: 1, + stdout: 'Package has 1 warning.\n' + 'Packages with an SDK constraint on a pre-release of the Dart ' + 'SDK should themselves be published as a pre-release version.'); + processRunner.mockProcessesForExecutable['flutter'] = [ + process, + ]; + + expect( + runCapturingPrint( + runner, ['publish-check', '--allow-pre-release']), + completes); + }); + + test('fail on prerelease if --allow-pre-release flag is off', () async { + createFakePlugin('d', packagesDir); + + final MockProcess process = MockProcess( + exitCode: 1, + stdout: 'Package has 1 warning.\n' + 'Packages with an SDK constraint on a pre-release of the Dart ' + 'SDK should themselves be published as a pre-release version.'); + processRunner.mockProcessesForExecutable['flutter'] = [ + process, + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['publish-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Packages with an SDK constraint on a pre-release of the Dart SDK'), + contains('Unable to publish d'), + ]), + ); + }); + + test('Success message on stderr is not printed as an error', () async { + createFakePlugin('d', packagesDir); + + processRunner.mockProcessesForExecutable['flutter'] = [ + MockProcess(stdout: 'Package has 0 warnings.'), + ]; + + final List output = + await runCapturingPrint(runner, ['publish-check']); + + expect(output, isNot(contains(contains('ERROR:')))); + }); + + test( + '--machine: Log JSON with status:no-publish and correct human message, if there are no packages need to be published. ', + () async { + const Map httpResponseA = { + 'name': 'a', + 'versions': [ + '0.0.1', + '0.1.0', + ], + }; + + const Map httpResponseB = { + 'name': 'b', + 'versions': [ + '0.0.1', + '0.1.0', + '0.2.0', + ], + }; + + final MockClient mockClient = MockClient((http.Request request) async { + if (request.url.pathSegments.last == 'no_publish_a.json') { + return http.Response(json.encode(httpResponseA), 200); + } else if (request.url.pathSegments.last == 'no_publish_b.json') { + return http.Response(json.encode(httpResponseB), 200); + } + return http.Response('', 500); + }); + final PublishCheckCommand command = PublishCheckCommand(packagesDir, + processRunner: processRunner, httpClient: mockClient); + + runner = CommandRunner( + 'publish_check_command', + 'Test for publish-check command.', + ); + runner.addCommand(command); + + createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); + createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); + + final List output = await runCapturingPrint( + runner, ['publish-check', '--machine']); + + expect(output.first, r''' +{ + "status": "no-publish", + "humanMessage": [ + "\n============================================================\n|| Running for no_publish_a\n============================================================\n", + "Package no_publish_a version: 0.1.0 has already be published on pub.", + "\n============================================================\n|| Running for no_publish_b\n============================================================\n", + "Package no_publish_b version: 0.2.0 has already be published on pub.", + "\n", + "------------------------------------------------------------", + "Run overview:", + " no_publish_a - ran", + " no_publish_b - ran", + "", + "Ran for 2 package(s)", + "\n", + "No issues found!" + ] +}'''); + }); + + test( + '--machine: Log JSON with status:needs-publish and correct human message, if there is at least 1 plugin needs to be published.', + () async { + const Map httpResponseA = { + 'name': 'a', + 'versions': [ + '0.0.1', + '0.1.0', + ], + }; + + const Map httpResponseB = { + 'name': 'b', + 'versions': [ + '0.0.1', + '0.1.0', + ], + }; + + final MockClient mockClient = MockClient((http.Request request) async { + if (request.url.pathSegments.last == 'no_publish_a.json') { + return http.Response(json.encode(httpResponseA), 200); + } else if (request.url.pathSegments.last == 'no_publish_b.json') { + return http.Response(json.encode(httpResponseB), 200); + } + return http.Response('', 500); + }); + final PublishCheckCommand command = PublishCheckCommand(packagesDir, + processRunner: processRunner, httpClient: mockClient); + + runner = CommandRunner( + 'publish_check_command', + 'Test for publish-check command.', + ); + runner.addCommand(command); + + createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); + createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); + + final List output = await runCapturingPrint( + runner, ['publish-check', '--machine']); + + expect(output.first, r''' +{ + "status": "needs-publish", + "humanMessage": [ + "\n============================================================\n|| Running for no_publish_a\n============================================================\n", + "Package no_publish_a version: 0.1.0 has already be published on pub.", + "\n============================================================\n|| Running for no_publish_b\n============================================================\n", + "Running pub publish --dry-run:", + "Package no_publish_b is able to be published.", + "\n", + "------------------------------------------------------------", + "Run overview:", + " no_publish_a - ran", + " no_publish_b - ran", + "", + "Ran for 2 package(s)", + "\n", + "No issues found!" + ] +}'''); + }); + + test( + '--machine: Log correct JSON, if there is at least 1 plugin contains error.', + () async { + const Map httpResponseA = { + 'name': 'a', + 'versions': [ + '0.0.1', + '0.1.0', + ], + }; + + const Map httpResponseB = { + 'name': 'b', + 'versions': [ + '0.0.1', + '0.1.0', + ], + }; + + final MockClient mockClient = MockClient((http.Request request) async { + print('url ${request.url}'); + print(request.url.pathSegments.last); + if (request.url.pathSegments.last == 'no_publish_a.json') { + return http.Response(json.encode(httpResponseA), 200); + } else if (request.url.pathSegments.last == 'no_publish_b.json') { + return http.Response(json.encode(httpResponseB), 200); + } + return http.Response('', 500); + }); + final PublishCheckCommand command = PublishCheckCommand(packagesDir, + processRunner: processRunner, httpClient: mockClient); + + runner = CommandRunner( + 'publish_check_command', + 'Test for publish-check command.', + ); + runner.addCommand(command); + + final Directory plugin1Dir = + createFakePlugin('no_publish_a', packagesDir, version: '0.1.0'); + createFakePlugin('no_publish_b', packagesDir, version: '0.2.0'); + + await plugin1Dir.childFile('pubspec.yaml').writeAsString('bad-yaml'); + + bool hasError = false; + final List output = await runCapturingPrint( + runner, ['publish-check', '--machine'], + errorHandler: (Error error) { + expect(error, isA()); + hasError = true; + }); + expect(hasError, isTrue); + + expect(output.first, contains(r''' +{ + "status": "error", + "humanMessage": [ + "\n============================================================\n|| Running for no_publish_a\n============================================================\n", + "Failed to parse `pubspec.yaml` at /packages/no_publish_a/pubspec.yaml: ParsedYamlException:''')); + // This is split into two checks since the details of the YamlException + // aren't controlled by this package, so asserting its exact format would + // make the test fragile to irrelevant changes in those details. + expect(output.first, contains(r''' + "No valid pubspec found.", + "\n============================================================\n|| Running for no_publish_b\n============================================================\n", + "url https://pub.dev/packages/no_publish_b.json", + "no_publish_b.json", + "Running pub publish --dry-run:", + "Package no_publish_b is able to be published.", + "\n", + "The following packages had errors:", + " no_publish_a", + "See above for full details." + ] +}''')); + }); + }); +} diff --git a/script/tool/test/publish_plugin_command_test.dart b/script/tool/test/publish_plugin_command_test.dart new file mode 100644 index 000000000000..14e99a10f365 --- /dev/null +++ b/script/tool/test/publish_plugin_command_test.dart @@ -0,0 +1,889 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/publish_plugin_command.dart'; +import 'package:http/http.dart' as http; +import 'package:http/testing.dart'; +import 'package:mockito/mockito.dart'; +import 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +import 'common/plugin_command_test.mocks.dart'; +import 'mocks.dart'; +import 'util.dart'; + +void main() { + final String flutterCommand = getFlutterCommand(const LocalPlatform()); + + late Directory packagesDir; + late MockGitDir gitDir; + late TestProcessRunner processRunner; + late CommandRunner commandRunner; + late MockStdin mockStdin; + late FileSystem fileSystem; + // Map of package name to mock response. + late Map> mockHttpResponses; + + void _createMockCredentialFile() { + final String credentialPath = PublishPluginCommand.getCredentialPath(); + fileSystem.file(credentialPath) + ..createSync(recursive: true) + ..writeAsStringSync('some credential'); + } + + setUp(() async { + fileSystem = MemoryFileSystem(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = TestProcessRunner(); + + mockHttpResponses = >{}; + final MockClient mockClient = MockClient((http.Request request) async { + final String packageName = + request.url.pathSegments.last.replaceAll('.json', ''); + final Map? response = mockHttpResponses[packageName]; + if (response != null) { + return http.Response(json.encode(response), 200); + } + // Default to simulating the plugin never having been published. + return http.Response('', 404); + }); + + gitDir = MockGitDir(); + when(gitDir.path).thenReturn(packagesDir.parent.path); + when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) + .thenAnswer((Invocation invocation) { + final List arguments = + invocation.positionalArguments[0]! as List; + // Route git calls through the process runner, to make mock output + // consistent with outer processes. Attach the first argument to the + // command to make targeting the mock results easier. + final String gitCommand = arguments.removeAt(0); + return processRunner.run('git-$gitCommand', arguments); + }); + + mockStdin = MockStdin(); + commandRunner = CommandRunner('tester', '') + ..addCommand(PublishPluginCommand( + packagesDir, + processRunner: processRunner, + stdinput: mockStdin, + gitDir: gitDir, + httpClient: mockClient, + )); + }); + + group('Initial validation', () { + test('refuses to proceed with dirty files', () async { + final Directory pluginDir = + createFakePlugin('foo', packagesDir, examples: []); + + processRunner.mockProcessesForExecutable['git-status'] = [ + MockProcess(stdout: '?? ${pluginDir.childFile('tmp').path}\n') + ]; + + Error? commandError; + final List output = + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--packages=foo', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('There are files in the package directory that haven\'t ' + 'been saved in git. Refusing to publish these files:\n\n' + '?? /packages/foo/tmp\n\n' + 'If the directory should be clean, you can run `git clean -xdf && ' + 'git reset --hard HEAD` to wipe all local changes.'), + contains('foo:\n' + ' uncommitted changes'), + ])); + }); + + test('fails immediately if the remote doesn\'t exist', () async { + createFakePlugin('foo', packagesDir, examples: []); + + processRunner.mockProcessesForExecutable['git-remote'] = [ + MockProcess(exitCode: 1), + ]; + + Error? commandError; + final List output = await runCapturingPrint( + commandRunner, ['publish-plugin', '--packages=foo'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Unable to find URL for remote upstream; cannot push tags'), + ])); + }); + }); + + group('Publishes package', () { + test('while showing all output from pub publish to the user', () async { + createFakePlugin('plugin1', packagesDir, examples: []); + createFakePlugin('plugin2', packagesDir, examples: []); + + processRunner.mockProcessesForExecutable[flutterCommand] = [ + MockProcess( + stdout: 'Foo', + stderr: 'Bar', + stdoutEncoding: utf8, + stderrEncoding: utf8), // pub publish for plugin1 + MockProcess( + stdout: 'Baz', + stdoutEncoding: utf8, + stderrEncoding: utf8), // pub publish for plugin1 + ]; + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--packages=plugin1,plugin2']); + + expect( + output, + containsAllInOrder([ + contains('Running `pub publish ` in /packages/plugin1...'), + contains('Foo'), + contains('Bar'), + contains('Package published!'), + contains('Running `pub publish ` in /packages/plugin2...'), + contains('Baz'), + contains('Package published!'), + ])); + }); + + test('forwards input from the user to `pub publish`', () async { + createFakePlugin('foo', packagesDir, examples: []); + + mockStdin.mockUserInputs.add(utf8.encode('user input')); + + await runCapturingPrint( + commandRunner, ['publish-plugin', '--packages=foo']); + + expect(processRunner.mockPublishProcess.stdinMock.lines, + contains('user input')); + }); + + test('forwards --pub-publish-flags to pub publish', () async { + final Directory pluginDir = + createFakePlugin('foo', packagesDir, examples: []); + + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--packages=foo', + '--pub-publish-flags', + '--dry-run,--server=bar' + ]); + + expect( + processRunner.recordedCalls, + contains(ProcessCall( + flutterCommand, + const ['pub', 'publish', '--dry-run', '--server=bar'], + pluginDir.path))); + }); + + test( + '--skip-confirmation flag automatically adds --force to --pub-publish-flags', + () async { + _createMockCredentialFile(); + final Directory pluginDir = + createFakePlugin('foo', packagesDir, examples: []); + + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--packages=foo', + '--skip-confirmation', + '--pub-publish-flags', + '--server=bar' + ]); + + expect( + processRunner.recordedCalls, + contains(ProcessCall( + flutterCommand, + const ['pub', 'publish', '--server=bar', '--force'], + pluginDir.path))); + }); + + test('throws if pub publish fails', () async { + createFakePlugin('foo', packagesDir, examples: []); + + processRunner.mockProcessesForExecutable[flutterCommand] = [ + MockProcess(exitCode: 128) // pub publish + ]; + + Error? commandError; + final List output = + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--packages=foo', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Publishing foo failed.'), + ])); + }); + + test('publish, dry run', () async { + final Directory pluginDir = + createFakePlugin('foo', packagesDir, examples: []); + + final List output = + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--packages=foo', + '--dry-run', + ]); + + expect( + processRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); + expect( + output, + containsAllInOrder([ + contains('=============== DRY RUN ==============='), + contains('Running for foo'), + contains('Running `pub publish ` in ${pluginDir.path}...'), + contains('Tagging release foo-v0.0.1...'), + contains('Pushing tag to upstream...'), + contains('Published foo successfully!'), + ])); + }); + + test('can publish non-flutter package', () async { + const String packageName = 'a_package'; + createFakePackage(packageName, packagesDir); + + final List output = + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--packages=$packageName', + ]); + + expect( + output, + containsAllInOrder( + [ + contains('Running `pub publish ` in /packages/a_package...'), + contains('Package published!'), + ], + ), + ); + }); + }); + + group('Tags release', () { + test('with the version and name from the pubspec.yaml', () async { + createFakePlugin('foo', packagesDir, examples: []); + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--packages=foo', + ]); + + expect(processRunner.recordedCalls, + contains(const ProcessCall('git-tag', ['foo-v0.0.1'], null))); + }); + + test('only if publishing succeeded', () async { + createFakePlugin('foo', packagesDir, examples: []); + + processRunner.mockProcessesForExecutable[flutterCommand] = [ + MockProcess(exitCode: 128) // pub publish + ]; + + Error? commandError; + final List output = + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--packages=foo', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Publishing foo failed.'), + ])); + expect( + processRunner.recordedCalls, + isNot(contains( + const ProcessCall('git-tag', ['foo-v0.0.1'], null)))); + }); + }); + + group('Pushes tags', () { + test('to upstream by default', () async { + createFakePlugin('foo', packagesDir, examples: []); + + mockStdin.readLineOutput = 'y'; + + final List output = + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--packages=foo', + ]); + + expect( + processRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'foo-v0.0.1'], null))); + expect( + output, + containsAllInOrder([ + contains('Pushing tag to upstream...'), + contains('Published foo successfully!'), + ])); + }); + + test('does not ask for user input if the --skip-confirmation flag is on', + () async { + _createMockCredentialFile(); + createFakePlugin('foo', packagesDir, examples: []); + + final List output = + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--skip-confirmation', + '--packages=foo', + ]); + + expect( + processRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'foo-v0.0.1'], null))); + expect( + output, + containsAllInOrder([ + contains('Published foo successfully!'), + ])); + }); + + test('to upstream by default, dry run', () async { + final Directory pluginDir = + createFakePlugin('foo', packagesDir, examples: []); + + mockStdin.readLineOutput = 'y'; + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--packages=foo', '--dry-run']); + + expect( + processRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); + expect( + output, + containsAllInOrder([ + contains('=============== DRY RUN ==============='), + contains('Running `pub publish ` in ${pluginDir.path}...'), + contains('Tagging release foo-v0.0.1...'), + contains('Pushing tag to upstream...'), + contains('Published foo successfully!'), + ])); + }); + + test('to different remotes based on a flag', () async { + createFakePlugin('foo', packagesDir, examples: []); + + mockStdin.readLineOutput = 'y'; + + final List output = + await runCapturingPrint(commandRunner, [ + 'publish-plugin', + '--packages=foo', + '--remote', + 'origin', + ]); + + expect( + processRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['origin', 'foo-v0.0.1'], null))); + expect( + output, + containsAllInOrder([ + contains('Published foo successfully!'), + ])); + }); + }); + + group('Auto release (all-changed flag)', () { + test('can release newly created plugins', () async { + mockHttpResponses['plugin1'] = { + 'name': 'plugin1', + 'versions': [], + }; + + mockHttpResponses['plugin2'] = { + 'name': 'plugin2', + 'versions': [], + }; + + // Non-federated + final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); + // federated + final Directory pluginDir2 = createFakePlugin( + 'plugin2', + packagesDir.childDirectory('plugin2'), + ); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; + mockStdin.readLineOutput = 'y'; + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + expect( + output, + containsAllInOrder([ + contains( + 'Publishing all packages that have changed relative to "HEAD~"'), + contains('Running `pub publish ` in ${pluginDir1.path}...'), + contains('Running `pub publish ` in ${pluginDir2.path}...'), + contains('plugin1 - \x1B[32mpublished\x1B[0m'), + contains('plugin2/plugin2 - \x1B[32mpublished\x1B[0m'), + ])); + expect( + processRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin1-v0.0.1'], null))); + expect( + processRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin2-v0.0.1'], null))); + }); + + test('can release newly created plugins, while there are existing plugins', + () async { + mockHttpResponses['plugin0'] = { + 'name': 'plugin0', + 'versions': ['0.0.1'], + }; + + mockHttpResponses['plugin1'] = { + 'name': 'plugin1', + 'versions': [], + }; + + mockHttpResponses['plugin2'] = { + 'name': 'plugin2', + 'versions': [], + }; + + // The existing plugin. + createFakePlugin('plugin0', packagesDir); + // Non-federated + final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); + // federated + final Directory pluginDir2 = + createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); + + // Git results for plugin0 having been released already, and plugin1 and + // plugin2 being new. + processRunner.mockProcessesForExecutable['git-tag'] = [ + MockProcess(stdout: 'plugin0-v0.0.1\n') + ]; + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; + + mockStdin.readLineOutput = 'y'; + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + expect( + output, + containsAllInOrder([ + 'Running `pub publish ` in ${pluginDir1.path}...\n', + 'Running `pub publish ` in ${pluginDir2.path}...\n', + ])); + expect( + processRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin1-v0.0.1'], null))); + expect( + processRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin2-v0.0.1'], null))); + }); + + test('can release newly created plugins, dry run', () async { + mockHttpResponses['plugin1'] = { + 'name': 'plugin1', + 'versions': [], + }; + + mockHttpResponses['plugin2'] = { + 'name': 'plugin2', + 'versions': [], + }; + + // Non-federated + final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); + // federated + final Directory pluginDir2 = + createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); + + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; + mockStdin.readLineOutput = 'y'; + + final List output = await runCapturingPrint( + commandRunner, [ + 'publish-plugin', + '--all-changed', + '--base-sha=HEAD~', + '--dry-run' + ]); + + expect( + output, + containsAllInOrder([ + contains('=============== DRY RUN ==============='), + contains('Running `pub publish ` in ${pluginDir1.path}...'), + contains('Tagging release plugin1-v0.0.1...'), + contains('Pushing tag to upstream...'), + contains('Published plugin1 successfully!'), + contains('Running `pub publish ` in ${pluginDir2.path}...'), + contains('Tagging release plugin2-v0.0.1...'), + contains('Pushing tag to upstream...'), + contains('Published plugin2 successfully!'), + ])); + expect( + processRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); + }); + + test('version change triggers releases.', () async { + mockHttpResponses['plugin1'] = { + 'name': 'plugin1', + 'versions': ['0.0.1'], + }; + + mockHttpResponses['plugin2'] = { + 'name': 'plugin2', + 'versions': ['0.0.1'], + }; + + // Non-federated + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, version: '0.0.2'); + // federated + final Directory pluginDir2 = createFakePlugin( + 'plugin2', packagesDir.childDirectory('plugin2'), + version: '0.0.2'); + + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; + + mockStdin.readLineOutput = 'y'; + + final List output2 = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( + output2, + containsAllInOrder([ + contains('Running `pub publish ` in ${pluginDir1.path}...'), + contains('Published plugin1 successfully!'), + contains('Running `pub publish ` in ${pluginDir2.path}...'), + contains('Published plugin2 successfully!'), + ])); + expect( + processRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin1-v0.0.2'], null))); + expect( + processRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin2-v0.0.2'], null))); + }); + + test( + 'delete package will not trigger publish but exit the command successfully!', + () async { + mockHttpResponses['plugin1'] = { + 'name': 'plugin1', + 'versions': ['0.0.1'], + }; + + mockHttpResponses['plugin2'] = { + 'name': 'plugin2', + 'versions': ['0.0.1'], + }; + + // Non-federated + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, version: '0.0.2'); + // federated + final Directory pluginDir2 = + createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); + pluginDir2.deleteSync(recursive: true); + + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; + + mockStdin.readLineOutput = 'y'; + + final List output2 = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + expect( + output2, + containsAllInOrder([ + contains('Running `pub publish ` in ${pluginDir1.path}...'), + contains('Published plugin1 successfully!'), + contains( + 'The pubspec file at ${pluginDir2.childFile('pubspec.yaml').path} does not exist. Publishing will not happen for plugin2.\nSafe to ignore if the package is deleted in this commit.\n'), + contains('SKIPPING: package deleted'), + contains('skipped (with warning)'), + ])); + expect( + processRunner.recordedCalls, + contains(const ProcessCall( + 'git-push', ['upstream', 'plugin1-v0.0.2'], null))); + }); + + test('Existing versions do not trigger release, also prints out message.', + () async { + mockHttpResponses['plugin1'] = { + 'name': 'plugin1', + 'versions': ['0.0.2'], + }; + + mockHttpResponses['plugin2'] = { + 'name': 'plugin2', + 'versions': ['0.0.2'], + }; + + // Non-federated + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, version: '0.0.2'); + // federated + final Directory pluginDir2 = createFakePlugin( + 'plugin2', packagesDir.childDirectory('plugin2'), + version: '0.0.2'); + + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; + processRunner.mockProcessesForExecutable['git-tag'] = [ + MockProcess( + stdout: 'plugin1-v0.0.2\n' + 'plugin2-v0.0.2\n') + ]; + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + expect( + output, + containsAllInOrder([ + contains('plugin1 0.0.2 has already been published'), + contains('SKIPPING: already published'), + contains('plugin2 0.0.2 has already been published'), + contains('SKIPPING: already published'), + ])); + + expect( + processRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); + }); + + test( + 'Existing versions do not trigger release, but fail if the tags do not exist.', + () async { + mockHttpResponses['plugin1'] = { + 'name': 'plugin1', + 'versions': ['0.0.2'], + }; + + mockHttpResponses['plugin2'] = { + 'name': 'plugin2', + 'versions': ['0.0.2'], + }; + + // Non-federated + final Directory pluginDir1 = + createFakePlugin('plugin1', packagesDir, version: '0.0.2'); + // federated + final Directory pluginDir2 = createFakePlugin( + 'plugin2', packagesDir.childDirectory('plugin2'), + version: '0.0.2'); + + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('pubspec.yaml').path}\n' + '${pluginDir2.childFile('pubspec.yaml').path}\n') + ]; + + Error? commandError; + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('plugin1 0.0.2 has already been published, ' + 'however the git release tag (plugin1-v0.0.2) was not found.'), + contains('plugin2 0.0.2 has already been published, ' + 'however the git release tag (plugin2-v0.0.2) was not found.'), + ])); + expect( + processRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); + }); + + test('No version change does not release any plugins', () async { + // Non-federated + final Directory pluginDir1 = createFakePlugin('plugin1', packagesDir); + // federated + final Directory pluginDir2 = + createFakePlugin('plugin2', packagesDir.childDirectory('plugin2')); + + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess( + stdout: '${pluginDir1.childFile('plugin1.dart').path}\n' + '${pluginDir2.childFile('plugin2.dart').path}\n') + ]; + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + expect(output, containsAllInOrder(['Ran for 0 package(s)'])); + expect( + processRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); + }); + + test('Do not release flutter_plugin_tools', () async { + mockHttpResponses['plugin1'] = { + 'name': 'flutter_plugin_tools', + 'versions': [], + }; + + final Directory flutterPluginTools = + createFakePlugin('flutter_plugin_tools', packagesDir); + processRunner.mockProcessesForExecutable['git-diff'] = [ + MockProcess(stdout: flutterPluginTools.childFile('pubspec.yaml').path) + ]; + + final List output = await runCapturingPrint(commandRunner, + ['publish-plugin', '--all-changed', '--base-sha=HEAD~']); + + expect( + output, + containsAllInOrder([ + contains( + 'SKIPPING: publishing flutter_plugin_tools via the tool is not supported') + ])); + expect( + output.contains( + 'Running `pub publish ` in ${flutterPluginTools.path}...', + ), + isFalse); + expect( + processRunner.recordedCalls + .map((ProcessCall call) => call.executable), + isNot(contains('git-push'))); + }); + }); +} + +/// An extension of [RecordingProcessRunner] that stores 'flutter pub publish' +/// calls so that their input streams can be checked in tests. +class TestProcessRunner extends RecordingProcessRunner { + // Most recent returned publish process. + late MockProcess mockPublishProcess; + + @override + Future start(String executable, List args, + {Directory? workingDirectory}) async { + final io.Process process = + await super.start(executable, args, workingDirectory: workingDirectory); + if (executable == getFlutterCommand(const LocalPlatform()) && + args.isNotEmpty && + args[0] == 'pub' && + args[1] == 'publish') { + mockPublishProcess = process as MockProcess; + } + return process; + } +} + +class MockStdin extends Mock implements io.Stdin { + List> mockUserInputs = >[]; + final StreamController> _controller = StreamController>(); + String? readLineOutput; + + @override + Stream transform(StreamTransformer, S> streamTransformer) { + mockUserInputs.forEach(_addUserInputsToSteam); + return _controller.stream.transform(streamTransformer); + } + + @override + StreamSubscription> listen(void 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); +} 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..d09dcebce4af --- /dev/null +++ b/script/tool/test/pubspec_check_command_test.dart @@ -0,0 +1,765 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/pubspec_check_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('test pubspec_check_command', () { + late CommandRunner runner; + late RecordingProcessRunner processRunner; + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); + packagesDir = fileSystem.currentDirectory.childDirectory('packages'); + createPackagesDirectory(parentDir: packagesDir.parent); + processRunner = RecordingProcessRunner(); + final PubspecCheckCommand command = PubspecCheckCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = CommandRunner( + 'pubspec_check_command', 'Test for pubspec_check_command'); + runner.addCommand(command); + }); + + /// Returns the top section of a pubspec.yaml for a package named [name], + /// for either a flutter/packages or flutter/plugins package depending on + /// the values of [isPlugin]. + /// + /// By default it will create a header that includes all of the expected + /// values, elements can be changed via arguments to create incorrect + /// entries. + /// + /// If [includeRepository] is true, by default the path in the link will + /// be "packages/[name]"; a different "packages"-relative path can be + /// provided with [repositoryPackagesDirRelativePath]. + String headerSection( + String name, { + bool isPlugin = false, + bool includeRepository = true, + String? repositoryPackagesDirRelativePath, + bool includeHomepage = false, + bool includeIssueTracker = true, + bool publishable = true, + String? description, + }) { + final String repositoryPath = repositoryPackagesDirRelativePath ?? name; + final String repoLink = 'https://github.com/flutter/' + '${isPlugin ? 'plugins' : 'packages'}/tree/master/' + 'packages/$repositoryPath'; + final String issueTrackerLink = + 'https://github.com/flutter/flutter/issues?' + 'q=is%3Aissue+is%3Aopen+label%3A%22p%3A+$name%22'; + description ??= 'A test package for validating that the pubspec.yaml ' + 'follows repo best practices.'; + return ''' +name: $name +description: $description +${includeRepository ? 'repository: $repoLink' : ''} +${includeHomepage ? 'homepage: $repoLink' : ''} +${includeIssueTracker ? 'issue_tracker: $issueTrackerLink' : ''} +version: 1.0.0 +${publishable ? '' : 'publish_to: \'none\''} +'''; + } + + String environmentSection() { + return ''' +environment: + sdk: ">=2.12.0 <3.0.0" + flutter: ">=2.0.0" +'''; + } + + String flutterSection({ + bool isPlugin = false, + String? implementedPackage, + }) { + final String pluginEntry = ''' + plugin: +${implementedPackage == null ? '' : ' implements: $implementedPackage'} + platforms: +'''; + return ''' +flutter: +${isPlugin ? pluginEntry : ''} +'''; + } + + String dependenciesSection() { + return ''' +dependencies: + flutter: + sdk: flutter +'''; + } + + String devDependenciesSection() { + return ''' +dev_dependencies: + flutter_test: + sdk: flutter +'''; + } + + String falseSecretsSection() { + return ''' +false_secrets: + - /lib/main.dart +'''; + } + + test('passes for a plugin following conventions', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +${falseSecretsSection()} +'''); + + final List output = await runCapturingPrint(runner, [ + 'pubspec-check', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin...'), + contains('Running for plugin/example...'), + contains('No issues found!'), + ]), + ); + }); + + test('passes for a Flutter package following conventions', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin')} +${environmentSection()} +${dependenciesSection()} +${devDependenciesSection()} +${flutterSection()} +${falseSecretsSection()} +'''); + + final List output = await runCapturingPrint(runner, [ + 'pubspec-check', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin...'), + contains('Running for plugin/example...'), + contains('No issues found!'), + ]), + ); + }); + + test('passes for a 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([ + contains('Running for package...'), + contains('No issues found!'), + ]), + ); + }); + + test('fails when homepage is included', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, includeHomepage: true)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Found a "homepage" entry; only "repository" should be used.'), + ]), + ); + }); + + test('fails when repository is missing', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, includeRepository: false)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Missing "repository"'), + ]), + ); + }); + + test('fails when homepage is given instead of repository', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, includeHomepage: true, includeRepository: false)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Found a "homepage" entry; only "repository" should be used.'), + ]), + ); + }); + + test('fails when repository is incorrect', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, repositoryPackagesDirRelativePath: 'different_plugin')} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The "repository" link should end with the package path.'), + ]), + ); + }); + + test('fails when issue tracker is missing', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, includeIssueTracker: false)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('A package should have an "issue_tracker" link'), + ]), + ); + }); + + test('fails when description is too short', () async { + final Directory pluginDirectory = + createFakePlugin('a_plugin', packagesDir.childDirectory('a_plugin')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, description: 'Too short')} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('"description" is too short. pub.dev recommends package ' + 'descriptions of 60-180 characters.'), + ]), + ); + }); + + test( + 'allows short descriptions for non-app-facing parts of federated plugins', + () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, description: 'Too short')} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('"description" is too short. pub.dev recommends package ' + 'descriptions of 60-180 characters.'), + ]), + ); + }); + + test('fails when description is too long', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + const String description = 'This description is too long. It just goes ' + 'on and on and on and on and on. pub.dev will down-score it because ' + 'there is just too much here. Someone shoul really cut this down to just ' + 'the core description so that search results are more useful and the ' + 'package does not lose pub points.'; + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true, description: description)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('"description" is too long. pub.dev recommends package ' + 'descriptions of 60-180 characters.'), + ]), + ); + }); + + test('fails when environment section is out of order', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true)} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +${environmentSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Major sections should follow standard repository ordering:'), + ]), + ); + }); + + test('fails when flutter section is out of order', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true)} +${flutterSection(isPlugin: true)} +${environmentSection()} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Major sections should follow standard repository ordering:'), + ]), + ); + }); + + test('fails when dependencies section is out of order', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${devDependenciesSection()} +${dependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Major sections should follow standard repository ordering:'), + ]), + ); + }); + + test('fails when dev_dependencies section is out of order', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true)} +${environmentSection()} +${devDependenciesSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Major sections should follow standard repository ordering:'), + ]), + ); + }); + + test('fails when false_secrets section is out of order', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${falseSecretsSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Major sections should follow standard repository ordering:'), + ]), + ); + }); + + test('fails when an implemenation package is missing "implements"', + () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin_a_foo', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Missing "implements: plugin_a" in "plugin" section.'), + ]), + ); + }); + + test('fails when an implemenation package has the wrong "implements"', + () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin_a_foo', isPlugin: true)} +${environmentSection()} +${flutterSection(isPlugin: true, implementedPackage: 'plugin_a_foo')} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Expecetd "implements: plugin_a"; ' + 'found "implements: plugin_a_foo".'), + ]), + ); + }); + + test('passes for a correct implemenation package', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection( + 'plugin_a_foo', + isPlugin: true, + repositoryPackagesDirRelativePath: 'plugin_a/plugin_a_foo', + )} +${environmentSection()} +${flutterSection(isPlugin: true, implementedPackage: 'plugin_a')} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final List output = + await runCapturingPrint(runner, ['pubspec-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin_a_foo...'), + contains('No issues found!'), + ]), + ); + }); + + test('passes for an app-facing package without "implements"', () async { + final Directory pluginDirectory = + createFakePlugin('plugin_a', packagesDir.childDirectory('plugin_a')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection( + 'plugin_a', + isPlugin: true, + repositoryPackagesDirRelativePath: 'plugin_a/plugin_a', + )} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final List output = + await runCapturingPrint(runner, ['pubspec-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin_a/plugin_a...'), + contains('No issues found!'), + ]), + ); + }); + + test('passes for a platform interface package without "implements"', + () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin_a_platform_interface', + packagesDir.childDirectory('plugin_a')); + + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection( + 'plugin_a_platform_interface', + isPlugin: true, + repositoryPackagesDirRelativePath: + 'plugin_a/plugin_a_platform_interface', + )} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final List output = + await runCapturingPrint(runner, ['pubspec-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin_a_platform_interface...'), + contains('No issues found!'), + ]), + ); + }); + + test('validates some properties even for unpublished packages', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin_a_foo', packagesDir.childDirectory('plugin_a')); + + // Environment section is in the wrong location. + // Missing 'implements'. + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection('plugin_a_foo', isPlugin: true, publishable: false)} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +${environmentSection()} +'''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['pubspec-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Major sections should follow standard repository ordering:'), + contains('Missing "implements: plugin_a" in "plugin" section.'), + ]), + ); + }); + + test('ignores some checks for unpublished packages', () async { + final Directory pluginDirectory = createFakePlugin('plugin', packagesDir); + + // Missing metadata that is only useful for published packages, such as + // repository and issue tracker. + pluginDirectory.childFile('pubspec.yaml').writeAsStringSync(''' +${headerSection( + 'plugin', + isPlugin: true, + publishable: false, + includeRepository: false, + includeIssueTracker: false, + )} +${environmentSection()} +${flutterSection(isPlugin: true)} +${dependenciesSection()} +${devDependenciesSection()} +'''); + + final List output = + await runCapturingPrint(runner, ['pubspec-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin...'), + contains('No issues found!'), + ]), + ); + }); + }); +} diff --git a/script/tool/test/test_command_test.dart b/script/tool/test/test_command_test.dart new file mode 100644 index 000000000000..f8aca38d3478 --- /dev/null +++ b/script/tool/test/test_command_test.dart @@ -0,0 +1,226 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:flutter_plugin_tools/src/test_command.dart'; +import 'package:platform/platform.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +void main() { + group('$TestCommand', () { + late FileSystem fileSystem; + late Platform mockPlatform; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final TestCommand command = TestCommand( + packagesDir, + processRunner: processRunner, + platform: mockPlatform, + ); + + runner = CommandRunner('test_test', 'Test for $TestCommand'); + runner.addCommand(command); + }); + + test('runs flutter test on each plugin', () async { + final Directory plugin1Dir = createFakePlugin('plugin1', packagesDir, + extraFiles: ['test/empty_test.dart']); + final Directory plugin2Dir = createFakePlugin('plugin2', packagesDir, + extraFiles: ['test/empty_test.dart']); + + await runCapturingPrint(runner, ['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['test', '--color'], plugin1Dir.path), + ProcessCall(getFlutterCommand(mockPlatform), + const ['test', '--color'], plugin2Dir.path), + ]), + ); + }); + + test('fails when Flutter tests fail', () async { + createFakePlugin('plugin1', packagesDir, + extraFiles: ['test/empty_test.dart']); + createFakePlugin('plugin2', packagesDir, + extraFiles: ['test/empty_test.dart']); + + processRunner + .mockProcessesForExecutable[getFlutterCommand(mockPlatform)] = + [ + MockProcess(exitCode: 1), // plugin 1 test + MockProcess(), // plugin 2 test + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['test'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains(' plugin1'), + ])); + }); + + test('skips testing plugins without test directory', () async { + createFakePlugin('plugin1', packagesDir); + final Directory plugin2Dir = createFakePlugin('plugin2', packagesDir, + extraFiles: ['test/empty_test.dart']); + + await runCapturingPrint(runner, ['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall(getFlutterCommand(mockPlatform), + const ['test', '--color'], plugin2Dir.path), + ]), + ); + }); + + test('runs pub run test on non-Flutter packages', () async { + final Directory pluginDir = createFakePlugin('a', packagesDir, + extraFiles: ['test/empty_test.dart']); + final Directory packageDir = createFakePackage('b', packagesDir, + extraFiles: ['test/empty_test.dart']); + + await runCapturingPrint( + runner, ['test', '--enable-experiment=exp1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const ['test', '--color', '--enable-experiment=exp1'], + pluginDir.path), + ProcessCall('dart', const ['pub', 'get'], packageDir.path), + ProcessCall( + 'dart', + const ['pub', 'run', '--enable-experiment=exp1', 'test'], + packageDir.path), + ]), + ); + }); + + test('fails when getting non-Flutter package dependencies fails', () async { + createFakePackage('a_package', packagesDir, + extraFiles: ['test/empty_test.dart']); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess(exitCode: 1), // dart pub get + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['test'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to fetch dependencies'), + contains('The following packages had errors:'), + contains(' a_package'), + ])); + }); + + test('fails when non-Flutter tests fail', () async { + createFakePackage('a_package', packagesDir, + extraFiles: ['test/empty_test.dart']); + + processRunner.mockProcessesForExecutable['dart'] = [ + MockProcess(), // dart pub get + MockProcess(exitCode: 1), // dart pub run test + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['test'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains(' a_package'), + ])); + }); + + test('runs on Chrome for web plugins', () async { + final Directory pluginDir = createFakePlugin( + 'plugin', + packagesDir, + extraFiles: ['test/empty_test.dart'], + platformSupport: { + kPlatformWeb: const PlatformDetails(PlatformSupport.inline), + }, + ); + + await runCapturingPrint(runner, ['test']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const ['test', '--color', '--platform=chrome'], + pluginDir.path), + ]), + ); + }); + + test('enable-experiment flag', () async { + final Directory pluginDir = createFakePlugin('a', packagesDir, + extraFiles: ['test/empty_test.dart']); + final Directory packageDir = createFakePackage('b', packagesDir, + extraFiles: ['test/empty_test.dart']); + + await runCapturingPrint( + runner, ['test', '--enable-experiment=exp1']); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + getFlutterCommand(mockPlatform), + const ['test', '--color', '--enable-experiment=exp1'], + pluginDir.path), + ProcessCall('dart', const ['pub', 'get'], packageDir.path), + ProcessCall( + 'dart', + const ['pub', 'run', '--enable-experiment=exp1', 'test'], + packageDir.path), + ]), + ); + }); + }); +} diff --git a/script/tool/test/util.dart b/script/tool/test/util.dart new file mode 100644 index 000000000000..9abb34bef35a --- /dev/null +++ b/script/tool/test/util.dart @@ -0,0 +1,411 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/file_utils.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:flutter_plugin_tools/src/common/process_runner.dart'; +import 'package:meta/meta.dart'; +import 'package:path/path.dart' as p; +import 'package:platform/platform.dart'; +import 'package:quiver/collection.dart'; + +import 'mocks.dart'; + +/// Returns the exe name that command will use when running Flutter on +/// [platform]. +String getFlutterCommand(Platform platform) => + platform.isWindows ? 'flutter.bat' : 'flutter'; + +/// Creates a packages directory in the given location. +/// +/// If [parentDir] is set the packages directory will be created there, +/// otherwise [fileSystem] must be provided and it will be created an arbitrary +/// location in that filesystem. +Directory createPackagesDirectory( + {Directory? parentDir, FileSystem? fileSystem}) { + assert(parentDir != null || fileSystem != null, + 'One of parentDir or fileSystem must be provided'); + assert(fileSystem == null || fileSystem is MemoryFileSystem, + 'If using a real filesystem, parentDir must be provided'); + final Directory packagesDir = + (parentDir ?? fileSystem!.currentDirectory).childDirectory('packages'); + packagesDir.createSync(); + return packagesDir; +} + +/// Details for platform support in a plugin. +@immutable +class PlatformDetails { + const PlatformDetails( + this.type, { + this.variants = const [], + this.hasNativeCode = true, + this.hasDartCode = false, + }); + + /// The type of support for the platform. + final PlatformSupport type; + + /// Any 'supportVariants' to list in the pubspec. + final List variants; + + /// Whether or not the plugin includes native code. + /// + /// Ignored for web, which does not have native code. + final bool hasNativeCode; + + /// Whether or not the plugin includes Dart code. + /// + /// Ignored for web, which always has native code. + final bool hasDartCode; +} + +/// Creates a plugin package with the given [name] in [packagesDirectory]. +/// +/// [platformSupport] is a map of platform string to the support details for +/// that platform. +/// +/// [extraFiles] is an optional list of plugin-relative paths, using Posix +/// separators, of extra files to create in the plugin. +// TODO(stuartmorgan): Convert the return to a RepositoryPackage. +Directory createFakePlugin( + String name, + Directory parentDirectory, { + List examples = const ['example'], + List extraFiles = const [], + Map platformSupport = + const {}, + String? version = '0.0.1', +}) { + final Directory pluginDirectory = createFakePackage(name, parentDirectory, + isFlutter: true, + examples: examples, + extraFiles: extraFiles, + version: version); + + createFakePubspec( + pluginDirectory, + name: name, + isFlutter: true, + isPlugin: true, + platformSupport: platformSupport, + version: version, + ); + + return pluginDirectory; +} + +/// Creates a plugin package with the given [name] in [packagesDirectory]. +/// +/// [extraFiles] is an optional list of package-relative paths, using unix-style +/// separators, of extra files to create in the package. +// TODO(stuartmorgan): Convert the return to a RepositoryPackage. +Directory createFakePackage( + String name, + Directory parentDirectory, { + List examples = const ['example'], + List extraFiles = const [], + bool isFlutter = false, + String? version = '0.0.1', +}) { + final Directory packageDirectory = parentDirectory.childDirectory(name); + packageDirectory.createSync(recursive: true); + + createFakePubspec(packageDirectory, + name: name, isFlutter: isFlutter, version: version); + createFakeCHANGELOG(packageDirectory, ''' +## $version + * Some changes. + '''); + createFakeAuthors(packageDirectory); + + if (examples.length == 1) { + final Directory exampleDir = packageDirectory.childDirectory(examples.first) + ..createSync(); + createFakePubspec(exampleDir, + name: '${name}_example', isFlutter: isFlutter, publishTo: 'none'); + } else if (examples.isNotEmpty) { + final Directory exampleDir = packageDirectory.childDirectory('example') + ..createSync(); + for (final String example in examples) { + final Directory currentExample = exampleDir.childDirectory(example) + ..createSync(); + createFakePubspec(currentExample, + name: example, isFlutter: isFlutter, publishTo: 'none'); + } + } + + final p.Context posixContext = p.posix; + for (final String file in extraFiles) { + childFileWithSubcomponents(packageDirectory, posixContext.split(file)) + .createSync(recursive: true); + } + + return packageDirectory; +} + +void createFakeCHANGELOG(Directory parent, String texts) { + parent.childFile('CHANGELOG.md').createSync(); + parent.childFile('CHANGELOG.md').writeAsStringSync(texts); +} + +/// Creates a `pubspec.yaml` file with a flutter dependency. +/// +/// [platformSupport] is a map of platform string to the support details for +/// that platform. If empty, no `plugin` entry will be created unless `isPlugin` +/// is set to `true`. +void createFakePubspec( + Directory parent, { + String name = 'fake_package', + bool isFlutter = true, + bool isPlugin = false, + Map platformSupport = + const {}, + String publishTo = 'http://no_pub_server.com', + String? version, +}) { + isPlugin |= platformSupport.isNotEmpty; + parent.childFile('pubspec.yaml').createSync(); + String yaml = ''' +name: $name +'''; + if (isFlutter) { + if (isPlugin) { + yaml += ''' +flutter: + plugin: + platforms: +'''; + for (final MapEntry platform + in platformSupport.entries) { + yaml += _pluginPlatformSection(platform.key, platform.value, name); + } + } + yaml += ''' +dependencies: + flutter: + sdk: flutter +'''; + } + if (version != null) { + yaml += ''' +version: $version +'''; + } + if (publishTo.isNotEmpty) { + yaml += ''' +publish_to: $publishTo # Hardcoded safeguard to prevent this from somehow being published by a broken test. +'''; + } + parent.childFile('pubspec.yaml').writeAsStringSync(yaml); +} + +void createFakeAuthors(Directory parent) { + final File authorsFile = parent.childFile('AUTHORS'); + authorsFile.createSync(); + authorsFile.writeAsStringSync('Google Inc.'); +} + +String _pluginPlatformSection( + String platform, PlatformDetails support, String packageName) { + String entry = ''; + // Build the main plugin entry. + if (support.type == PlatformSupport.federated) { + entry = ''' + $platform: + default_package: ${packageName}_$platform +'''; + } else { + final List lines = [ + ' $platform:', + ]; + switch (platform) { + case kPlatformAndroid: + lines.add(' package: io.flutter.plugins.fake'); + continue nativeByDefault; + nativeByDefault: + case kPlatformIos: + case kPlatformLinux: + case kPlatformMacos: + case kPlatformWindows: + if (support.hasNativeCode) { + final String className = + platform == kPlatformIos ? 'FLTFakePlugin' : 'FakePlugin'; + lines.add(' pluginClass: $className'); + } + if (support.hasDartCode) { + lines.add(' dartPluginClass: FakeDartPlugin'); + } + break; + case kPlatformWeb: + lines.addAll([ + ' pluginClass: FakePlugin', + ' fileName: ${packageName}_web.dart', + ]); + break; + default: + assert(false, 'Unrecognized platform: $platform'); + break; + } + entry = lines.join('\n') + '\n'; + } + + // Add any variants. + if (support.variants.isNotEmpty) { + entry += ''' + supportedVariants: +'''; + for (final String variant in support.variants) { + entry += ''' + - $variant +'''; + } + } + + return entry; +} + +typedef _ErrorHandler = void Function(Error error); + +/// Run the command [runner] with the given [args] and return +/// what was printed. +/// A custom [errorHandler] can be used to handle the runner error as desired without throwing. +Future> runCapturingPrint( + CommandRunner runner, List args, + {_ErrorHandler? errorHandler}) async { + 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 { + final List recordedCalls = []; + + /// Maps an executable to a list of processes that should be used for each + /// successive call to it via [run], [runAndStream], or [start]. + final Map> mockProcessesForExecutable = + >{}; + + @override + Future runAndStream( + String executable, + List args, { + Directory? workingDir, + bool exitOnError = false, + }) async { + recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); + final io.Process? processToReturn = _getProcessToReturn(executable); + final int exitCode = + processToReturn == null ? 0 : await processToReturn.exitCode; + if (exitOnError && (exitCode != 0)) { + throw io.ProcessException(executable, args); + } + return Future.value(exitCode); + } + + /// Returns [io.ProcessResult] created from [mockProcessesForExecutable]. + @override + Future run( + String executable, + List args, { + Directory? workingDir, + bool exitOnError = false, + bool logOnError = false, + Encoding stdoutEncoding = io.systemEncoding, + Encoding stderrEncoding = io.systemEncoding, + }) async { + recordedCalls.add(ProcessCall(executable, args, workingDir?.path)); + + final io.Process? process = _getProcessToReturn(executable); + final List? processStdout = + await process?.stdout.transform(stdoutEncoding.decoder).toList(); + final String stdout = processStdout?.join('') ?? ''; + final List? processStderr = + await process?.stderr.transform(stderrEncoding.decoder).toList(); + final String stderr = processStderr?.join('') ?? ''; + + final io.ProcessResult result = process == null + ? io.ProcessResult(1, 0, '', '') + : io.ProcessResult(process.pid, await process.exitCode, stdout, stderr); + + if (exitOnError && (result.exitCode != 0)) { + throw io.ProcessException(executable, args); + } + + return Future.value(result); + } + + @override + Future start(String executable, List args, + {Directory? workingDirectory}) async { + recordedCalls.add(ProcessCall(executable, args, workingDirectory?.path)); + return Future.value( + _getProcessToReturn(executable) ?? MockProcess()); + } + + io.Process? _getProcessToReturn(String executable) { + final List? processes = mockProcessesForExecutable[executable]; + if (processes != null && processes.isNotEmpty) { + return processes.removeAt(0); + } + return null; + } +} + +/// A recorded process call. +@immutable +class ProcessCall { + const ProcessCall(this.executable, this.args, this.workingDir); + + /// The executable that was called. + final String executable; + + /// The arguments passed to [executable] in the call. + final List args; + + /// The working directory this process was called from. + final String? workingDir; + + @override + bool operator ==(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_command_test.dart b/script/tool/test/version_check_command_test.dart new file mode 100644 index 000000000000..39132212d664 --- /dev/null +++ b/script/tool/test/version_check_command_test.dart @@ -0,0 +1,877 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/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/plugin_command_test.mocks.dart'; +import '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(master, newVersion: head); + if (allowed) { + expect(allowedVersions, contains(head)); + if (nextVersionType != null) { + expect(allowedVersions[head], equals(nextVersionType)); + } + } else { + expect(allowedVersions, isNot(contains(head))); + } +} + +class MockProcessResult extends Mock implements io.ProcessResult {} + +void main() { + const String indentation = ' '; + group('$VersionCheckCommand', () { + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + late List> gitDirCommands; + Map gitShowResponses; + late MockGitDir gitDir; + // Ignored if mockHttpResponse is set. + int mockHttpStatus; + Map? mockHttpResponse; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + + gitDirCommands = >[]; + gitShowResponses = {}; + gitDir = MockGitDir(); + when(gitDir.path).thenReturn(packagesDir.parent.path); + when(gitDir.runCommand(any, throwOnError: anyNamed('throwOnError'))) + .thenAnswer((Invocation invocation) { + gitDirCommands.add(invocation.positionalArguments[0] as List); + final MockProcessResult mockProcessResult = MockProcessResult(); + if (invocation.positionalArguments[0][0] == 'show') { + final String? response = + gitShowResponses[invocation.positionalArguments[0][1]]; + if (response == null) { + throw const io.ProcessException('git', ['show']); + } + when(mockProcessResult.stdout as String?) + .thenReturn(response); + } else if (invocation.positionalArguments[0][0] == 'merge-base') { + when(mockProcessResult.stdout as String?) + .thenReturn('abc123'); + } + return Future.value(mockProcessResult); + }); + + // Default to simulating the plugin never having been published. + mockHttpStatus = 404; + mockHttpResponse = null; + final MockClient mockClient = MockClient((http.Request request) async { + return http.Response(json.encode(mockHttpResponse), + mockHttpResponse == null ? mockHttpStatus : 200); + }); + + processRunner = RecordingProcessRunner(); + final VersionCheckCommand command = VersionCheckCommand(packagesDir, + processRunner: processRunner, + platform: mockPlatform, + gitDir: gitDir, + httpClient: mockClient); + + runner = CommandRunner( + 'version_check_command', 'Test for $VersionCheckCommand'); + runner.addCommand(command); + }); + + test('allows valid version', () async { + createFakePlugin('plugin', packagesDir, version: '2.0.0'); + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('1.0.0 -> 2.0.0'), + ]), + ); + expect(gitDirCommands.length, equals(1)); + expect( + gitDirCommands, + containsAll([ + equals(['show', 'master:packages/plugin/pubspec.yaml']), + ])); + }); + + test('denies invalid version', () async { + createFakePlugin('plugin', packagesDir, version: '0.2.0'); + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 0.0.1', + }; + final Future> result = runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + + await expectLater( + result, + throwsA(isA()), + ); + expect(gitDirCommands.length, equals(1)); + expect( + gitDirCommands, + containsAll([ + equals(['show', 'master:packages/plugin/pubspec.yaml']), + ])); + }); + + test('allows valid version without explicit base-sha', () async { + createFakePlugin('plugin', packagesDir, version: '2.0.0'); + gitShowResponses = { + 'abc123:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + final List output = + await runCapturingPrint(runner, ['version-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('1.0.0 -> 2.0.0'), + ]), + ); + }); + + test('allows valid version for new package.', () async { + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + final List output = + await runCapturingPrint(runner, ['version-check']); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('Unable to find previous version at git base.'), + ]), + ); + }); + + test('allows likely reverts.', () async { + createFakePlugin('plugin', packagesDir, version: '0.6.1'); + gitShowResponses = { + 'abc123:packages/plugin/pubspec.yaml': 'version: 0.6.2', + }; + final List output = + await runCapturingPrint(runner, ['version-check']); + + expect( + output, + containsAllInOrder([ + contains('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, version: '0.5.1'); + gitShowResponses = { + 'abc123:packages/plugin/pubspec.yaml': 'version: 0.6.2', + }; + final Future> result = + runCapturingPrint(runner, ['version-check']); + + await expectLater( + result, + throwsA(isA()), + ); + }); + + test('denies invalid version without explicit base-sha', () async { + createFakePlugin('plugin', packagesDir, version: '0.2.0'); + gitShowResponses = { + 'abc123:packages/plugin/pubspec.yaml': 'version: 0.0.1', + }; + final Future> result = + runCapturingPrint(runner, ['version-check']); + + await expectLater( + result, + throwsA(isA()), + ); + }); + + test('allows minor changes to platform interfaces', () async { + createFakePlugin('plugin_platform_interface', packagesDir, + version: '1.1.0'); + gitShowResponses = { + 'master:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.0.0', + }; + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('1.0.0 -> 1.1.0'), + ]), + ); + expect(gitDirCommands.length, equals(1)); + expect( + gitDirCommands, + containsAll([ + equals([ + 'show', + 'master:packages/plugin_platform_interface/pubspec.yaml' + ]), + ])); + }); + + test('disallows breaking changes to platform interfaces by default', + () async { + createFakePlugin('plugin_platform_interface', packagesDir, + version: '2.0.0'); + gitShowResponses = { + 'master:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.0.0', + }; + final Future> output = runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + await expectLater( + output, + throwsA(isA()), + ); + expect(gitDirCommands.length, equals(1)); + expect( + gitDirCommands, + containsAll([ + equals([ + 'show', + 'master:packages/plugin_platform_interface/pubspec.yaml' + ]), + ])); + }); + + test('allows breaking changes to platform interfaces with explanation', + () async { + createFakePlugin('plugin_platform_interface', packagesDir, + version: '2.0.0'); + gitShowResponses = { + 'master:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.0.0', + }; + final File changeDescriptionFile = + fileSystem.file('change_description.txt'); + changeDescriptionFile.writeAsStringSync(''' +Some general PR description + +## Breaking change justification + +This is necessary because of X, Y, and Z + +## Another section'''); + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + '--change-description-file=${changeDescriptionFile.path}' + ]); + + expect( + output, + containsAllInOrder([ + contains('Allowing breaking change to plugin_platform_interface ' + 'due to "## Breaking change justification" in the change ' + 'description.'), + contains('Ran for 1 package(s) (1 with warnings)'), + ]), + ); + }); + + test('throws if a nonexistent change description file is specified', + () async { + createFakePlugin('plugin_platform_interface', packagesDir, + version: '2.0.0'); + gitShowResponses = { + 'master:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.0.0', + }; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + '--change-description-file=a_missing_file.txt' + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('No such file: a_missing_file.txt'), + ]), + ); + }); + + test('allows breaking changes to platform interfaces with bypass flag', + () async { + createFakePlugin('plugin_platform_interface', packagesDir, + version: '2.0.0'); + gitShowResponses = { + 'master:packages/plugin_platform_interface/pubspec.yaml': + 'version: 1.0.0', + }; + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + '--ignore-platform-interface-breaks' + ]); + + expect( + output, + containsAllInOrder([ + contains('Allowing breaking change to plugin_platform_interface due ' + 'to --ignore-platform-interface-breaks'), + contains('Ran for 1 package(s) (1 with warnings)'), + ]), + ); + }); + + test('Allow empty lines in front of the first version in CHANGELOG', + () async { + const String version = '1.0.1'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: version); + const String changelog = ''' + +## $version +* Some changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + ]), + ); + }); + + test('Throws if versions in changelog and pubspec do not match', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: '1.0.1'); + const String changelog = ''' +## 1.0.2 +* Some changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + bool hasError = false; + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + '--against-pub' + ], errorHandler: (Error e) { + expect(e, isA()); + hasError = true; + }); + expect(hasError, isTrue); + + expect( + output, + containsAllInOrder([ + contains('Versions in CHANGELOG.md and pubspec.yaml do not match.'), + ]), + ); + }); + + test('Success if CHANGELOG and pubspec versions match', () async { + const String version = '1.0.1'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: version); + + const String changelog = ''' +## $version +* Some changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + ]), + ); + }); + + test( + 'Fail if pubspec version only matches an older version listed in CHANGELOG', + () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, 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([ + contains('Versions in CHANGELOG.md and pubspec.yaml do not match.'), + ]), + ); + }); + + test('Allow NEXT as a placeholder for gathering CHANGELOG entries', + () async { + const String version = '1.0.0'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: version); + + const String changelog = ''' +## NEXT +* Some changes that won't be published until the next time there's a release. +## $version +* Some other changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + + final List output = await runCapturingPrint( + runner, ['version-check', '--base-sha=master']); + await expectLater( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('Found NEXT; validating next version in the CHANGELOG.'), + ]), + ); + }); + + test('Fail if NEXT appears after a version', () async { + const String version = '1.0.1'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: version); + + const String changelog = ''' +## $version +* Some changes. +## NEXT +* Some changes that should have been folded in 1.0.1. +## 1.0.0 +* Some other changes. +'''; + 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([ + contains('When bumping the version for release, the NEXT section ' + 'should be incorporated into the new version\'s release notes.') + ]), + ); + }); + + test('Fail if NEXT is left in the CHANGELOG when adding a version bump', + () async { + const String version = '1.0.1'; + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: version); + + const String changelog = ''' +## NEXT +* Some changes that should have been folded in 1.0.1. +## $version +* Some changes. +## 1.0.0 +* Some other changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + + 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([ + contains('When bumping the version for release, the NEXT section ' + 'should be incorporated into the new version\'s release notes.'), + contains('plugin:\n' + ' CHANGELOG.md failed validation.'), + ]), + ); + }); + + test('Fail if the version changes without replacing NEXT', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: '1.0.1'); + + const String changelog = ''' +## NEXT +* Some changes that should be listed as part of 1.0.1. +## 1.0.0 +* Some other changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + + 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([ + contains('When bumping the version for release, the NEXT section ' + 'should be incorporated into the new version\'s release notes.') + ]), + ); + }); + + test( + 'fails gracefully if the version headers are not found due to using the wrong style', + () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## NEXT +* Some changes for a later release. +# 1.0.0 +* Some other changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unable to find a version in CHANGELOG.md'), + contains('The current version should be on a line starting with ' + '"## ", either on the first non-empty line or after a "## NEXT" ' + 'section.'), + ]), + ); + }); + + test('fails gracefully if the version is unparseable', () async { + final Directory pluginDirectory = + createFakePlugin('plugin', packagesDir, version: '1.0.0'); + + const String changelog = ''' +## Alpha +* Some changes. +'''; + createFakeCHANGELOG(pluginDirectory, changelog); + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + + Error? commandError; + final List output = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + ], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('"Alpha" could not be parsed as a version.'), + ]), + ); + }); + + test('allows valid against pub', () async { + mockHttpResponse = { + 'name': 'some_package', + 'versions': [ + '0.0.1', + '0.0.2', + '1.0.0', + ], + }; + + createFakePlugin('plugin', packagesDir, version: '2.0.0'); + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + final List output = await runCapturingPrint(runner, + ['version-check', '--base-sha=master', '--against-pub']); + + expect( + output, + containsAllInOrder([ + contains('plugin: Current largest version on pub: 1.0.0'), + ]), + ); + }); + + test('denies invalid against pub', () async { + mockHttpResponse = { + 'name': 'some_package', + 'versions': [ + '0.0.1', + '0.0.2', + ], + }; + + createFakePlugin('plugin', packagesDir, version: '2.0.0'); + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + + bool hasError = false; + final List result = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + '--against-pub' + ], errorHandler: (Error e) { + expect(e, isA()); + hasError = true; + }); + expect(hasError, isTrue); + + expect( + result, + containsAllInOrder([ + contains(''' +${indentation}Incorrectly updated version. +${indentation}HEAD: 2.0.0, pub: 0.0.2. +${indentation}Allowed versions: {1.0.0: NextVersionType.BREAKING_MAJOR, 0.1.0: NextVersionType.MINOR, 0.0.3: NextVersionType.PATCH}''') + ]), + ); + }); + + test( + 'throw and print error message if http request failed when checking against pub', + () async { + mockHttpStatus = 400; + + createFakePlugin('plugin', packagesDir, version: '2.0.0'); + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + bool hasError = false; + final List result = await runCapturingPrint(runner, [ + 'version-check', + '--base-sha=master', + '--against-pub' + ], errorHandler: (Error e) { + expect(e, isA()); + hasError = true; + }); + expect(hasError, isTrue); + + expect( + result, + containsAllInOrder([ + contains(''' +${indentation}Error fetching version on pub for plugin. +${indentation}HTTP Status 400 +${indentation}HTTP response: null +''') + ]), + ); + }); + + test('when checking against pub, allow any version if http status is 404.', + () async { + mockHttpStatus = 404; + + createFakePlugin('plugin', packagesDir, version: '2.0.0'); + gitShowResponses = { + 'master:packages/plugin/pubspec.yaml': 'version: 1.0.0', + }; + final List result = await runCapturingPrint(runner, + ['version-check', '--base-sha=master', '--against-pub']); + + expect( + result, + containsAllInOrder([ + contains('Unable to find previous version on pub server.'), + ]), + ); + }); + }); + + group('Pre 1.0', () { + test('nextVersion allows patch version', () { + testAllowedVersion('0.12.0', '0.12.0+1', + nextVersionType: NextVersionType.PATCH); + testAllowedVersion('0.12.0+4', '0.12.0+5', + nextVersionType: NextVersionType.PATCH); + }); + + test('nextVersion does not allow jumping patch', () { + testAllowedVersion('0.12.0', '0.12.0+2', allowed: false); + testAllowedVersion('0.12.0+2', '0.12.0+4', allowed: false); + }); + + test('nextVersion does not allow going back', () { + testAllowedVersion('0.12.0', '0.11.0', allowed: false); + testAllowedVersion('0.12.0+2', '0.12.0+1', allowed: false); + testAllowedVersion('0.12.0+1', '0.12.0', allowed: false); + }); + + test('nextVersion allows minor version', () { + testAllowedVersion('0.12.0', '0.12.1', + nextVersionType: NextVersionType.MINOR); + testAllowedVersion('0.12.0+4', '0.12.1', + nextVersionType: NextVersionType.MINOR); + }); + + test('nextVersion does not allow jumping minor', () { + testAllowedVersion('0.12.0', '0.12.2', allowed: false); + testAllowedVersion('0.12.0+2', '0.12.3', allowed: false); + }); + }); + + group('Releasing 1.0', () { + test('nextVersion allows releasing 1.0', () { + testAllowedVersion('0.12.0', '1.0.0', + nextVersionType: NextVersionType.BREAKING_MAJOR); + testAllowedVersion('0.12.0+4', '1.0.0', + nextVersionType: NextVersionType.BREAKING_MAJOR); + }); + + test('nextVersion does not allow jumping major', () { + testAllowedVersion('0.12.0', '2.0.0', allowed: false); + testAllowedVersion('0.12.0+4', '2.0.0', allowed: false); + }); + + test('nextVersion does not allow un-releasing', () { + testAllowedVersion('1.0.0', '0.12.0+4', allowed: false); + testAllowedVersion('1.0.0', '0.12.0', allowed: false); + }); + }); + + group('Post 1.0', () { + test('nextVersion allows patch jumps', () { + testAllowedVersion('1.0.1', '1.0.2', + nextVersionType: NextVersionType.PATCH); + testAllowedVersion('1.0.0', '1.0.1', + nextVersionType: NextVersionType.PATCH); + }); + + test('nextVersion does not allow build jumps', () { + testAllowedVersion('1.0.1', '1.0.1+1', allowed: false); + testAllowedVersion('1.0.0+5', '1.0.0+6', allowed: false); + }); + + test('nextVersion does not allow skipping patches', () { + testAllowedVersion('1.0.1', '1.0.3', allowed: false); + testAllowedVersion('1.0.0', '1.0.6', allowed: false); + }); + + test('nextVersion allows minor version jumps', () { + testAllowedVersion('1.0.1', '1.1.0', + nextVersionType: NextVersionType.MINOR); + testAllowedVersion('1.0.0', '1.1.0', + nextVersionType: NextVersionType.MINOR); + }); + + test('nextVersion does not allow skipping minor versions', () { + testAllowedVersion('1.0.1', '1.2.0', allowed: false); + testAllowedVersion('1.1.0', '1.3.0', allowed: false); + }); + + test('nextVersion allows breaking changes', () { + testAllowedVersion('1.0.1', '2.0.0', + nextVersionType: NextVersionType.BREAKING_MAJOR); + testAllowedVersion('1.0.0', '2.0.0', + nextVersionType: NextVersionType.BREAKING_MAJOR); + }); + + test('nextVersion does not allow skipping major versions', () { + testAllowedVersion('1.0.1', '3.0.0', allowed: false); + testAllowedVersion('1.1.0', '2.3.0', allowed: false); + }); + }); +} diff --git a/script/tool/test/xcode_analyze_command_test.dart b/script/tool/test/xcode_analyze_command_test.dart new file mode 100644 index 000000000000..10008ae33a11 --- /dev/null +++ b/script/tool/test/xcode_analyze_command_test.dart @@ -0,0 +1,416 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' as io; + +import 'package:args/command_runner.dart'; +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_plugin_tools/src/common/core.dart'; +import 'package:flutter_plugin_tools/src/common/plugin_utils.dart'; +import 'package:flutter_plugin_tools/src/xcode_analyze_command.dart'; +import 'package:test/test.dart'; + +import 'mocks.dart'; +import 'util.dart'; + +// TODO(stuartmorgan): Rework these tests to use a mock Xcode instead of +// doing all the process mocking and validation. +void main() { + group('test xcode_analyze_command', () { + late FileSystem fileSystem; + late MockPlatform mockPlatform; + late Directory packagesDir; + late CommandRunner runner; + late RecordingProcessRunner processRunner; + + setUp(() { + fileSystem = MemoryFileSystem(); + mockPlatform = MockPlatform(isMacOS: true); + packagesDir = createPackagesDirectory(fileSystem: fileSystem); + processRunner = RecordingProcessRunner(); + final XcodeAnalyzeCommand command = XcodeAnalyzeCommand(packagesDir, + processRunner: processRunner, platform: mockPlatform); + + runner = CommandRunner( + 'xcode_analyze_command', 'Test for xcode_analyze_command'); + runner.addCommand(command); + }); + + test('Fails if no platforms are provided', () async { + Error? commandError; + final List output = await runCapturingPrint( + runner, ['xcode-analyze'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('At least one platform flag must be provided'), + ]), + ); + }); + + group('iOS', () { + test('skip if iOS is not supported', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }); + + final List output = + await runCapturingPrint(runner, ['xcode-analyze', '--ios']); + expect(output, + contains(contains('Not implemented for target platform(s).'))); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skip if iOS is implemented in a federated package', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.federated) + }); + + final List output = + await runCapturingPrint(runner, ['xcode-analyze', '--ios']); + expect(output, + contains(contains('Not implemented for target platform(s).'))); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('runs for iOS plugin', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'xcode-analyze', + '--ios', + ]); + + expect( + output, + containsAllInOrder([ + contains('Running for plugin'), + contains('plugin/example (iOS) passed analysis.') + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-destination', + 'generic/platform=iOS Simulator', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('fails if xcrun fails', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) + }); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(exitCode: 1) + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, + [ + 'xcode-analyze', + '--ios', + ], + errorHandler: (Error e) { + commandError = e; + }, + ); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains(' plugin'), + ])); + }); + }); + + group('macOS', () { + test('skip if macOS is not supported', () async { + createFakePlugin( + 'plugin', + packagesDir, + ); + + final List output = await runCapturingPrint( + runner, ['xcode-analyze', '--macos']); + expect(output, + contains(contains('Not implemented for target platform(s).'))); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('skip if macOS is implemented in a federated package', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.federated), + }); + + final List output = await runCapturingPrint( + runner, ['xcode-analyze', '--macos']); + expect(output, + contains(contains('Not implemented for target platform(s).'))); + expect(processRunner.recordedCalls, orderedEquals([])); + }); + + test('runs for macOS plugin', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'xcode-analyze', + '--macos', + ]); + + expect(output, + contains(contains('plugin/example (macOS) passed analysis.'))); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('fails if xcrun fails', () async { + createFakePlugin('plugin', packagesDir, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }); + + processRunner.mockProcessesForExecutable['xcrun'] = [ + MockProcess(exitCode: 1) + ]; + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['xcode-analyze', '--macos'], + errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('The following packages had errors:'), + contains(' plugin'), + ]), + ); + }); + }); + + group('combined', () { + test('runs both iOS and macOS when supported', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline), + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'xcode-analyze', + '--ios', + '--macos', + ]); + + expect( + output, + containsAll([ + contains('plugin/example (iOS) passed analysis.'), + contains('plugin/example (macOS) passed analysis.'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-destination', + 'generic/platform=iOS Simulator', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('runs only macOS for a macOS plugin', () async { + final Directory pluginDirectory1 = createFakePlugin( + 'plugin', packagesDir, + platformSupport: { + kPlatformMacos: const PlatformDetails(PlatformSupport.inline), + }); + + final Directory pluginExampleDirectory = + pluginDirectory1.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'xcode-analyze', + '--ios', + '--macos', + ]); + + expect( + output, + containsAllInOrder([ + contains('plugin/example (macOS) passed analysis.'), + ])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'macos/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('runs only iOS for a iOS plugin', () async { + final Directory pluginDirectory = createFakePlugin( + 'plugin', packagesDir, platformSupport: { + kPlatformIos: const PlatformDetails(PlatformSupport.inline) + }); + + final Directory pluginExampleDirectory = + pluginDirectory.childDirectory('example'); + + final List output = await runCapturingPrint(runner, [ + 'xcode-analyze', + '--ios', + '--macos', + ]); + + expect( + output, + containsAllInOrder( + [contains('plugin/example (iOS) passed analysis.')])); + + expect( + processRunner.recordedCalls, + orderedEquals([ + ProcessCall( + 'xcrun', + const [ + 'xcodebuild', + 'analyze', + '-workspace', + 'ios/Runner.xcworkspace', + '-scheme', + 'Runner', + '-configuration', + 'Debug', + '-destination', + 'generic/platform=iOS Simulator', + 'GCC_TREAT_WARNINGS_AS_ERRORS=YES', + ], + pluginExampleDirectory.path), + ])); + }); + + test('skips when neither are supported', () async { + createFakePlugin('plugin', packagesDir); + + final List output = await runCapturingPrint(runner, [ + 'xcode-analyze', + '--ios', + '--macos', + ]); + + expect( + output, + containsAllInOrder([ + contains('SKIPPING: Not implemented for target platform(s).'), + ])); + + expect(processRunner.recordedCalls, orderedEquals([])); + }); + }); + }); +} diff --git a/script/tool_runner.sh b/script/tool_runner.sh new file mode 100755 index 000000000000..99bab387e6b6 --- /dev/null +++ b/script/tool_runner.sh @@ -0,0 +1,22 @@ +#!/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 + +# WARNING! Do not remove this script, or change its behavior, unless you have +# verified that it will not break the flutter/flutter analysis run of this +# repository: https://github.com/flutter/flutter/blob/master/dev/bots/test.dart + +readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null && pwd)" +readonly REPO_DIR="$(dirname "$SCRIPT_DIR")" +readonly TOOL_PATH="$REPO_DIR/script/tool/bin/flutter_plugin_tools.dart" + +# Ensure that the tool dependencies have been fetched. +(pushd "$REPO_DIR/script/tool" && dart pub get && popd) >/dev/null + +# The tool expects to be run from the repo root. +cd "$REPO_DIR" +# Run from the in-tree source. +dart run "$TOOL_PATH" "$@" --packages-for-branch $PLUGIN_SHARDING